Initial version
This commit is contained in:
commit
50d66c2985
5 changed files with 1117 additions and 0 deletions
533
static/script.js
Normal file
533
static/script.js
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
// 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 = '<option value="">Select a server...</option>';
|
||||
|
||||
// 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 = '<li>No servers saved</li>';
|
||||
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 = '<li>Error loading servers</li>';
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<p>No models found.</p>';
|
||||
} 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 = `
|
||||
<div class="model-name">${modelName}</div>
|
||||
<div class="model-id">${modelId}</div>
|
||||
`;
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue