Spaces:
Sleeping
Sleeping
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- DOM Elements --- | |
| const chatArea = document.getElementById('chat-area'); | |
| const closeSidebarBtn = document.getElementById('close-sidebar-btn'); | |
| const messageInput = document.getElementById('message-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const newChatBtn = document.getElementById('new-chat-btn'); | |
| const chatMessages = document.getElementById('chat-messages'); | |
| const chatHistoryList = document.getElementById('chat-history-list'); | |
| const menuToggle = document.getElementById('menu-toggle'); | |
| const appContainer = document.getElementById('app-container'); | |
| const chatTitle = document.getElementById('chat-title'); | |
| const imageUploadBtn = document.getElementById('image-upload-btn'); | |
| const imageUploadInput = document.getElementById('image-upload-input'); | |
| const imagePreviewContainer = document.getElementById('image-preview-container'); | |
| const imagePreview = document.getElementById('image-preview'); | |
| const removeImageBtn = document.getElementById('remove-image-btn'); | |
| // Add search and filter elements | |
| let searchInput, clearAllBtn, userStatsDiv; | |
| // --- State --- | |
| let currentChatId = null; | |
| let currentUserId = null; | |
| let conversationsCache = {}; | |
| let selectedImageFile = null; | |
| let imgbbApiKey = ''; | |
| let firebase = null; | |
| let firebaseDB = null; | |
| let useFirebase = false; | |
| // --- User Management --- | |
| const generateUserFingerprint = () => { | |
| // Create a device fingerprint based on available browser info | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.textBaseline = 'top'; | |
| ctx.font = '14px Arial'; | |
| ctx.fillText('Device fingerprint', 2, 2); | |
| const fingerprint = [ | |
| navigator.userAgent, | |
| navigator.language, | |
| navigator.platform, | |
| navigator.cookieEnabled, | |
| navigator.doNotTrack, | |
| screen.width + 'x' + screen.height, | |
| screen.colorDepth, | |
| new Date().getTimezoneOffset(), | |
| canvas.toDataURL(), | |
| navigator.hardwareConcurrency || 'unknown', | |
| navigator.deviceMemory || 'unknown' | |
| ].join('|'); | |
| // Create hash | |
| return hashCode(fingerprint).toString(); | |
| }; | |
| const hashCode = (str) => { | |
| let hash = 0; | |
| for (let i = 0; i < str.length; i++) { | |
| const char = str.charCodeAt(i); | |
| hash = ((hash << 5) - hash) + char; | |
| hash = hash & hash; // Convert to 32bit integer | |
| } | |
| return Math.abs(hash); | |
| }; | |
| const initializeUser = async () => { | |
| // Try to get existing user ID from storage | |
| let userId = localStorage.getItem('easyfarms_user_id'); | |
| if (!userId) { | |
| // Generate new user ID based on device fingerprint | |
| const deviceFingerprint = generateUserFingerprint(); | |
| const timestamp = Date.now(); | |
| const randomPart = Math.random().toString(36).substr(2, 8); | |
| userId = `user_${deviceFingerprint}_${timestamp}_${randomPart}`; | |
| localStorage.setItem('easyfarms_user_id', userId); | |
| console.log('Generated new user ID:', userId); | |
| } else { | |
| console.log('Using existing user ID:', userId); | |
| } | |
| currentUserId = userId; | |
| // Also save to Firebase if available | |
| if (useFirebase && firebaseDB) { | |
| try { | |
| await saveUserData(userId, { | |
| created_at: new Date().toISOString(), | |
| device_fingerprint: generateUserFingerprint(), | |
| last_seen: new Date().toISOString() | |
| }); | |
| } catch (error) { | |
| console.error('Failed to save user data to Firebase:', error); | |
| } | |
| } | |
| return userId; | |
| }; | |
| const saveUserData = async (userId, userData) => { | |
| try { | |
| if (useFirebase && firebaseDB) { | |
| const { setDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| await setDoc(doc(firebaseDB, 'users', userId), { | |
| ...userData, | |
| updatedAt: new Date().toISOString() | |
| }); | |
| console.log('User data saved to Firebase:', userId); | |
| } else { | |
| // Fallback to localStorage | |
| const users = JSON.parse(localStorage.getItem('easyfarms_users') || '{}'); | |
| users[userId] = { | |
| ...userData, | |
| updatedAt: new Date().toISOString() | |
| }; | |
| localStorage.setItem('easyfarms_users', JSON.stringify(users)); | |
| console.log('User data saved to localStorage:', userId); | |
| } | |
| } catch (error) { | |
| console.error('Failed to save user data:', error); | |
| } | |
| }; | |
| // --- Firebase Configuration --- | |
| const initializeFirebase = async () => { | |
| try { | |
| if (typeof firebase !== 'undefined' && window.firebaseConfig) { | |
| const { initializeApp } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-app.js'); | |
| const { getFirestore, collection, doc, setDoc, getDoc, getDocs, deleteDoc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| const app = initializeApp(window.firebaseConfig); | |
| firebaseDB = getFirestore(app); | |
| useFirebase = true; | |
| console.log('Firebase initialized successfully'); | |
| return { setDoc, getDoc, getDocs, deleteDoc, collection, doc }; | |
| } else { | |
| console.log('Firebase not configured, using localStorage'); | |
| return null; | |
| } | |
| } catch (error) { | |
| console.error('Firebase initialization failed, falling back to localStorage:', error); | |
| return null; | |
| } | |
| }; | |
| // --- Storage Management with User Isolation --- | |
| const saveSessionData = async (chatId, data) => { | |
| try { | |
| if (useFirebase && firebaseDB) { | |
| const { setDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| await setDoc(doc(firebaseDB, 'user_sessions', `${currentUserId}_${chatId}`), { | |
| ...data, | |
| userId: currentUserId, | |
| chatId: chatId, | |
| updatedAt: new Date().toISOString() | |
| }); | |
| console.log('Session saved to Firebase:', chatId, 'for user:', currentUserId); | |
| } else { | |
| // Fallback to localStorage with user isolation | |
| const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| userSessions[chatId] = { | |
| ...data, | |
| userId: currentUserId, | |
| updatedAt: new Date().toISOString() | |
| }; | |
| localStorage.setItem(`easyfarms_sessions_${currentUserId}`, JSON.stringify(userSessions)); | |
| console.log('Session saved to localStorage:', chatId, 'for user:', currentUserId); | |
| } | |
| } catch (error) { | |
| console.error('Failed to save session data:', error); | |
| // Fallback to localStorage | |
| const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| userSessions[chatId] = data; | |
| localStorage.setItem(`easyfarms_sessions_${currentUserId}`, JSON.stringify(userSessions)); | |
| } | |
| }; | |
| const loadSessionData = async (chatId) => { | |
| try { | |
| if (useFirebase && firebaseDB) { | |
| const { getDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| const docRef = doc(firebaseDB, 'user_sessions', `${currentUserId}_${chatId}`); | |
| const docSnap = await getDoc(docRef); | |
| if (docSnap.exists()) { | |
| console.log('Session loaded from Firebase:', chatId); | |
| return docSnap.data(); | |
| } | |
| } else { | |
| // Fallback to localStorage | |
| const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| if (userSessions[chatId]) { | |
| console.log('Session loaded from localStorage:', chatId); | |
| return userSessions[chatId]; | |
| } | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error('Failed to load session data:', error); | |
| // Fallback to localStorage | |
| const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| return userSessions[chatId] || null; | |
| } | |
| }; | |
| const loadAllSessions = async () => { | |
| try { | |
| if (useFirebase && firebaseDB) { | |
| const { getDocs, collection, query, where } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| const q = query(collection(firebaseDB, 'user_sessions'), where('userId', '==', currentUserId)); | |
| const querySnapshot = await getDocs(q); | |
| const sessions = {}; | |
| querySnapshot.forEach((doc) => { | |
| const data = doc.data(); | |
| sessions[data.chatId] = data; | |
| }); | |
| console.log('All sessions loaded from Firebase for user:', currentUserId); | |
| return sessions; | |
| } else { | |
| // Fallback to localStorage | |
| const sessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| console.log('All sessions loaded from localStorage for user:', currentUserId); | |
| return sessions; | |
| } | |
| } catch (error) { | |
| console.error('Failed to load all sessions:', error); | |
| // Fallback to localStorage | |
| return JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| } | |
| }; | |
| const deleteSessionData = async (chatId) => { | |
| try { | |
| if (useFirebase && firebaseDB) { | |
| const { deleteDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| await deleteDoc(doc(firebaseDB, 'user_sessions', `${currentUserId}_${chatId}`)); | |
| console.log('Session deleted from Firebase:', chatId); | |
| } else { | |
| // Fallback to localStorage | |
| const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}'); | |
| delete userSessions[chatId]; | |
| localStorage.setItem(`easyfarms_sessions_${currentUserId}`, JSON.stringify(userSessions)); | |
| console.log('Session deleted from localStorage:', chatId); | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete session data:', error); | |
| } | |
| }; | |
| // --- Initialization --- | |
| const init = async () => { | |
| if (!messageInput || !sendBtn || !imageUploadBtn || !imagePreviewContainer) { | |
| console.error("Critical UI elements not found. Check HTML IDs."); | |
| return; | |
| } | |
| // Initialize Firebase | |
| firebase = await initializeFirebase(); | |
| // Initialize user | |
| await initializeUser(); | |
| await fetchConfig(); | |
| await loadCachedSessions(); | |
| await renderChatHistoryFromAPI(); | |
| setupSearchAndFilter(); | |
| setupUserStats(); | |
| // Event Listeners | |
| sendBtn.addEventListener('click', sendMessage); | |
| messageInput.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| imageUploadBtn.addEventListener('click', () => imageUploadInput.click()); | |
| imageUploadInput.addEventListener('change', handleImageSelect); | |
| removeImageBtn.addEventListener('click', removeSelectedImage); | |
| // Sidebar Listeners | |
| newChatBtn.addEventListener('click', () => { startNewChat(); closeSidebar(); }); | |
| menuToggle.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| appContainer.classList.toggle('sidebar-visible'); | |
| }); | |
| closeSidebarBtn.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| closeSidebar(); | |
| }); | |
| chatArea.addEventListener('click', () => { | |
| if (appContainer.classList.contains('sidebar-visible')) { | |
| closeSidebar(); | |
| } | |
| }); | |
| // Update last seen periodically | |
| setInterval(updateLastSeen, 30000); // Every 30 seconds | |
| console.log('EasyFarms Chat initialized with user authentication for user:', currentUserId); | |
| }; | |
| const fetchConfig = async () => { | |
| try { | |
| const response = await fetch('/config'); | |
| const config = await response.json(); | |
| imgbbApiKey = config.imgbb_api_key; | |
| if (!imgbbApiKey) { | |
| console.error("ImgBB API Key is missing."); | |
| } | |
| } catch (error) { | |
| console.error('Failed to fetch config:', error); | |
| } | |
| }; | |
| const setupUserStats = () => { | |
| // Create user stats div | |
| if (!userStatsDiv) { | |
| userStatsDiv = document.createElement('div'); | |
| userStatsDiv.className = 'user-stats'; | |
| userStatsDiv.style.cssText = ` | |
| padding: 10px; | |
| margin: 10px 0; | |
| background: #f8f9fa; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| color: #666; | |
| border-top: 1px solid #eee; | |
| `; | |
| // Insert at the bottom of sidebar | |
| chatHistoryList.parentNode.appendChild(userStatsDiv); | |
| } | |
| updateUserStats(); | |
| }; | |
| const updateUserStats = async () => { | |
| try { | |
| const response = await fetch(`/users/${currentUserId}/stats`); | |
| if (response.ok) { | |
| const stats = await response.json(); | |
| userStatsDiv.innerHTML = ` | |
| <div><strong>Your Stats</strong></div> | |
| <div>Chats: ${stats.total_sessions || 0}</div> | |
| <div>Messages: ${stats.total_messages || 0}</div> | |
| <div>Recent: ${stats.recent_sessions_24h || 0}</div> | |
| `; | |
| } | |
| } catch (error) { | |
| console.error('Failed to update user stats:', error); | |
| // Show basic local stats | |
| const sessions = await loadAllSessions(); | |
| const sessionCount = Object.keys(sessions).length; | |
| userStatsDiv.innerHTML = ` | |
| <div><strong>Local Stats</strong></div> | |
| <div>Chats: ${sessionCount}</div> | |
| <div>User: ${currentUserId.substr(0, 12)}...</div> | |
| `; | |
| } | |
| }; | |
| const updateLastSeen = async () => { | |
| try { | |
| await saveUserData(currentUserId, { | |
| last_seen: new Date().toISOString() | |
| }); | |
| } catch (error) { | |
| console.error('Failed to update last seen:', error); | |
| } | |
| }; | |
| const setupSearchAndFilter = () => { | |
| // Create search input if it doesn't exist | |
| if (!searchInput) { | |
| searchInput = document.createElement('input'); | |
| searchInput.type = 'text'; | |
| searchInput.placeholder = 'Search chats...'; | |
| searchInput.className = 'search-input'; | |
| searchInput.style.cssText = ` | |
| width: 100%; | |
| padding: 8px; | |
| margin: 10px 0; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| `; | |
| chatHistoryList.parentNode.insertBefore(searchInput, chatHistoryList); | |
| } | |
| // Create clear all button if it doesn't exist | |
| if (!clearAllBtn) { | |
| clearAllBtn = document.createElement('button'); | |
| clearAllBtn.textContent = 'Clear All My Chats'; | |
| clearAllBtn.className = 'clear-all-btn'; | |
| clearAllBtn.style.cssText = ` | |
| width: 100%; | |
| padding: 8px; | |
| margin: 5px 0; | |
| background: #ff4444; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| `; | |
| searchInput.parentNode.insertBefore(clearAllBtn, chatHistoryList); | |
| } | |
| // Add event listeners | |
| searchInput.addEventListener('input', filterChats); | |
| clearAllBtn.addEventListener('click', confirmClearAllChats); | |
| }; | |
| const filterChats = () => { | |
| const searchTerm = searchInput.value.toLowerCase(); | |
| const chatItems = chatHistoryList.querySelectorAll('.chat-history-item'); | |
| chatItems.forEach(item => { | |
| const title = item.textContent.toLowerCase(); | |
| const shouldShow = title.includes(searchTerm); | |
| item.style.display = shouldShow ? 'block' : 'none'; | |
| }); | |
| console.log('Filtered chats with term:', searchTerm); | |
| }; | |
| const confirmClearAllChats = () => { | |
| if (confirm('Are you sure you want to delete all your chats? This action cannot be undone.')) { | |
| clearAllChats(); | |
| } | |
| }; | |
| const clearAllChats = async () => { | |
| try { | |
| // Delete all user chats from backend | |
| const response = await fetch(`/users/${currentUserId}/chats`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| console.log('All chats deleted from backend for user:', currentUserId); | |
| } else { | |
| console.error('Failed to delete all chats from backend'); | |
| } | |
| // Clear local storage | |
| const allSessions = await loadAllSessions(); | |
| const chatIds = Object.keys(allSessions); | |
| for (const chatId of chatIds) { | |
| await deleteSessionData(chatId); | |
| } | |
| // Clear UI and cache | |
| conversationsCache = {}; | |
| chatHistoryList.innerHTML = ''; | |
| startNewChat(); | |
| updateUserStats(); | |
| console.log('All chats cleared successfully for user:', currentUserId); | |
| } catch (error) { | |
| console.error('Failed to clear all chats:', error); | |
| } | |
| }; | |
| // --- Core Chat Functions --- | |
| const sendMessage = async () => { | |
| const messageText = messageInput.value.trim(); | |
| if (!messageText && !selectedImageFile) return; | |
| // Display user message immediately | |
| displayMessage({ | |
| role: 'user', | |
| content: messageText, | |
| imageUrl: selectedImageFile | |
| }); | |
| const loadingIndicator = displayMessage({ | |
| role: 'assistant', | |
| content: 'Thinking...', | |
| isLoading: true | |
| }); | |
| try { | |
| let permanentImageUrl = null; | |
| if (selectedImageFile) { | |
| permanentImageUrl = await uploadImageToImgBB(selectedImageFile); | |
| } | |
| // Prepare request data with user ID | |
| const requestData = { | |
| query: messageText, | |
| session_id: currentChatId, | |
| user_id: currentUserId | |
| }; | |
| if (permanentImageUrl) { | |
| requestData.image_url = permanentImageUrl; | |
| } | |
| // Send to API endpoint | |
| const response = await fetch('/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(requestData) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| console.log('Chat response:', data); | |
| // Remove loading indicator | |
| chatMessages.removeChild(loadingIndicator); | |
| // Display assistant response | |
| displayMessage({ | |
| role: 'assistant', | |
| content: data.response, | |
| message_id: data.assistant_message_id | |
| }); | |
| // Update cache and UI | |
| await updateCacheWithNewSystem( | |
| data.chat_id, | |
| { | |
| content: messageText, | |
| imageUrl: permanentImageUrl, | |
| message_id: data.user_message_id | |
| }, | |
| { | |
| content: data.response, | |
| message_id: data.assistant_message_id | |
| }, | |
| data.is_new_chat | |
| ); | |
| // Update stats | |
| updateUserStats(); | |
| } catch (error) { | |
| console.error('Error sending message:', error); | |
| // Remove loading indicator and show error | |
| chatMessages.removeChild(loadingIndicator); | |
| // Display mock error response (don't save to database) | |
| displayMessage({ | |
| role: 'assistant', | |
| content: "I apologize, but I'm having trouble connecting right now. Please check your internet connection and try again. Your chat session is still active.", | |
| isError: true | |
| }); | |
| } finally { | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| removeSelectedImage(); | |
| } | |
| }; | |
| const uploadImageToImgBB = async (imageFile) => { | |
| if (!imgbbApiKey) throw new Error("ImgBB API Key not configured."); | |
| const formData = new FormData(); | |
| formData.append('image', imageFile); | |
| formData.append('key', imgbbApiKey); | |
| const response = await fetch('https://api.imgbb.com/1/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| return result.data.url; | |
| } else { | |
| throw new Error(result.error.message || 'Image upload failed.'); | |
| } | |
| }; | |
| // --- Chat Management --- | |
| const startNewChat = () => { | |
| currentChatId = null; | |
| chatMessages.innerHTML = `<div class="welcome-message"><h1>EasyFarms Assistant</h1><p>User: ${currentUserId.substr(0, 16)}...</p></div>`; | |
| chatTitle.textContent = "New Chat"; | |
| updateActiveChatItem(); | |
| console.log('Started new chat for user:', currentUserId); | |
| }; | |
| const switchChat = async (chatId) => { | |
| console.log('Switching to chat:', chatId, 'for user:', currentUserId); | |
| currentChatId = chatId; | |
| chatMessages.innerHTML = ''; | |
| // Check cache first | |
| if (conversationsCache[chatId] && conversationsCache[chatId].messages) { | |
| conversationsCache[chatId].messages.forEach(displayMessage); | |
| chatTitle.textContent = conversationsCache[chatId].title || "Chat"; | |
| } else { | |
| // Load from API | |
| const loading = displayMessage({ | |
| role: 'assistant', | |
| content: 'Loading chat history...', | |
| isLoading: true | |
| }); | |
| try { | |
| const response = await fetch(`/chat/${chatId}/messages?user_id=${currentUserId}`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const messages = await response.json(); | |
| console.log('Loaded messages for chat:', chatId, messages); | |
| // Update cache | |
| if (!conversationsCache[chatId]) conversationsCache[chatId] = {}; | |
| conversationsCache[chatId].messages = messages; | |
| // Save to storage | |
| await saveSessionData(chatId, conversationsCache[chatId]); | |
| // Remove loading and display messages | |
| chatMessages.removeChild(loading); | |
| messages.forEach(displayMessage); | |
| // Set title from first user message or use cached title | |
| const sessionData = await loadSessionData(chatId); | |
| chatTitle.textContent = sessionData?.title || conversationsCache[chatId]?.title || "Chat"; | |
| } catch (error) { | |
| console.error('Failed to load chat history:', error); | |
| chatMessages.removeChild(loading); | |
| displayMessage({ | |
| role: 'assistant', | |
| content: "Failed to load chat history. Please try again.", | |
| isError: true | |
| }); | |
| } | |
| } | |
| updateActiveChatItem(); | |
| closeSidebar(); | |
| }; | |
| const deleteChat = async (chatId, chatItem) => { | |
| if (!confirm('Are you sure you want to delete this chat?')) return; | |
| try { | |
| // Delete from backend | |
| const response = await fetch(`/chat/${chatId}?user_id=${currentUserId}`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| console.log('Chat deleted from backend:', chatId); | |
| } else { | |
| console.error('Failed to delete chat from backend:', chatId); | |
| } | |
| // Delete from storage | |
| await deleteSessionData(chatId); | |
| // Remove from cache | |
| delete conversationsCache[chatId]; | |
| // Remove from UI | |
| chatItem.remove(); | |
| // If this was the current chat, start a new one | |
| if (currentChatId === chatId) { | |
| startNewChat(); | |
| } | |
| console.log('Chat deleted successfully:', chatId); | |
| updateUserStats(); | |
| } catch (error) { | |
| console.error('Error deleting chat:', error); | |
| alert('Failed to delete chat. Please try again.'); | |
| } | |
| }; | |
| const updateCacheWithNewSystem = async (chatId, userTurn, assistantTurn, isNewChat) => { | |
| const previousChatId = currentChatId; | |
| currentChatId = chatId; | |
| if (isNewChat || !conversationsCache[chatId]) { | |
| const title = (userTurn.content || "Image Query").substring(0, 30) + '...'; | |
| conversationsCache[chatId] = { | |
| title, | |
| messages: [], | |
| created_at: new Date().toISOString(), | |
| user_id: currentUserId | |
| }; | |
| // Add to UI | |
| const item = createChatHistoryItem(chatId, title); | |
| chatHistoryList.prepend(item); | |
| console.log('Created new chat:', chatId, 'for user:', currentUserId); | |
| } | |
| // Create message objects with IDs | |
| const userMessage = { | |
| role: 'user', | |
| content: userTurn.content, | |
| message_id: userTurn.message_id, | |
| timestamp: new Date().toISOString() | |
| }; | |
| if (userTurn.imageUrl) userMessage.imageUrl = userTurn.imageUrl; | |
| const assistantMessage = { | |
| role: 'assistant', | |
| content: assistantTurn.content, | |
| message_id: assistantTurn.message_id, | |
| timestamp: new Date().toISOString() | |
| }; | |
| // Add to cache | |
| conversationsCache[chatId].messages.push(userMessage, assistantMessage); | |
| conversationsCache[chatId].updated_at = new Date().toISOString(); | |
| conversationsCache[chatId].user_id = currentUserId; | |
| // Save to storage | |
| await saveSessionData(chatId, conversationsCache[chatId]); | |
| updateActiveChatItem(); | |
| chatTitle.textContent = conversationsCache[chatId].title; | |
| console.log('Updated cache for chat:', chatId, 'user:', currentUserId); | |
| }; | |
| const createChatHistoryItem = (chatId, title) => { | |
| const item = document.createElement('div'); | |
| item.className = 'chat-history-item'; | |
| item.dataset.sessionId = chatId; | |
| // Create title element | |
| const titleElement = document.createElement('span'); | |
| titleElement.textContent = title; | |
| titleElement.className = 'chat-title'; | |
| // Create delete button | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.innerHTML = '×'; | |
| deleteBtn.className = 'delete-chat-btn'; | |
| deleteBtn.style.cssText = ` | |
| float: right; | |
| background: none; | |
| border: none; | |
| color: #999; | |
| cursor: pointer; | |
| font-size: 18px; | |
| padding: 0; | |
| width: 20px; | |
| height: 20px; | |
| line-height: 18px; | |
| `; | |
| deleteBtn.title = 'Delete chat'; | |
| // Add event listeners | |
| titleElement.addEventListener('click', () => switchChat(chatId)); | |
| deleteBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| deleteChat(chatId, item); | |
| }); | |
| item.appendChild(titleElement); | |
| item.appendChild(deleteBtn); | |
| return item; | |
| }; | |
| const loadCachedSessions = async () => { | |
| try { | |
| conversationsCache = await loadAllSessions(); | |
| console.log('Loaded cached sessions for user:', currentUserId, 'Count:', Object.keys(conversationsCache).length); | |
| } catch (error) { | |
| console.error('Failed to load cached sessions:', error); | |
| conversationsCache = {}; | |
| } | |
| }; | |
| const displayMessage = (message) => { | |
| const { role, content, imageUrl, isLoading, isError, message_id } = message; | |
| const sender = role || message.sender; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.classList.add('message', `${sender}-message`); | |
| if (message_id) { | |
| messageDiv.dataset.messageId = message_id; | |
| console.log('Displayed message with ID:', message_id); | |
| } | |
| let htmlContent = ''; | |
| const imageSrc = (typeof imageUrl === 'object' && imageUrl instanceof File) | |
| ? URL.createObjectURL(imageUrl) | |
| : imageUrl; | |
| if (imageSrc) { | |
| htmlContent += `<img src="${imageSrc}" alt="User upload" class="user-upload">`; | |
| } | |
| if (content) { | |
| htmlContent += marked.parse(content); | |
| } | |
| messageDiv.innerHTML = htmlContent || (isLoading ? '...' : ''); | |
| if (isLoading) messageDiv.classList.add('loading'); | |
| if (isError) messageDiv.classList.add('error'); | |
| chatMessages.appendChild(messageDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| return messageDiv; | |
| }; | |
| // --- Image Preview Handling --- | |
| const handleImageSelect = (event) => { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| selectedImageFile = file; | |
| imagePreview.src = URL.createObjectURL(file); | |
| imagePreviewContainer.style.display = 'block'; | |
| console.log('Image selected:', file.name); | |
| } | |
| }; | |
| const removeSelectedImage = () => { | |
| selectedImageFile = null; | |
| imageUploadInput.value = ''; | |
| imagePreviewContainer.style.display = 'none'; | |
| imagePreview.src = '#'; | |
| console.log('Image removed'); | |
| }; | |
| // --- API Integration --- | |
| const renderChatHistoryFromAPI = async () => { | |
| try { | |
| const response = await fetch(`/chats?user_id=${currentUserId}`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const sessions = await response.json(); | |
| console.log('Loaded chat sessions from API for user:', currentUserId, sessions.length); | |
| chatHistoryList.innerHTML = ''; | |
| // Sort by updated_at (most recent first) | |
| sessions.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); | |
| sessions.forEach(session => { | |
| // Update cache | |
| if (!conversationsCache[session.session_id]) { | |
| conversationsCache[session.session_id] = {}; | |
| } | |
| conversationsCache[session.session_id].title = session.title; | |
| conversationsCache[session.session_id].message_count = session.message_count; | |
| conversationsCache[session.session_id].created_at = session.created_at; | |
| conversationsCache[session.session_id].updated_at = session.updated_at; | |
| conversationsCache[session.session_id].user_id = currentUserId; | |
| // Create UI item | |
| const item = createChatHistoryItem(session.session_id, session.title); | |
| chatHistoryList.appendChild(item); | |
| }); | |
| // Save updated cache | |
| for (const [chatId, data] of Object.entries(conversationsCache)) { | |
| if (data.user_id === currentUserId) { | |
| await saveSessionData(chatId, data); | |
| } | |
| } | |
| updateActiveChatItem(); | |
| updateUserStats(); | |
| } catch (error) { | |
| console.error("Failed to render chat history from API:", error); | |
| // Fallback to local cache | |
| console.log('Falling back to local cache for user:', currentUserId); | |
| for (const [chatId, data] of Object.entries(conversationsCache)) { | |
| if (data.title && data.user_id === currentUserId) { | |
| const item = createChatHistoryItem(chatId, data.title); | |
| chatHistoryList.appendChild(item); | |
| } | |
| } | |
| } | |
| }; | |
| const updateActiveChatItem = () => { | |
| document.querySelectorAll('.chat-history-item').forEach(item => { | |
| item.classList.toggle('active', item.dataset.sessionId === currentChatId); | |
| }); | |
| }; | |
| const closeSidebar = () => appContainer.classList.remove('sidebar-visible'); | |
| // --- User Account Management --- | |
| const exportUserData = async () => { | |
| try { | |
| const response = await fetch(`/users/${currentUserId}/export`); | |
| if (response.ok) { | |
| const userData = await response.json(); | |
| const dataStr = JSON.stringify(userData, null, 2); | |
| const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
| const exportFileDefaultName = `easyfarms_data_${currentUserId.substr(0, 8)}_${new Date().toISOString().split('T')[0]}.json`; | |
| const linkElement = document.createElement('a'); | |
| linkElement.setAttribute('href', dataUri); | |
| linkElement.setAttribute('download', exportFileDefaultName); | |
| linkElement.click(); | |
| console.log('User data exported successfully'); | |
| } else { | |
| throw new Error('Export failed'); | |
| } | |
| } catch (error) { | |
| console.error('Failed to export user data:', error); | |
| alert('Failed to export data. Please try again.'); | |
| } | |
| }; | |
| const deleteUserAccount = async () => { | |
| if (!confirm('Are you sure you want to delete your account? This will permanently delete all your chats and data.')) { | |
| return; | |
| } | |
| if (!confirm('This action cannot be undone. Type "DELETE" to confirm:') || | |
| prompt('Type "DELETE" to confirm account deletion:') !== 'DELETE') { | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`/users/${currentUserId}`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.ok) { | |
| // Clear all local data | |
| localStorage.removeItem('easyfarms_user_id'); | |
| localStorage.removeItem(`easyfarms_sessions_${currentUserId}`); | |
| localStorage.removeItem(`easyfarms_users`); | |
| // Clear Firebase data if applicable | |
| if (useFirebase && firebaseDB) { | |
| try { | |
| const { deleteDoc, doc, collection, getDocs, query, where } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js'); | |
| // Delete user document | |
| await deleteDoc(doc(firebaseDB, 'users', currentUserId)); | |
| // Delete user sessions | |
| const q = query(collection(firebaseDB, 'user_sessions'), where('userId', '==', currentUserId)); | |
| const querySnapshot = await getDocs(q); | |
| querySnapshot.forEach(async (doc) => { | |
| await deleteDoc(doc.ref); | |
| }); | |
| } catch (firebaseError) { | |
| console.error('Failed to clean Firebase data:', firebaseError); | |
| } | |
| } | |
| alert('Account deleted successfully. Refreshing page...'); | |
| location.reload(); | |
| } else { | |
| throw new Error('Account deletion failed'); | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete account:', error); | |
| alert('Failed to delete account. Please try again.'); | |
| } | |
| }; | |
| const createUserMenu = () => { | |
| // Create user menu button | |
| const userMenuBtn = document.createElement('button'); | |
| userMenuBtn.innerHTML = '⚙️'; | |
| userMenuBtn.title = 'User Settings'; | |
| userMenuBtn.style.cssText = ` | |
| position: fixed; | |
| top: 10px; | |
| right: 10px; | |
| background: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| z-index: 1000; | |
| `; | |
| // Create user menu dropdown | |
| const userMenu = document.createElement('div'); | |
| userMenu.style.cssText = ` | |
| position: fixed; | |
| top: 55px; | |
| right: 10px; | |
| background: white; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| display: none; | |
| z-index: 1001; | |
| min-width: 200px; | |
| `; | |
| userMenu.innerHTML = ` | |
| <div style="margin-bottom: 10px; font-weight: bold;">User Settings</div> | |
| <div style="margin-bottom: 5px; font-size: 12px; color: #666;">ID: ${currentUserId.substr(0, 16)}...</div> | |
| <button id="export-data-btn" style="width: 100%; margin: 5px 0; padding: 5px; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">Export My Data</button> | |
| <button id="delete-account-btn" style="width: 100%; margin: 5px 0; padding: 5px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">Delete Account</button> | |
| `; | |
| document.body.appendChild(userMenuBtn); | |
| document.body.appendChild(userMenu); | |
| // Toggle menu | |
| userMenuBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| userMenu.style.display = userMenu.style.display === 'none' ? 'block' : 'none'; | |
| }); | |
| // Close menu when clicking outside | |
| document.addEventListener('click', () => { | |
| userMenu.style.display = 'none'; | |
| }); | |
| // Menu actions | |
| document.getElementById('export-data-btn').addEventListener('click', exportUserData); | |
| document.getElementById('delete-account-btn').addEventListener('click', deleteUserAccount); | |
| }; | |
| // --- Enhanced Error Handling --- | |
| window.addEventListener('unhandledrejection', (event) => { | |
| console.error('Unhandled promise rejection:', event.reason); | |
| // Don't show error to user for now, just log it | |
| }); | |
| window.addEventListener('error', (event) => { | |
| console.error('Global error:', event.error); | |
| // Don't show error to user for now, just log it | |
| }); | |
| // --- Session Recovery --- | |
| const attemptSessionRecovery = async () => { | |
| try { | |
| // Try to recover any unsaved messages | |
| const unsavedMessages = localStorage.getItem(`unsaved_messages_${currentUserId}`); | |
| if (unsavedMessages) { | |
| const messages = JSON.parse(unsavedMessages); | |
| console.log('Found unsaved messages, attempting recovery:', messages); | |
| // Display recovered messages | |
| messages.forEach(msg => { | |
| displayMessage({ | |
| role: msg.role, | |
| content: `[RECOVERED] ${msg.content}`, | |
| isError: true | |
| }); | |
| }); | |
| // Clear unsaved messages | |
| localStorage.removeItem(`unsaved_messages_${currentUserId}`); | |
| } | |
| } catch (error) { | |
| console.error('Session recovery failed:', error); | |
| } | |
| }; | |
| const saveUnsavedMessage = (role, content) => { | |
| try { | |
| const unsavedMessages = JSON.parse(localStorage.getItem(`unsaved_messages_${currentUserId}`) || '[]'); | |
| unsavedMessages.push({ | |
| role, | |
| content, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // Keep only last 5 unsaved messages | |
| if (unsavedMessages.length > 5) { | |
| unsavedMessages.splice(0, unsavedMessages.length - 5); | |
| } | |
| localStorage.setItem(`unsaved_messages_${currentUserId}`, JSON.stringify(unsavedMessages)); | |
| } catch (error) { | |
| console.error('Failed to save unsaved message:', error); | |
| } | |
| }; | |
| // --- Performance Monitoring --- | |
| const logPerformance = () => { | |
| if (performance && performance.timing) { | |
| const perfData = performance.timing; | |
| const loadTime = perfData.loadEventEnd - perfData.navigationStart; | |
| console.log(`Page load time: ${loadTime}ms`); | |
| // Log to backend if needed | |
| // fetch('/analytics/performance', { | |
| // method: 'POST', | |
| // body: JSON.stringify({ user_id: currentUserId, load_time: loadTime }) | |
| // }); | |
| } | |
| }; | |
| // --- Initialize Everything --- | |
| // Add user menu after initialization | |
| const setupUserInterface = () => { | |
| createUserMenu(); | |
| attemptSessionRecovery(); | |
| logPerformance(); | |
| }; | |
| // Start the Application | |
| init().then(() => { | |
| setupUserInterface(); | |
| console.log('EasyFarms Chat fully initialized for user:', currentUserId); | |
| }).catch((error) => { | |
| console.error('Failed to initialize application:', error); | |
| document.body.innerHTML = ` | |
| <div style="padding: 20px; text-align: center;"> | |
| <h2>Failed to Load EasyFarms Chat</h2> | |
| <p>Please refresh the page and try again.</p> | |
| <p style="color: #666; font-size: 12px;">Error: ${error.message}</p> | |
| </div> | |
| `; | |
| }); | |
| }); |