// localStorage key for servers const LOCAL_STORAGE_SERVERS_KEY = 'openai_servers'; document.addEventListener('DOMContentLoaded', () => { const loadingDiv = document.getElementById('loading'); const errorMessageDiv = document.getElementById('error-message'); const resultsDiv = document.getElementById('results'); const modelsListDiv = document.getElementById('models-list'); // Server Management Elements const serverNameInput = document.getElementById('server-name'); const serverUrlInput = document.getElementById('server-url'); const serverApiKeyInput = document.getElementById('server-api-key'); const addServerButton = document.getElementById('add-server'); const serverSelector = document.getElementById('server-selector'); const removeServerButton = document.getElementById('remove-server'); const serverSettingsBtn = document.getElementById('server-settings-btn'); const serverModal = document.getElementById('server-modal'); const closeServerModal = document.getElementById('close-server-modal'); const serverListItems = document.getElementById('server-list-items'); // Chat Modal Elements const chatModal = document.getElementById('chat-modal'); const chatModelName = document.getElementById('chat-model-name'); const chatMessages = document.getElementById('chat-messages'); const chatInput = document.getElementById('chat-input'); const sendMessageButton = document.getElementById('send-message'); const closeChatModalButton = chatModal.querySelector('.close'); const closeModalBackground = chatModal; // Ensure modal is hidden on page load (defensive programming) if (chatModal) { chatModal.classList.add('hidden'); } // Current chat state let currentChatModel = null; let currentEndpointUrl = null; let currentApiKey = null; // Initialize server management console.log('Initializing server management...'); loadServers(); console.log('Server management initialized'); // Add event listeners for server management addServerButton.addEventListener('click', addServer); removeServerButton.addEventListener('click', removeServer); serverSettingsBtn.addEventListener('click', () => { serverModal.classList.remove('hidden'); updateServerListDisplay(); }); closeServerModal.addEventListener('click', () => { serverModal.classList.add('hidden'); }); // Auto-fetch models when server selection changes serverSelector.addEventListener('change', fetchModels); // Close modals when clicking outside serverModal.addEventListener('click', (e) => { if (e.target === serverModal) { serverModal.classList.add('hidden'); } }); // Chat Modal Event Listeners - Improved robustness if (closeChatModalButton) { closeChatModalButton.addEventListener('click', closeChatModal); } if (closeModalBackground) { closeModalBackground.addEventListener('click', (e) => { if (e.target === closeModalBackground) { closeChatModal(); } }); } // Also allow ESC key to close modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && chatModal && !chatModal.classList.contains('hidden')) { closeChatModal(); } }); sendMessageButton.addEventListener('click', sendMessage); chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); function loadServers() { console.log('Loading servers from localStorage...'); try { // Get the raw data from localStorage const rawData = localStorage.getItem(LOCAL_STORAGE_SERVERS_KEY); console.log('Raw localStorage data:', rawData); // Handle case where no data exists if (rawData === null || rawData === undefined) { console.log('No servers found in localStorage'); updateServerSelector([]); return; } // Parse the JSON data const servers = JSON.parse(rawData || '{}'); console.log('Parsed servers:', servers); // Validate that parsed data is an object if (typeof servers !== 'object' || servers === null || Array.isArray(servers)) { console.warn('Invalid servers format in localStorage, resetting to empty object'); localStorage.setItem(LOCAL_STORAGE_SERVERS_KEY, JSON.stringify({})); updateServerSelector([]); return; } const serverNames = Object.keys(servers); console.log('Server names found:', serverNames); updateServerSelector(serverNames); updateServerListDisplay(); // Update the modal list display console.log('Servers loaded successfully'); // Add visual feedback if servers were loaded if (serverNames.length > 0) { console.log(`Successfully loaded ${serverNames.length} server(s)`); // Briefly highlight the server selector to show it was updated serverSelector.style.border = '2px solid #27ae60'; setTimeout(() => { serverSelector.style.border = '2px solid #ddd'; }, 1000); } } catch (error) { console.error('Error loading servers from localStorage:', error); console.error('localStorage data that caused error:', localStorage.getItem(LOCAL_STORAGE_SERVERS_KEY)); updateServerSelector([]); } } function updateServerSelector(serverNames) { // Clear existing options except the default serverSelector.innerHTML = ''; // Add available servers serverNames.forEach(serverName => { const option = document.createElement('option'); option.value = serverName; option.textContent = serverName; serverSelector.appendChild(option); }); } function updateServerListDisplay() { try { const servers = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SERVERS_KEY) || '{}'); const serverNames = Object.keys(servers); serverListItems.innerHTML = ''; if (serverNames.length === 0) { serverListItems.innerHTML = '
  • No servers saved
  • '; return; } serverNames.forEach(serverName => { const li = document.createElement('li'); li.textContent = serverName; li.title = `URL: ${servers[serverName].url}`; serverListItems.appendChild(li); }); } catch (error) { console.error('Error updating server list display:', error); serverListItems.innerHTML = '
  • Error loading servers
  • '; } } function addServer() { const name = serverNameInput.value.trim(); const url = serverUrlInput.value.trim(); const apiKey = serverApiKeyInput.value.trim(); if (!name || !url || !apiKey) { showError('Please enter a server name, URL, and API key'); return; } // Validate URL format try { new URL(url); } catch (e) { showError('Please enter a valid URL (e.g., https://api.openai.com)'); return; } try { // Get existing servers from localStorage const servers = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SERVERS_KEY) || '{}'); // Add new server with both URL and API key servers[name] = { url: url, apiKey: apiKey }; // Save back to localStorage localStorage.setItem(LOCAL_STORAGE_SERVERS_KEY, JSON.stringify(servers)); // Clear inputs serverNameInput.value = ''; serverUrlInput.value = ''; serverApiKeyInput.value = ''; // Reload servers loadServers(); showError('Server added successfully', 'success'); } catch (error) { console.error('Error adding server to localStorage:', error); showError('Failed to add server. Please try again.'); } } function removeServer() { const selectedServerName = serverSelector.value; if (!selectedServerName) { showError('Please select a server to remove'); return; } if (!confirm(`Are you sure you want to remove the server "${selectedServerName}"?`)) { return; } try { // Get existing servers from localStorage const servers = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SERVERS_KEY) || '{}'); // Remove the selected server delete servers[selectedServerName]; // Save back to localStorage localStorage.setItem(LOCAL_STORAGE_SERVERS_KEY, JSON.stringify(servers)); // Reload servers loadServers(); showError('Server removed successfully', 'success'); } catch (error) { console.error('Error removing server from localStorage:', error); showError('Failed to remove server. Please try again.'); } } async function fetchModels() { const selectedServerName = serverSelector.value; // Must select a server if (!selectedServerName) { showError('Please select a server'); return; } // Get server configuration from localStorage try { const servers = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SERVERS_KEY) || '{}'); const selectedServer = servers[selectedServerName]; if (!selectedServer) { showError(`Server "${selectedServerName}" not found`); return; } // Show loading, hide results and errors showLoading(); hideError(); hideResults(); // Construct models endpoint URL let url = selectedServer.url; if (!url.endsWith('/models')) { url = url.replace(/\/$/, '') + '/models'; } // Set up headers const headers = { 'Content-Type': 'application/json' }; if (selectedServer.apiKey) { headers['Authorization'] = `Bearer ${selectedServer.apiKey}`; } // Make direct API call const response = await fetch(url, { method: 'GET', headers: headers }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`); } const data = await response.json(); if (!data.data || !Array.isArray(data.data)) { throw new Error('Invalid response format: expected "data" array'); } displayModels(data.data, selectedServer.url, selectedServer.apiKey); } catch (error) { console.error('Error fetching models:', error); showError(`Failed to fetch models: ${error.message}`); } finally { hideLoading(); } } function displayModels(models, endpointUrl, apiKey) { modelsListDiv.innerHTML = ''; if (models.length === 0) { modelsListDiv.innerHTML = '

    No models found.

    '; } else { models.forEach(model => { const modelItem = document.createElement('div'); modelItem.className = 'model-item'; modelItem.dataset.modelId = model.id; modelItem.dataset.endpointUrl = endpointUrl; modelItem.dataset.apiKey = apiKey || ''; const modelName = model.name || model.id || 'Unknown Model'; const modelId = model.id || 'Unknown ID'; modelItem.innerHTML = `
    ${modelName}
    ${modelId}
    `; // Add click event listener to open chat modelItem.addEventListener('click', () => { openChatModal(modelId, modelName, endpointUrl, apiKey); }); modelsListDiv.appendChild(modelItem); }); } showResults(); } function openChatModal(modelId, modelName, endpointUrl, apiKey) { currentChatModel = modelId; currentEndpointUrl = endpointUrl; currentApiKey = apiKey; chatModelName.textContent = `Chat with ${modelName}`; chatMessages.innerHTML = ''; chatInput.value = ''; chatModal.classList.remove('hidden'); chatInput.focus(); // Add welcome message addMessage('ai', `Hello! I'm ${modelName}. How can I help you today?`); } function closeChatModal() { // Defensive programming - ensure modal exists if (chatModal) { chatModal.classList.add('hidden'); // Additional safety - ensure all chat state is cleared currentChatModel = null; currentEndpointUrl = null; currentApiKey = null; // Clear chat messages when closing if (chatMessages) { chatMessages.innerHTML = ''; } // Clear input if (chatInput) { chatInput.value = ''; } // Disable send button if (sendMessageButton) { sendMessageButton.disabled = true; } } } function addMessage(role, content) { const messageDiv = document.createElement('div'); messageDiv.className = `chat-message ${role}`; messageDiv.textContent = content; chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; } function sendMessage() { const message = chatInput.value.trim(); if (!message || !currentChatModel) return; // Add user message addMessage('user', message); chatInput.value = ''; sendMessageButton.disabled = true; // Add AI response placeholder for streaming const aiMessageDiv = document.createElement('div'); aiMessageDiv.className = 'chat-message ai streaming'; aiMessageDiv.textContent = 'Thinking...'; chatMessages.appendChild(aiMessageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; // Construct chat completions endpoint URL let chatUrl = currentEndpointUrl; if (chatUrl.endsWith('/chat/completions')) { // Already correct format } else if (chatUrl.endsWith('/v1')) { chatUrl = `${chatUrl}/chat/completions`; } else { chatUrl = `${chatUrl.replace(/\/$/, '')}/v1/chat/completions`; } // Prepare the request body for direct API call const requestBody = { model: currentChatModel, messages: [ { role: 'user', content: message } ], stream: false // Set to true for streaming support if needed }; // Set up headers const headers = { 'Content-Type': 'application/json' }; if (currentApiKey) { headers['Authorization'] = `Bearer ${currentApiKey}`; } // Make direct API call fetch(chatUrl, { method: 'POST', headers: headers, body: JSON.stringify(requestBody) }) .then(response => { if (!response.ok) { return response.text().then(errorText => { throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`); }); } return response.json(); }) .then(data => { // Remove streaming placeholder chatMessages.removeChild(aiMessageDiv); // Extract the response content const content = data.choices && data.choices[0] && data.choices[0].message ? data.choices[0].message.content : 'No response received.'; // Add actual AI response const responseDiv = document.createElement('div'); responseDiv.className = 'chat-message ai'; responseDiv.textContent = content; chatMessages.appendChild(responseDiv); chatMessages.scrollTop = chatMessages.scrollHeight; }) .catch(error => { // Remove streaming placeholder chatMessages.removeChild(aiMessageDiv); // Add error message const errorDiv = document.createElement('div'); errorDiv.className = 'chat-message ai'; errorDiv.textContent = `Error: ${error.message}`; chatMessages.appendChild(errorDiv); chatMessages.scrollTop = chatMessages.scrollHeight; }) .finally(() => { sendMessageButton.disabled = false; }); } function showLoading() { loadingDiv.classList.remove('hidden'); } function hideLoading() { loadingDiv.classList.add('hidden'); } function showError(message, type = 'error') { errorMessageDiv.textContent = message; errorMessageDiv.classList.remove('hidden'); // Add success styling for success messages if (type === 'success') { errorMessageDiv.style.backgroundColor = '#27ae60'; } else { errorMessageDiv.style.backgroundColor = '#e74c3c'; } // Auto-hide success messages after 3 seconds if (type === 'success') { setTimeout(() => { hideError(); }, 3000); } } function hideError() { errorMessageDiv.classList.add('hidden'); } function showResults() { resultsDiv.classList.remove('hidden'); } function hideResults() { resultsDiv.classList.add('hidden'); } });