Initial version
This commit is contained in:
commit
50d66c2985
5 changed files with 1117 additions and 0 deletions
18
app.py
Normal file
18
app.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from flask import Flask, send_from_directory
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return send_from_directory('static', 'index.html')
|
||||||
|
|
||||||
|
# Serve static files
|
||||||
|
@app.route('/<path:filename>')
|
||||||
|
def static_files(filename):
|
||||||
|
return send_from_directory('static', filename)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
flask
|
||||||
|
requests
|
||||||
96
static/index.html
Normal file
96
static/index.html
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OpenAI Models Viewer</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>OpenAI Models Viewer</h1>
|
||||||
|
<p>Enter an OpenAI-compatible endpoint URL to view available models</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Server Management Section -->
|
||||||
|
<div class="server-section">
|
||||||
|
<h3>Server Management</h3>
|
||||||
|
<div class="server-select-group">
|
||||||
|
<label for="server-selector">Select Server:</label>
|
||||||
|
<div class="server-selector-container">
|
||||||
|
<select id="server-selector">
|
||||||
|
<option value="">Select a server...</option>
|
||||||
|
</select>
|
||||||
|
<button id="server-settings-btn" class="settings-btn" title="Server Settings">
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-section">
|
||||||
|
<!-- Manual URL entry removed -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="loading hidden">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Fetching models...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error-message" class="error hidden"></div>
|
||||||
|
|
||||||
|
<div id="results" class="results hidden">
|
||||||
|
<h2>Available Models</h2>
|
||||||
|
<div id="models-list"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Management Modal -->
|
||||||
|
<div id="server-modal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Server Management</h3>
|
||||||
|
<span class="close" id="close-server-modal">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="server-input-group">
|
||||||
|
<label for="server-name">Server Name:</label>
|
||||||
|
<input type="text" id="server-name" placeholder="Server Name (e.g., OpenAI Prod)">
|
||||||
|
<label for="server-url">Server URL:</label>
|
||||||
|
<input type="url" id="server-url" placeholder="https://api.openai.com">
|
||||||
|
<label for="server-api-key">API Key:</label>
|
||||||
|
<input type="password" id="server-api-key" placeholder="API Key">
|
||||||
|
<button id="add-server">Add Server</button>
|
||||||
|
</div>
|
||||||
|
<div class="server-list">
|
||||||
|
<h4>Saved Servers:</h4>
|
||||||
|
<ul id="server-list-items"></ul>
|
||||||
|
</div>
|
||||||
|
<button id="remove-server">Remove Selected Server</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Modal -->
|
||||||
|
<div id="chat-modal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="chat-model-name">Chat with Model</h3>
|
||||||
|
<span class="close">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="chat-messages" class="chat-messages"></div>
|
||||||
|
<div class="chat-input-container">
|
||||||
|
<textarea id="chat-input" placeholder="Type your message..." rows="3"></textarea>
|
||||||
|
<button id="send-message" disabled>Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
468
static/style.css
Normal file
468
static/style.css
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Management Section */
|
||||||
|
.server-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-section h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Selector Container with Settings Button */
|
||||||
|
.server-selector-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Management Modal Styles */
|
||||||
|
#server-modal .modal-content {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list h4 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#server-list-items {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#server-list-items li {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#server-list-items li:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input-group,
|
||||||
|
.server-select-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input-group label,
|
||||||
|
.server-select-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endpoint-url,
|
||||||
|
#server-url,
|
||||||
|
#server-api-key,
|
||||||
|
#server-name {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endpoint-url:focus,
|
||||||
|
#server-url:focus,
|
||||||
|
#server-api-key:focus,
|
||||||
|
#server-name:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetch-button,
|
||||||
|
#add-server,
|
||||||
|
#remove-server {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetch-button:hover,
|
||||||
|
#add-server:hover,
|
||||||
|
#remove-server:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetch-button:disabled {
|
||||||
|
background-color: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Selector Styling */
|
||||||
|
#server-selector {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#server-selector:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Button Styling */
|
||||||
|
#add-server,
|
||||||
|
#remove-server {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-server:hover,
|
||||||
|
#remove-server:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green button for add */
|
||||||
|
#add-server {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-server:hover {
|
||||||
|
background-color: #219653;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Red button for remove */
|
||||||
|
#remove-server {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#remove-server:hover {
|
||||||
|
background-color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-item {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-id {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section,
|
||||||
|
.results {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Modal Styles */
|
||||||
|
.modal {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure hidden modals are completely invisible and don't interfere with page interaction */
|
||||||
|
.modal.hidden {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.user {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.ai {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-message {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-message:hover:not(:disabled) {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-message:disabled {
|
||||||
|
background-color: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.streaming {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue