EasyBot / static /script.js
NitinBot001's picture
Update static/script.js
7ba9095 verified
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>
`;
});
});