const GPTResearcher = (() => { let isResearchActive = false; let connectionTimeout = null; let conversationHistory = []; let isInitialLoad = true; // Flag to track initial page load let cookiesEnabled = true; // Flag to track if cookies are enabled let allReports = ''; // Store all reports cumulatively let currentReport = ''; // Store the current report (will be overwritten) let isFirstReport = true; // Flag to track if this is the first report let chatContainer = null; // Global reference to chat container let lastRequestData = null; // Store the last request data for reconnection // Add WebSocket monitoring variables let socket = null; let connectionStartTime = null; let lastActivityTime = null; let connectionAttempts = 0; let messagesReceived = 0; let websocketMonitorInterval = null; let dispose_socket = null; // Re-add dispose_socket let reconnectAttempts = 0; let maxReconnectAttempts = 5; let reconnectInterval = 2000; // Start with 2 seconds const init = () => { // Check if cookies are enabled checkCookiesEnabled(); // Load history immediately on page load loadConversationHistory(); // After a short delay, mark initial load as complete setTimeout(() => { isInitialLoad = false; }, 1000); // Setup form submission document.getElementById('researchForm').addEventListener('submit', (e) => { e.preventDefault(); startResearch(); return false; }); document .getElementById('copyToClipboard') .addEventListener('click', copyToClipboard) // Add event listener for the top copy button const topCopyButton = document.getElementById('copyToClipboardTop'); if (topCopyButton) { topCopyButton.addEventListener('click', copyToClipboard); } // Initialize expand buttons initExpandButtons(); // Initialize history panel functionality initHistoryPanel(); // Initialize WebSocket monitoring panel initWebSocketPanel(); // Initialize MCP functionality initMCPSection(); // The download bar is now fixed in place with CSS // No need to set display property here updateState('initial'); // Initialize research icon to not spinning updateResearchIcon(false); // Hide loading overlay if it exists const loadingOverlay = document.getElementById('loadingOverlay'); if (loadingOverlay) { loadingOverlay.classList.add('loading-hidden'); } } // Check if cookies are enabled const checkCookiesEnabled = () => { try { // Try to set a test cookie document.cookie = "testcookie=1; path=/"; const cookieEnabled = document.cookie.indexOf("testcookie") !== -1; if (!cookieEnabled) { console.warn("Cookies are disabled in this browser"); cookiesEnabled = false; showToast("Cookies are disabled. History will use localStorage instead.", 5000); } else { // Clean up test cookie document.cookie = "testcookie=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; cookiesEnabled = true; } return cookieEnabled; } catch (e) { console.error("Error checking cookies:", e); cookiesEnabled = false; return false; } } // Initialize conversation history panel functionality const initHistoryPanel = () => { // Load history from cookie loadConversationHistory(); // Setup history panel toggle button const historyPanelOpenBtn = document.getElementById('historyPanelOpenBtn'); const historyPanel = document.getElementById('historyPanel'); const historyPanelToggle = document.getElementById('historyPanelToggle'); if (historyPanelOpenBtn) { historyPanelOpenBtn.addEventListener('click', () => { loadConversationHistory(); // Reload history when opening panel historyPanel.classList.add('open'); }); } if (historyPanelToggle) { historyPanelToggle.addEventListener('click', () => { historyPanel.classList.remove('open'); }); } // Close panel when clicking outside document.addEventListener('click', (e) => { // If the panel is open and the click is outside the panel and not on the toggle button if (historyPanel.classList.contains('open') && !historyPanel.contains(e.target) && e.target !== historyPanelOpenBtn && !historyPanelOpenBtn.contains(e.target)) { historyPanel.classList.remove('open'); } }); // Setup search functionality const historySearch = document.getElementById('historySearch'); const historySearchBtn = document.getElementById('historySearchBtn'); if (historySearch && historySearchBtn) { historySearch.addEventListener('input', filterHistoryEntries); historySearchBtn.addEventListener('click', () => filterHistoryEntries()); } // Setup sort functionality const historySortOrder = document.getElementById('historySortOrder'); if (historySortOrder) { historySortOrder.addEventListener('change', () => { sortHistoryEntries(historySortOrder.value); renderHistoryEntries(); }); } // Setup clear history button const historyClearBtn = document.getElementById('historyClearBtn'); if (historyClearBtn) { historyClearBtn.addEventListener('click', clearConversationHistory); } // Add action buttons to history panel const historyFilters = document.querySelector('.history-panel-filters'); if (historyFilters) { // Create a container for the buttons const actionsContainer = document.createElement('div'); actionsContainer.className = 'history-actions-container'; // Add export history button with enhanced styling and tooltip const exportBtn = document.createElement('button'); exportBtn.className = 'history-action-btn'; exportBtn.title = 'Export research history to file'; exportBtn.innerHTML = ''; exportBtn.addEventListener('click', exportHistory); // Add import history button with enhanced styling and tooltip const importBtn = document.createElement('button'); importBtn.className = 'history-action-btn'; importBtn.title = 'Import research history from file'; importBtn.innerHTML = ''; importBtn.addEventListener('click', triggerImportHistory); // Add cookie debug button with enhanced styling and tooltip const debugBtn = document.createElement('button'); debugBtn.className = 'history-action-btn'; debugBtn.title = 'Check storage status'; debugBtn.innerHTML = ''; debugBtn.addEventListener('click', checkCookieStatus); // Add buttons to container in a logical order actionsContainer.appendChild(importBtn); actionsContainer.appendChild(exportBtn); actionsContainer.appendChild(debugBtn); // Create a hidden file input for importing const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.id = 'historyFileInput'; fileInput.accept = '.json'; fileInput.style.display = 'none'; fileInput.addEventListener('change', handleFileImport); // Add container and file input to filters historyFilters.appendChild(actionsContainer); historyFilters.appendChild(fileInput); } // Initial render of history entries renderHistoryEntries(); } // Initialize WebSocket monitoring panel const initWebSocketPanel = () => { const websocketPanel = document.getElementById('websocketPanel'); const websocketPanelOpenBtn = document.getElementById('websocketPanelOpenBtn'); const websocketPanelToggle = document.getElementById('websocketPanelToggle'); if (!websocketPanel || !websocketPanelOpenBtn || !websocketPanelToggle) { console.error("WebSocket panel elements not found"); return; } console.log("Initializing WebSocket panel"); // Ensure it starts hidden websocketPanel.classList.remove('open'); websocketPanelOpenBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log("Opening WebSocket panel"); websocketPanel.classList.add('open'); }); websocketPanelToggle.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log("Closing WebSocket panel"); websocketPanel.classList.remove('open'); }); // Close panel when clicking outside document.addEventListener('click', (e) => { // If the panel is open and the click is outside the panel and not on the toggle button if (websocketPanel.classList.contains('open') && !websocketPanel.contains(e.target) && e.target !== websocketPanelOpenBtn && !websocketPanelOpenBtn.contains(e.target)) { websocketPanel.classList.remove('open'); } }); // Start periodic WebSocket status updates startWebSocketMonitoring(); } // Start WebSocket monitoring const startWebSocketMonitoring = () => { console.log("Starting WebSocket monitoring"); // Update status immediately updateWebSocketStatus(); // Clear any existing interval if (websocketMonitorInterval) { clearInterval(websocketMonitorInterval); } // Update status every 2 seconds websocketMonitorInterval = setInterval(updateWebSocketStatus, 2000); } // Update WebSocket status in the panel const updateWebSocketStatus = () => { // Only proceed if the necessary elements exist const connectionStatusEl = document.getElementById('connectionStatus'); const connectionIndicatorEl = document.getElementById('connectionIndicator'); const researchStatusEl = document.getElementById('researchStatus'); const connectionDurationEl = document.getElementById('connectionDuration'); const lastActivityEl = document.getElementById('lastActivity'); const readyStateEl = document.getElementById('readyState'); const connectionAttemptsEl = document.getElementById('connectionAttempts'); const messagesReceivedEl = document.getElementById('messagesReceived'); const currentTaskEl = document.getElementById('currentTask'); if (!connectionStatusEl || !connectionIndicatorEl) return; // Update connection status const socketStatus = getSocketStatus(); connectionStatusEl.textContent = socketStatus.statusText; // Update indicator class connectionIndicatorEl.className = 'status-indicator'; connectionIndicatorEl.classList.add(socketStatus.indicatorClass); // Update research status if (researchStatusEl) { researchStatusEl.textContent = isResearchActive ? 'Active' : 'Inactive'; } // Update connection duration if (connectionDurationEl && connectionStartTime) { const duration = Math.floor((Date.now() - connectionStartTime) / 1000); connectionDurationEl.textContent = formatDuration(duration); } else if (connectionDurationEl) { connectionDurationEl.textContent = '-'; } // Update last activity if (lastActivityEl && lastActivityTime) { const elapsed = Math.floor((Date.now() - lastActivityTime) / 1000); lastActivityEl.textContent = elapsed < 60 ? `${elapsed} sec ago` : formatDuration(elapsed) + ' ago'; } else if (lastActivityEl) { lastActivityEl.textContent = '-'; } // Update ReadyState if (readyStateEl && socket) { readyStateEl.textContent = getReadyStateText(socket.readyState); } else if (readyStateEl) { readyStateEl.textContent = '-'; } // Update connection attempts if (connectionAttemptsEl) { connectionAttemptsEl.textContent = connectionAttempts.toString(); } // Update messages received if (messagesReceivedEl) { messagesReceivedEl.textContent = messagesReceived.toString(); } // Update current task if (currentTaskEl) { const taskInput = document.getElementById('task'); currentTaskEl.textContent = isResearchActive && taskInput && taskInput.value ? (taskInput.value.length > 30 ? taskInput.value.substring(0, 27) + '...' : taskInput.value) : '-'; } } // Get socket status object const getSocketStatus = () => { if (!socket) { return { statusText: 'Disconnected', indicatorClass: 'disconnected' }; } switch (socket.readyState) { case WebSocket.CONNECTING: return { statusText: 'Connecting', indicatorClass: 'connecting' }; case WebSocket.OPEN: return { statusText: 'Connected', indicatorClass: 'connected' }; case WebSocket.CLOSING: return { statusText: 'Closing', indicatorClass: 'connecting' }; case WebSocket.CLOSED: default: return { statusText: 'Disconnected', indicatorClass: 'disconnected' }; } } // Get readable text for WebSocket readyState const getReadyStateText = (readyState) => { switch (readyState) { case WebSocket.CONNECTING: return '0 (Connecting)'; case WebSocket.OPEN: return '1 (Open)'; case WebSocket.CLOSING: return '2 (Closing)'; case WebSocket.CLOSED: return '3 (Closed)'; default: return `${readyState} (Unknown)`; } } // Format duration in seconds to human-readable string const formatDuration = (seconds) => { if (seconds < 60) { return `${seconds} sec`; } else if (seconds < 3600) { return `${Math.floor(seconds / 60)} min ${seconds % 60} sec`; } else { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours} hr ${minutes} min`; } } // Load conversation history from cookie const loadConversationHistory = () => { try { const storedHistory = getCookie('conversationHistory'); if (storedHistory && storedHistory.trim() !== '') { try { const parsedHistory = JSON.parse(storedHistory); if (Array.isArray(parsedHistory)) { conversationHistory = parsedHistory; console.debug('Loaded research history from storage:', conversationHistory); console.log('Loaded research history:', conversationHistory.length, 'items'); } else { console.warn('History storage does not contain an array'); conversationHistory = []; deleteCookie('conversationHistory'); } } catch (jsonError) { console.error('Invalid JSON in history storage:', jsonError); conversationHistory = []; deleteCookie('conversationHistory'); } } else { console.log('No research history found in storage'); conversationHistory = []; } } catch (error) { console.error('Error loading research history from storage:', error); conversationHistory = []; // If JSON parsing fails, delete the corrupt cookie deleteCookie('conversationHistory'); } // Force render after loading renderHistoryEntries(); } // Save conversation history to cookie const saveConversationHistory = () => { try { if (conversationHistory.length === 0) { deleteCookie('conversationHistory'); console.debug('No history to save, deleted storage'); return; } // Only keep the last 20 entries let storageHistory = [...conversationHistory]; if (storageHistory.length > 20) { storageHistory = storageHistory.slice(0, 20); console.debug('Trimmed history to last 20 entries'); } // Only keep minimal fields: prompt, links and timestamp storageHistory = storageHistory.map(entry => ({ prompt: entry.prompt || '', links: entry.links || {}, timestamp: entry.timestamp || new Date().toISOString() })); const jsonString = JSON.stringify(storageHistory); console.debug('History JSON size:', jsonString.length, 'characters'); setCookie('conversationHistory', jsonString, 30); if (storageHistory.length > 0 && !isInitialLoad) { showToast('Research history saved!'); } } catch (error) { console.error('Error saving research history:', error); showToast('Error saving history. Some entries may not be saved.'); } } // Delete a history entry const deleteHistoryEntry = (index) => { if (confirm('Are you sure you want to delete this research entry?')) { conversationHistory.splice(index, 1); saveConversationHistory(); renderHistoryEntries(); showToast('Entry deleted successfully'); } } // Clear all conversation history const clearConversationHistory = () => { if (confirm('Are you sure you want to clear all research history? This cannot be undone.')) { conversationHistory = []; saveConversationHistory(); renderHistoryEntries(); showToast('Research history cleared successfully'); } } // Filter history entries based on search term const filterHistoryEntries = () => { const searchTerm = document.getElementById('historySearch').value.toLowerCase(); const historyEntries = document.getElementById('historyEntries'); if (!historyEntries) return; const entries = historyEntries.querySelectorAll('.history-entry'); entries.forEach(entry => { const title = entry.querySelector('.history-entry-title').textContent.toLowerCase(); // Search only in the title since we no longer have preview text if (title.includes(searchTerm)) { entry.style.display = 'block'; } else { entry.style.display = 'none'; } }); } // Sort history entries by timestamp const sortHistoryEntries = (order) => { conversationHistory.sort((a, b) => { // Default to newest first if timestamps don't exist if (!a.timestamp || !b.timestamp) return 0; if (order === 'newest') { return new Date(b.timestamp) - new Date(a.timestamp); } else { return new Date(a.timestamp) - new Date(b.timestamp); } }); } // Render history entries in the panel const renderHistoryEntries = () => { const historyEntries = document.getElementById('historyEntries'); if (!historyEntries) return; historyEntries.innerHTML = ''; if (!conversationHistory || conversationHistory.length === 0) { historyEntries.innerHTML = '
No research history yet.
'; return; } // Sort by the current selection const sortOrder = document.getElementById('historySortOrder')?.value || 'newest'; sortHistoryEntries(sortOrder); console.debug('Sorted history entries:', sortOrder); conversationHistory.forEach((entry, index) => { const entryElement = document.createElement('div'); entryElement.className = 'history-entry'; entryElement.setAttribute('data-id', index); // Make the entire entry clickable to load it entryElement.addEventListener('click', () => { loadResearchEntry(index); }); // Format timestamp if available let timestampHTML = ''; if (entry.timestamp) { try { const timestamp = new Date(entry.timestamp); const formattedDate = timestamp.toLocaleDateString(); const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); timestampHTML = ``; } catch (e) { console.error('Error formatting timestamp:', e); } } // Make sure links object exists const links = entry.links || {}; // Build the HTML for the entry with enhanced formatting entryElement.innerHTML = `No images found for this research.
' } } const showImageDialog = (imageUrl) => { let dialog = document.querySelector('.image-dialog'); if (!dialog) { dialog = document.createElement('div'); dialog.className = 'image-dialog'; const img = document.createElement('img'); img.alt = 'Full-size Research Image'; const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close'; closeBtn.className = 'close-btn'; // Added class for styling dialog.appendChild(img); dialog.appendChild(closeBtn); document.body.appendChild(dialog); closeBtn.onclick = () => { dialog.classList.remove('visible'); }; // Close on clicking backdrop dialog.addEventListener('click', (e) => { if (e.target === dialog) { dialog.classList.remove('visible'); } }); } const imgElement = dialog.querySelector('img'); imgElement.src = imageUrl; dialog.classList.add('visible'); // Close with Escape key const escapeKeyListener = (e) => { if (e.key === 'Escape') { dialog.classList.remove('visible'); document.removeEventListener('keydown', escapeKeyListener); } }; document.addEventListener('keydown', escapeKeyListener); } // Function to show download bar and enable buttons const showDownloadPanels = () => { // Show the bar by adding the visible class const stickyDownloadsBar = document.getElementById('stickyDownloadsBar'); if (stickyDownloadsBar) { stickyDownloadsBar.classList.add('visible'); } // Enable all download buttons const downloadButtons = document.querySelectorAll('.download-option-btn, .report-action-btn'); downloadButtons.forEach(button => { button.classList.remove('disabled'); }); // Make top buttons report-actions section visible const reportActions = document.querySelector('.report-actions'); if (reportActions) { reportActions.style.display = 'flex'; } } // --- Storage Helpers (Cookies or LocalStorage) --- function setCookie(name, value, days) { // Maximum cookie size is around 4KB (4096 bytes) const MAX_COOKIE_SIZE = 4000; // If cookies are disabled, use localStorage instead if (!cookiesEnabled) { try { localStorage.setItem(name, value); console.debug(`Data saved to localStorage: ${name}`); return true; } catch (e) { console.error("Error saving to localStorage:", e); return false; } } let expires = ''; if (days) { const date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = '; expires=' + date.toUTCString(); } // Encode the value const encodedValue = encodeURIComponent(value); // Calculate cookie size const cookieSize = (name + '=' + encodedValue + expires + '; path=/').length; console.debug(`Setting cookie: ${name}, size: ${cookieSize} bytes`); // If cookie is too large, display warning and truncate history if (cookieSize > MAX_COOKIE_SIZE) { console.warn(`Cookie size (${cookieSize} bytes) exceeds the ${MAX_COOKIE_SIZE} bytes limit!`); showToast('Warning: History too large for cookie storage! Oldest entries will be removed.'); if (name === 'conversationHistory') { try { // Parse, reduce entries, and try again const historyData = JSON.parse(value); if (Array.isArray(historyData) && historyData.length > 1) { // Remove the last entry and try again recursively const reducedHistory = historyData.slice(0, -1); console.debug(`Reducing history from ${historyData.length} to ${reducedHistory.length} entries`); setCookie(name, JSON.stringify(reducedHistory), days); return; // Exit after recursive call } } catch (e) { console.error('Could not parse history to reduce size:', e); } } return false; // Indicate failure } // Set the cookie document.cookie = name + '=' + encodedValue + expires + '; path=/'; console.debug(`Cookie set: ${name}`); return true; // Indicate success } function getCookie(name) { console.debug(`Getting data: ${name}`); // If cookies are disabled, use localStorage instead if (!cookiesEnabled) { try { const value = localStorage.getItem(name); if (value) { console.debug(`Data found in localStorage: ${name}, length: ${value.length} chars`); return value; } console.debug(`Data not found in localStorage: ${name}`); return null; } catch (e) { console.error("Error retrieving from localStorage:", e); return null; } } const nameEQ = name + '='; const ca = document.cookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) { const value = decodeURIComponent(c.substring(nameEQ.length, c.length)); console.debug(`Found cookie: ${name}, length: ${value.length} chars`); return value; } } console.debug(`Cookie not found: ${name}`); return null; } function deleteCookie(name) { console.debug(`Deleting storage: ${name}`); // If cookies are disabled, use localStorage instead if (!cookiesEnabled) { try { localStorage.removeItem(name); return; } catch (e) { console.error("Error removing from localStorage:", e); return; } } document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; } // --- End Storage Helpers --- // Debug Helper - check cookie status const checkCookieStatus = () => { if (!cookiesEnabled) { const storageData = localStorage.getItem('conversationHistory'); if (storageData) { const byteSize = new Blob([storageData]).size; const kilobyteSize = (byteSize / 1024).toFixed(2); try { const parsed = JSON.parse(storageData); const entryCount = Array.isArray(parsed) ? parsed.length : 0; showToast(`Using localStorage: ${kilobyteSize}KB, ${entryCount} entries`); console.debug(`LocalStorage size: ${byteSize} bytes, ${kilobyteSize}KB`); console.debug(`LocalStorage entries: ${entryCount}`); } catch (e) { showToast(`LocalStorage contains invalid data: ${kilobyteSize}KB`); console.error('LocalStorage parse error:', e); } } else { showToast('No research history found in localStorage'); } return; } const allCookies = document.cookie; console.debug('All cookies:', allCookies); const conversationCookie = getCookie('conversationHistory'); if (conversationCookie) { const byteSize = new Blob([conversationCookie]).size; const kilobyteSize = (byteSize / 1024).toFixed(2); try { const parsed = JSON.parse(conversationCookie); const entryCount = Array.isArray(parsed) ? parsed.length : 0; showToast(`Cookie found: ${kilobyteSize}KB, ${entryCount} research entries`); console.debug(`Cookie size: ${byteSize} bytes, ${kilobyteSize}KB`); console.debug(`Cookie entries: ${entryCount}`); } catch (e) { showToast(`Cookie found but invalid: ${kilobyteSize}KB`); console.error('Cookie parse error:', e); } } else { showToast('No research history cookie found'); } } // Export history to a downloadable JSON file const exportHistory = () => { try { if (!conversationHistory || conversationHistory.length === 0) { showToast('No research history to export'); return; } // Create a formatted JSON string with pretty-printing const historyJson = JSON.stringify(conversationHistory, null, 2); // Create a Blob containing the data const blob = new Blob([historyJson], { type: 'application/json' }); // Create an object URL for the blob const url = URL.createObjectURL(blob); // Create a temporary link element const link = document.createElement('a'); link.href = url; // Set download attribute with filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); link.download = `research-history-${timestamp}.json`; // Append to the document document.body.appendChild(link); // Programmatically click the link to trigger the download link.click(); // Clean up document.body.removeChild(link); URL.revokeObjectURL(url); showToast('Research history exported to JSON file'); console.debug('History exported, entries:', conversationHistory.length); } catch (error) { console.error('Error exporting history:', error); showToast('Error exporting research history'); } } // Trigger the file input for importing history const triggerImportHistory = () => { const fileInput = document.getElementById('historyFileInput'); if (fileInput) { fileInput.click(); } else { showToast('Import functionality not available'); } } // Handle the file import for history const handleFileImport = (event) => { const file = event.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = (e) => { try { const content = e.target.result; const importedData = JSON.parse(content); // Validate the imported data if (!Array.isArray(importedData)) { throw new Error('Imported data is not an array'); } // Check if each entry has the required fields const validEntries = importedData.filter(entry => { return entry && typeof entry === 'object' && (entry.prompt || entry.task) && // Allow both prompt and legacy task field (entry.links || entry.downloadLinks); // Allow both links and legacy downloadLinks }); if (validEntries.length === 0) { showToast('No valid research entries found in the imported file'); return; } // Map the entries to the current structure if needed const mappedEntries = validEntries.map(entry => { return { prompt: entry.prompt || entry.task || '', links: entry.links || entry.downloadLinks || {}, timestamp: entry.timestamp || new Date().toISOString() }; }); // Confirm before overwriting existing history if (conversationHistory && conversationHistory.length > 0) { if (confirm(`You have ${conversationHistory.length} existing research entries. Do you want to: - Click OK to MERGE imported history with existing history - Click Cancel to REPLACE all existing history with imported data`)) { // Merge with existing history conversationHistory = [...mappedEntries, ...conversationHistory]; } else { // Replace existing history conversationHistory = mappedEntries; } } else { // No existing history, just set the imported data conversationHistory = mappedEntries; } // Save the new history and update the UI saveConversationHistory(); renderHistoryEntries(); showToast(`Successfully imported ${validEntries.length} research entries`); console.debug('Research history imported, valid entries:', validEntries.length); } catch (error) { console.error('Error importing history:', error); showToast('Error importing research history: Invalid file format'); } // Reset the file input so the same file can be selected again event.target.value = ''; }; reader.onerror = () => { console.error('Error reading file'); showToast('Error reading the imported file'); event.target.value = ''; }; reader.readAsText(file); } // Initialize chat functionality const initChat = () => { const chatInput = document.getElementById('chatInput'); const sendChatBtn = document.getElementById('sendChatBtn'); const voiceInputBtn = document.getElementById('voiceInputBtn'); if (!chatInput || !sendChatBtn) return; // Clear previous messages const chatMessages = document.getElementById('chatMessages'); if (chatMessages) { chatMessages.innerHTML = ''; } // Add event listeners for chat input chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMessage(); } }); sendChatBtn.addEventListener('click', sendChatMessage); // Initialize speech recognition if supported if (voiceInputBtn) { initSpeechRecognition(voiceInputBtn, chatInput); } // Auto-resize textarea as content grows chatInput.addEventListener('input', () => { chatInput.style.height = 'auto'; chatInput.style.height = (chatInput.scrollHeight) + 'px'; }); // Add welcome message addChatMessage('I can answer questions about the research report. What would you like to know?', false); } // Initialize speech recognition const initSpeechRecognition = (button, inputElement) => { // Check if browser supports speech recognition const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { console.warn('Speech recognition not supported in this browser'); button.style.display = 'none'; return; } const recognition = new SpeechRecognition(); // Configure speech recognition recognition.continuous = false; recognition.lang = 'en-US'; recognition.interimResults = true; recognition.maxAlternatives = 1; let isListening = false; let finalTranscript = ''; // Add event listeners for speech recognition recognition.onstart = () => { isListening = true; finalTranscript = ''; button.classList.add('listening'); button.innerHTML = ''; button.title = 'Stop listening'; // Show visual feedback showToast('Listening...', 1000); }; recognition.onresult = (event) => { let interimTranscript = ''; // Loop through the results for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } // Update the input element with the transcription inputElement.value = finalTranscript + interimTranscript; // Trigger input event to resize textarea const inputEvent = new Event('input', { bubbles: true }); inputElement.dispatchEvent(inputEvent); }; recognition.onerror = (event) => { console.error('Speech recognition error', event.error); resetRecognition(); if (event.error === 'not-allowed') { showToast('Microphone access denied. Please allow microphone access in your browser settings.', 3000); } else { showToast('Speech recognition error: ' + event.error, 3000); } }; recognition.onend = () => { resetRecognition(); }; // Reset the recognition state const resetRecognition = () => { isListening = false; button.classList.remove('listening'); button.innerHTML = ''; button.title = 'Use voice input'; }; // Toggle speech recognition on button click button.addEventListener('click', () => { if (isListening) { recognition.stop(); } else { recognition.start(); } }); }; // Create a new function to handle WebSocket reconnection const reconnectWebSocket = (message = null) => { // Don't attempt too many reconnections if (reconnectAttempts >= maxReconnectAttempts) { console.error(`Failed to reconnect after ${maxReconnectAttempts} attempts`); addChatMessage(`Unable to reconnect after ${maxReconnectAttempts} attempts. Please refresh the page.`, false); return false; } reconnectAttempts++; // Calculate backoff time (exponential backoff) const backoff = reconnectInterval * Math.pow(1.5, reconnectAttempts - 1); console.log(`Attempting to reconnect (${reconnectAttempts}/${maxReconnectAttempts}) in ${backoff}ms...`); // Show reconnection status to user addChatMessage(`Connection lost. Attempting to reconnect (${reconnectAttempts}/${maxReconnectAttempts})...`, false); // Try to reconnect after delay setTimeout(() => { try { // Setup new WebSocket connection dispose_socket = listenToSockEvents(); // Set up a one-time handler to send the message after reconnection if (message) { const messageToSend = message; const checkConnectionAndSend = () => { if (socket && socket.readyState === WebSocket.OPEN) { console.log("Reconnected successfully, sending queued message"); socket.send(messageToSend); return true; } else if (reconnectAttempts < maxReconnectAttempts) { console.log("Socket not ready yet, retrying..."); setTimeout(checkConnectionAndSend, 1000); return false; } return false; }; setTimeout(checkConnectionAndSend, 1000); } return true; } catch (e) { console.error("Error during reconnection:", e); return false; } }, backoff); return true; }; // Send a chat message const sendChatMessage = () => { const chatInput = document.getElementById('chatInput'); if (!chatInput || !chatInput.value.trim()) return; const message = chatInput.value.trim(); // Add user message to chat addChatMessage(message, true); // Clear input chatInput.value = ''; chatInput.style.height = 'auto'; // Add loading indicator const loadingId = addLoadingIndicator(); // Prepare the message to send const messageToSend = `chat ${JSON.stringify({ message: message })}`; // Send message through WebSocket if (socket && socket.readyState === WebSocket.OPEN) { socket.send(messageToSend); } else { // If socket is closed, try to reconnect removeLoadingIndicator(loadingId); // Reset reconnect attempts if this is a new chat session if (reconnectAttempts >= maxReconnectAttempts) { reconnectAttempts = 0; } // Attempt to reconnect and queue the message to be sent after reconnection if (!reconnectWebSocket(messageToSend)) { // If reconnection fails or max attempts reached addChatMessage('Unable to send message. Connection is unavailable.', false); } } } // Add a chat message to the UI const addChatMessage = (message, isUser = false) => { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) return; const messageEl = document.createElement('div'); messageEl.className = `chat-message ${isUser ? 'user-message' : 'ai-message'}`; // Process message for AI responses (convert markdown to HTML for AI messages) let processedMessage = message; if (!isUser) { // Use showdown for markdown conversion const converter = new showdown.Converter({ ghCodeBlocks: true, tables: true, tasklists: true, openLinksInNewWindow: true }); processedMessage = converter.makeHtml(message); } // Set message content messageEl.innerHTML = isUser ? escapeHtml(processedMessage) : processedMessage; // Add timestamp const timestampEl = document.createElement('div'); timestampEl.className = 'chat-timestamp'; const now = new Date(); timestampEl.textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); messageEl.appendChild(timestampEl); // Add to chat container chatMessages.appendChild(messageEl); // Scroll to bottom chatMessages.scrollTop = chatMessages.scrollHeight; } // Add a loading indicator const addLoadingIndicator = () => { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) return null; const loadingId = 'loading-' + Date.now(); const loadingEl = document.createElement('div'); loadingEl.className = 'chat-message ai-message chat-loading'; loadingEl.id = loadingId; // Create the dots for (let i = 0; i < 3; i++) { const dot = document.createElement('div'); dot.className = 'chat-dot'; loadingEl.appendChild(dot); } chatMessages.appendChild(loadingEl); chatMessages.scrollTop = chatMessages.scrollHeight; return loadingId; } // Remove loading indicator const removeLoadingIndicator = (loadingId) => { if (!loadingId) return; const loadingEl = document.getElementById(loadingId); if (loadingEl) { loadingEl.remove(); } } // Escape HTML to prevent XSS in user messages const escapeHtml = (text) => { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize expand buttons const initExpandButtons = () => { // Report container expand button const expandReportBtn = document.getElementById('expandReportBtn'); if (expandReportBtn) { expandReportBtn.addEventListener('click', () => { const reportContainer = document.querySelector('.report-container'); toggleExpand(reportContainer); }); } // Chat container expand button const expandChatBtn = document.getElementById('expandChatBtn'); if (expandChatBtn) { expandChatBtn.addEventListener('click', () => { const chatContainer = document.getElementById('chatContainer'); toggleExpand(chatContainer); }); } // Output container expand button const expandOutputBtn = document.getElementById('expandOutputBtn'); if (expandOutputBtn) { expandOutputBtn.addEventListener('click', () => { const outputContainer = document.querySelector('.research-output-container'); toggleExpand(outputContainer); }); } // Close expanded view when ESC key is pressed document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const expandedElements = document.querySelectorAll('.expanded-view'); expandedElements.forEach(el => { // Reset the button icon const button = el.querySelector('.expand-button i'); if (button) { button.classList.remove('fa-compress-alt'); button.classList.add('fa-expand-alt'); } // Reset content container styles const contentContainers = el.querySelectorAll('#reportContainer, #output, #chatMessages'); contentContainers.forEach(container => { if (container) { container.style.maxHeight = ''; } }); // Remove expanded-view class el.classList.remove('expanded-view'); }); } }); } // Toggle expand mode for an element const toggleExpand = (element) => { if (!element) return; // Toggle expanded-view class element.classList.toggle('expanded-view'); // Change button icon and title based on state const buttonIcon = element.querySelector('.expand-button i'); const button = element.querySelector('.expand-button'); if (buttonIcon && button) { if (element.classList.contains('expanded-view')) { buttonIcon.classList.remove('fa-compress-alt'); buttonIcon.classList.add('fa-compress-alt'); button.title = 'Collapse'; // Update title to Collapse // Find content containers and expand their height const contentContainers = element.querySelectorAll('#reportContainer, #output, #chatMessages'); contentContainers.forEach(container => { if (container) { // Set expanded heights - no positioning changes if (container.id === 'reportContainer') { container.style.maxHeight = '800px'; // Fixed expanded height for report } else { container.style.maxHeight = '600px'; // Fixed expanded height for other content } } }); } else { buttonIcon.classList.remove('fa-compress-alt'); buttonIcon.classList.add('fa-expand-alt'); button.title = 'Expand'; // Update title to Expand // Reset heights back to original when collapsed const contentContainers = element.querySelectorAll('#reportContainer, #output, #chatMessages'); contentContainers.forEach(container => { if (container) { container.style.maxHeight = ''; } }); } } } // MCP Configuration Management // Initialize MCP functionality const initMCPSection = () => { const mcpEnabled = document.getElementById('mcpEnabled'); const mcpConfigSection = document.getElementById('mcpConfigSection'); const mcpInfoBtn = document.getElementById('mcpInfoBtn'); const mcpConfig = document.getElementById('mcpConfig'); const mcpFormatBtn = document.getElementById('mcpFormatBtn'); const mcpExampleLink = document.getElementById('mcpExampleLink'); if (!mcpEnabled || !mcpConfigSection) { console.warn('MCP elements not found'); return; } // Toggle MCP config section mcpEnabled.addEventListener('change', () => { if (mcpEnabled.checked) { mcpConfigSection.style.display = 'block'; updateRetrieverForMCP(true); } else { mcpConfigSection.style.display = 'none'; updateRetrieverForMCP(false); } }); // MCP info modal if (mcpInfoBtn) { mcpInfoBtn.addEventListener('click', showMCPInfo); } // JSON validation and formatting if (mcpConfig) { mcpConfig.addEventListener('input', validateMCPConfig); mcpConfig.addEventListener('blur', validateMCPConfig); } if (mcpFormatBtn) { mcpFormatBtn.addEventListener('click', formatMCPConfig); } if (mcpExampleLink) { mcpExampleLink.addEventListener('click', (e) => { e.preventDefault(); showMCPExample(); }); } // Preset buttons const presetButtons = document.querySelectorAll('.preset-btn'); presetButtons.forEach(btn => { btn.addEventListener('click', (e) => { const preset = e.currentTarget.dataset.preset; addMCPPreset(preset); }); }); // Create MCP info modal createMCPInfoModal(); // Initial validation validateMCPConfig(); }; // Validate MCP JSON configuration const validateMCPConfig = () => { const mcpConfig = document.getElementById('mcpConfig'); const mcpConfigStatus = document.getElementById('mcpConfigStatus'); if (!mcpConfig || !mcpConfigStatus) return; const configText = mcpConfig.value.trim(); if (!configText || configText === '[]') { mcpConfig.className = 'form-control mcp-config-textarea'; mcpConfigStatus.textContent = 'Empty configuration'; mcpConfigStatus.className = 'mcp-status-text'; return true; } try { const parsed = JSON.parse(configText); if (!Array.isArray(parsed)) { throw new Error('Configuration must be an array'); } // Validate each server config const errors = []; parsed.forEach((server, index) => { if (!server.name) { errors.push(`Server ${index + 1}: missing name`); } if (!server.command && !server.connection_url) { errors.push(`Server ${index + 1}: missing command or connection_url`); } }); if (errors.length > 0) { throw new Error(errors.join('; ')); } mcpConfig.className = 'form-control mcp-config-textarea valid'; mcpConfigStatus.textContent = `Valid JSON ✓ (${parsed.length} server${parsed.length !== 1 ? 's' : ''})`; mcpConfigStatus.className = 'mcp-status-text valid'; return true; } catch (error) { mcpConfig.className = 'form-control mcp-config-textarea invalid'; mcpConfigStatus.textContent = `Invalid JSON: ${error.message}`; mcpConfigStatus.className = 'mcp-status-text invalid'; return false; } }; // Format MCP JSON configuration const formatMCPConfig = () => { const mcpConfig = document.getElementById('mcpConfig'); if (!mcpConfig) return; try { const parsed = JSON.parse(mcpConfig.value.trim() || '[]'); mcpConfig.value = JSON.stringify(parsed, null, 2); validateMCPConfig(); showToast('JSON formatted successfully!'); } catch (error) { showToast('Cannot format invalid JSON'); } }; // Show MCP configuration example const showMCPExample = () => { const exampleConfig = [ { "name": "github", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "your_github_token_here" } }, { "name": "filesystem", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"], "env": {} } ]; const mcpConfig = document.getElementById('mcpConfig'); if (mcpConfig) { mcpConfig.value = JSON.stringify(exampleConfig, null, 2); validateMCPConfig(); showToast('Example configuration loaded!'); } }; // Update retriever configuration for MCP const updateRetrieverForMCP = (enableMCP) => { if (enableMCP) { showToast('MCP enabled - will be included in research process'); } else { showToast('MCP disabled - using web search only'); } }; // Show MCP information modal const showMCPInfo = () => { const modal = document.getElementById('mcpInfoModal'); if (modal) { modal.classList.add('visible'); } }; // Create MCP info modal const createMCPInfoModal = () => { if (document.getElementById('mcpInfoModal')) return; const modal = document.createElement('div'); modal.id = 'mcpInfoModal'; modal.className = 'mcp-info-modal'; modal.innerHTML = `MCP enables GPT Researcher to connect with external tools and data sources through a standardized protocol.
Each MCP server should be a JSON object with these properties: