feat: major dashboard overhaul and polish
- Switched from mock data to real backend APIs. - Implemented unified ApiClient for consistent frontend data fetching. - Refactored dashboard structure and styles for a modern SaaS aesthetic. - Fixed Axum 0.8+ routing and parameter syntax issues. - Implemented real client creation/deletion and provider health monitoring. - Synchronized WebSocket event structures between backend and frontend.
This commit is contained in:
@@ -7,644 +7,136 @@ class ProvidersPage {
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load data
|
||||
await this.loadProviderStats();
|
||||
await this.loadProvidersList();
|
||||
await this.loadModelsList();
|
||||
await this.loadConnectionTests();
|
||||
|
||||
// Setup event listeners
|
||||
await this.loadProviders();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadProviderStats() {
|
||||
const container = document.getElementById('provider-stats');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">4</div>
|
||||
<div class="stat-label">Total Providers</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
3 active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-plug"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">3</div>
|
||||
<div class="stat-label">Connected</div>
|
||||
<div class="stat-change positive">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
All systems operational
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">1</div>
|
||||
<div class="stat-label">Issues</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
DeepSeek: 85% health
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">1</div>
|
||||
<div class="stat-label">Offline</div>
|
||||
<div class="stat-change">
|
||||
<i class="fas fa-redo"></i>
|
||||
Grok: Connection failed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
async loadProviders() {
|
||||
try {
|
||||
const data = await window.api.get('/providers');
|
||||
this.providers = data;
|
||||
this.renderProviders();
|
||||
this.renderStats();
|
||||
} catch (error) {
|
||||
console.error('Error loading providers:', error);
|
||||
window.authManager.showToast('Failed to load providers', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async loadProvidersList() {
|
||||
renderProviders() {
|
||||
const container = document.getElementById('providers-list');
|
||||
if (!container) return;
|
||||
|
||||
this.providers = [
|
||||
{ name: 'OpenAI', enabled: true, status: 'online', apiKey: 'sk-*****123', models: ['gpt-4', 'gpt-3.5-turbo'], lastUsed: '2024-01-15 14:32:15' },
|
||||
{ name: 'Gemini', enabled: true, status: 'online', apiKey: 'AIza*****456', models: ['gemini-pro', 'gemini-pro-vision'], lastUsed: '2024-01-15 14:30:45' },
|
||||
{ name: 'DeepSeek', enabled: true, status: 'warning', apiKey: 'sk-*****789', models: ['deepseek-chat', 'deepseek-coder'], lastUsed: '2024-01-15 14:28:12' },
|
||||
{ name: 'Grok', enabled: false, status: 'offline', apiKey: 'gk-*****012', models: ['grok-beta'], lastUsed: '2024-01-12 10:15:22' }
|
||||
];
|
||||
|
||||
container.innerHTML = this.providers.map(provider => {
|
||||
const statusClass = provider.status === 'online' ? 'success' :
|
||||
provider.status === 'warning' ? 'warning' : 'danger';
|
||||
const statusIcon = provider.status === 'online' ? 'check-circle' :
|
||||
provider.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
|
||||
|
||||
return `
|
||||
<div class="provider-card">
|
||||
<div class="provider-header">
|
||||
<div class="provider-info">
|
||||
<h4 class="provider-name">${provider.name}</h4>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${provider.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" ${provider.enabled ? 'checked' : ''} data-provider="${provider.name}">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<button class="btn-action" title="Configure" data-action="configure" data-provider="${provider.name}">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
<button class="btn-action" title="Test Connection" data-action="test" data-provider="${provider.name}">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">API Key:</span>
|
||||
<code class="detail-value">${provider.apiKey}</code>
|
||||
<button class="btn-copy" data-text="${provider.apiKey}" title="Copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Models:</span>
|
||||
<span class="detail-value">${provider.models.join(', ')}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Last Used:</span>
|
||||
<span class="detail-value">${provider.lastUsed}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for provider cards
|
||||
this.addProviderStyles();
|
||||
}
|
||||
if (this.providers.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">No providers configured</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
addProviderStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.provider-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--text-light);
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.provider-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-copy:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
container.innerHTML = `
|
||||
<div class="provider-cards-grid">
|
||||
${this.providers.map(provider => this.renderProviderCard(provider)).join('')}
|
||||
</div>
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadModelsList() {
|
||||
const container = document.getElementById('models-list');
|
||||
if (!container) return;
|
||||
renderProviderCard(provider) {
|
||||
const statusClass = provider.status === 'online' ? 'success' : 'warning';
|
||||
const modelCount = provider.models ? provider.models.length : 0;
|
||||
|
||||
const models = [
|
||||
{ provider: 'OpenAI', name: 'gpt-4', enabled: true, context: 8192, maxTokens: 4096 },
|
||||
{ provider: 'OpenAI', name: 'gpt-3.5-turbo', enabled: true, context: 16384, maxTokens: 4096 },
|
||||
{ provider: 'Gemini', name: 'gemini-pro', enabled: true, context: 32768, maxTokens: 8192 },
|
||||
{ provider: 'Gemini', name: 'gemini-pro-vision', enabled: true, context: 32768, maxTokens: 4096 },
|
||||
{ provider: 'DeepSeek', name: 'deepseek-chat', enabled: true, context: 16384, maxTokens: 4096 },
|
||||
{ provider: 'DeepSeek', name: 'deepseek-coder', enabled: true, context: 16384, maxTokens: 4096 },
|
||||
{ provider: 'Grok', name: 'grok-beta', enabled: false, context: 8192, maxTokens: 2048 }
|
||||
];
|
||||
|
||||
container.innerHTML = models.map(model => `
|
||||
<div class="model-item">
|
||||
<div class="model-header">
|
||||
<span class="model-name">${model.name}</span>
|
||||
<span class="model-provider">${model.provider}</span>
|
||||
return `
|
||||
<div class="provider-card ${provider.status}">
|
||||
<div class="provider-card-header">
|
||||
<div class="provider-info">
|
||||
<h4 class="provider-name">${provider.name}</h4>
|
||||
<span class="provider-id">${provider.id}</span>
|
||||
</div>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-circle"></i>
|
||||
${provider.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="model-details">
|
||||
<span class="model-detail">
|
||||
<i class="fas fa-microchip"></i>
|
||||
Context: ${model.context.toLocaleString()} tokens
|
||||
</span>
|
||||
<span class="model-detail">
|
||||
<i class="fas fa-ruler"></i>
|
||||
Max: ${model.maxTokens.toLocaleString()} tokens
|
||||
</span>
|
||||
<span class="model-status ${model.enabled ? 'enabled' : 'disabled'}">
|
||||
<i class="fas fa-${model.enabled ? 'check' : 'times'}"></i>
|
||||
${model.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<div class="provider-card-body">
|
||||
<div class="provider-meta">
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-microchip"></i>
|
||||
<span>${modelCount} Models Available</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>Last used: ${provider.last_used ? window.api.formatTimeAgo(provider.last_used) : 'Never'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-tags">
|
||||
${(provider.models || []).slice(0, 5).map(m => `<span class="model-tag">${m}</span>`).join('')}
|
||||
${modelCount > 5 ? `<span class="model-tag more">+${modelCount - 5} more</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-card-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="window.providersPage.testProvider('${provider.id}')">
|
||||
<i class="fas fa-vial"></i> Test
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="window.providersPage.configureProvider('${provider.id}')">
|
||||
<i class="fas fa-cog"></i> Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add CSS for model items
|
||||
this.addModelStyles();
|
||||
}
|
||||
|
||||
addModelStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.model-item {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.model-provider {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--bg-primary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.model-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.model-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.model-detail i {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.model-status {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.model-status.enabled {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.model-status.disabled {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: var(--danger);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadConnectionTests() {
|
||||
const container = document.getElementById('connection-tests');
|
||||
renderStats() {
|
||||
const container = document.getElementById('provider-stats');
|
||||
if (!container) return;
|
||||
|
||||
const tests = [
|
||||
{ provider: 'OpenAI', status: 'success', latency: 245, timestamp: '2024-01-15 14:35:00' },
|
||||
{ provider: 'Gemini', status: 'success', latency: 189, timestamp: '2024-01-15 14:34:30' },
|
||||
{ provider: 'DeepSeek', status: 'warning', latency: 520, timestamp: '2024-01-15 14:34:00' },
|
||||
{ provider: 'Grok', status: 'error', latency: null, timestamp: '2024-01-15 14:33:30' }
|
||||
];
|
||||
const onlineCount = this.providers.filter(p => p.status === 'online').length;
|
||||
const totalModels = this.providers.reduce((sum, p) => sum + (p.models ? p.models.length : 0), 0);
|
||||
|
||||
container.innerHTML = tests.map(test => {
|
||||
const statusClass = test.status === 'success' ? 'success' :
|
||||
test.status === 'warning' ? 'warning' : 'danger';
|
||||
const statusIcon = test.status === 'success' ? 'check-circle' :
|
||||
test.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
|
||||
|
||||
return `
|
||||
<div class="test-result">
|
||||
<div class="test-provider">${test.provider}</div>
|
||||
<div class="test-status">
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${test.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="test-latency">${test.latency ? `${test.latency}ms` : 'N/A'}</div>
|
||||
<div class="test-time">${test.timestamp}</div>
|
||||
container.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${this.providers.length}</div>
|
||||
<div class="stat-label">Total Providers</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for test results
|
||||
this.addTestStyles();
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${onlineCount}</div>
|
||||
<div class="stat-label">Online Status</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">${totalModels}</div>
|
||||
<div class="stat-label">Total Models</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
addTestStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.test-result {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 2fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.test-result:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.test-provider {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.test-latency {
|
||||
color: var(--text-secondary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.test-time {
|
||||
color: var(--text-light);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
async testProvider(id) {
|
||||
window.authManager.showToast(`Testing connection to ${id}...`, 'info');
|
||||
try {
|
||||
await window.api.post(`/providers/${id}/test`, {});
|
||||
window.authManager.showToast(`${id} connection successful!`, 'success');
|
||||
this.loadProviders();
|
||||
} catch (error) {
|
||||
window.authManager.showToast(`${id} test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
configureProvider(id) {
|
||||
window.authManager.showToast('Provider configuration via UI not yet implemented', 'info');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Test all providers button
|
||||
const testAllBtn = document.getElementById('test-all-providers');
|
||||
if (testAllBtn) {
|
||||
testAllBtn.addEventListener('click', () => {
|
||||
this.testAllProviders();
|
||||
});
|
||||
testAllBtn.onclick = () => {
|
||||
this.providers.forEach(p => this.testProvider(p.id));
|
||||
};
|
||||
}
|
||||
|
||||
// Toggle switches
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.matches('.toggle-switch input')) {
|
||||
const provider = e.target.dataset.provider;
|
||||
const enabled = e.target.checked;
|
||||
this.toggleProvider(provider, enabled);
|
||||
}
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-action')) {
|
||||
const button = e.target.closest('.btn-action');
|
||||
const action = button.dataset.action;
|
||||
const provider = button.dataset.provider;
|
||||
|
||||
switch (action) {
|
||||
case 'configure':
|
||||
this.configureProvider(provider);
|
||||
break;
|
||||
case 'test':
|
||||
this.testProvider(provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy buttons
|
||||
if (e.target.closest('.btn-copy')) {
|
||||
const button = e.target.closest('.btn-copy');
|
||||
const text = button.dataset.text;
|
||||
this.copyToClipboard(text);
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Copied to clipboard', 'success');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleProvider(providerName, enabled) {
|
||||
const provider = this.providers.find(p => p.name === providerName);
|
||||
if (!provider) return;
|
||||
|
||||
// In a real app, this would update the provider via API
|
||||
provider.enabled = enabled;
|
||||
provider.status = enabled ? 'online' : 'offline';
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(
|
||||
`${providerName} ${enabled ? 'enabled' : 'disabled'}`,
|
||||
enabled ? 'success' : 'warning'
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh providers list
|
||||
this.loadProvidersList();
|
||||
}
|
||||
|
||||
configureProvider(providerName) {
|
||||
const provider = this.providers.find(p => p.name === providerName);
|
||||
if (!provider) return;
|
||||
|
||||
// Show configuration modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Configure ${providerName}</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="configure-provider-form">
|
||||
<div class="form-control">
|
||||
<label for="api-key">API Key</label>
|
||||
<input type="password" id="api-key" value="${provider.apiKey}" placeholder="Enter API key" required>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="base-url">Base URL (Optional)</label>
|
||||
<input type="text" id="base-url" placeholder="https://api.openai.com/v1">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="timeout">Timeout (seconds)</label>
|
||||
<input type="number" id="timeout" value="30" min="1" max="300">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="retry-count">Retry Count</label>
|
||||
<input type="number" id="retry-count" value="3" min="0" max="10">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary save-config">Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const saveBtn = modal.querySelector('.save-config');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// In a real app, this would save provider configuration
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`${providerName} configuration saved`, 'success');
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
testProvider(providerName) {
|
||||
const provider = this.providers.find(p => p.name === providerName);
|
||||
if (!provider) return;
|
||||
|
||||
// Show testing in progress
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Testing ${providerName} connection...`, 'info');
|
||||
}
|
||||
|
||||
// Simulate API test
|
||||
setTimeout(() => {
|
||||
// In a real app, this would test the provider connection via API
|
||||
const success = Math.random() > 0.3; // 70% success rate for demo
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(
|
||||
`${providerName} connection ${success ? 'successful' : 'failed'}`,
|
||||
success ? 'success' : 'error'
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh connection tests
|
||||
this.loadConnectionTests();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
testAllProviders() {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Testing all providers...', 'info');
|
||||
}
|
||||
|
||||
// Test each provider sequentially
|
||||
this.providers.forEach((provider, index) => {
|
||||
setTimeout(() => {
|
||||
this.testProvider(provider.name);
|
||||
}, index * 2000); // Stagger tests
|
||||
});
|
||||
}
|
||||
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadProviderStats();
|
||||
this.loadProvidersList();
|
||||
this.loadModelsList();
|
||||
this.loadConnectionTests();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize providers page when needed
|
||||
window.initProviders = async () => {
|
||||
window.providersPage = new ProvidersPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ProvidersPage;
|
||||
}
|
||||
Reference in New Issue
Block a user