| |
| |
| |
|
|
| class APIExplorerPage { |
| constructor() { |
| this.currentMethod = 'GET'; |
| this.history = []; |
| } |
|
|
| async init() { |
| try { |
| console.log('[APIExplorer] Initializing...'); |
| this.bindEvents(); |
| this.loadHistory(); |
| await this.loadProviders(); |
| console.log('[APIExplorer] Ready'); |
| } catch (error) { |
| console.error('[APIExplorer] Init error:', error); |
| } |
| } |
|
|
| bindEvents() { |
| const sendBtn = document.getElementById('send-btn'); |
| const methodSelect = document.getElementById('method-select'); |
| const endpointSelect = document.getElementById('endpoint-select'); |
| const bodyGroup = document.getElementById('body-group'); |
| const copyBtn = document.getElementById('copy-btn'); |
| const clearBtn = document.getElementById('clear-btn'); |
| const clearHistoryBtn = document.getElementById('clear-history-btn'); |
|
|
| if (sendBtn) { |
| sendBtn.addEventListener('click', () => this.sendRequest()); |
| } |
|
|
| if (methodSelect) { |
| methodSelect.addEventListener('change', (e) => { |
| this.currentMethod = e.target.value; |
| this.toggleBodyField(); |
| }); |
| } |
|
|
| if (endpointSelect) { |
| endpointSelect.addEventListener('change', (e) => { |
| const selectedOption = e.target.selectedOptions[0]; |
| const dataMethod = selectedOption.getAttribute('data-method'); |
| if (dataMethod) { |
| this.currentMethod = dataMethod; |
| methodSelect.value = dataMethod; |
| this.toggleBodyField(); |
| } |
| }); |
| } |
|
|
| if (copyBtn) { |
| copyBtn.addEventListener('click', () => this.copyResponse()); |
| } |
|
|
| if (clearBtn) { |
| clearBtn.addEventListener('click', () => this.clearResponse()); |
| } |
|
|
| if (clearHistoryBtn) { |
| clearHistoryBtn.addEventListener('click', () => this.clearHistory()); |
| } |
|
|
| this.toggleBodyField(); |
| } |
|
|
| toggleBodyField() { |
| const bodyGroup = document.getElementById('body-group'); |
| if (bodyGroup) { |
| bodyGroup.style.display = (this.currentMethod === 'POST' || this.currentMethod === 'PUT') ? 'block' : 'none'; |
| } |
| } |
|
|
| async sendRequest() { |
| const endpointSelect = document.getElementById('endpoint-select'); |
| const bodyInput = document.getElementById('request-body'); |
| const responseContent = document.getElementById('response-content'); |
| const responseStatus = document.getElementById('response-status'); |
| const responseTime = document.getElementById('response-time'); |
|
|
| if (!endpointSelect || !responseContent) return; |
|
|
| const endpoint = endpointSelect.value; |
| if (!endpoint) { |
| responseContent.textContent = JSON.stringify({ error: 'Please select an endpoint' }, null, 2); |
| return; |
| } |
|
|
| const url = window.location.origin + endpoint; |
| |
| |
| responseContent.innerHTML = ` |
| <div style="text-align: center; padding: 2rem;"> |
| <div class="spinner" style="display: inline-block; width: 32px; height: 32px; border: 3px solid rgba(255,255,255,0.1); border-top: 3px solid var(--color-primary, #3b82f6); border-radius: 50%; animation: spin 1s linear infinite;"></div> |
| <p style="margin-top: 1rem; color: var(--text-muted, #6b7280);">Sending request...</p> |
| </div> |
| `; |
| responseStatus.textContent = 'Loading...'; |
| responseStatus.className = 'status status-loading'; |
| responseTime.textContent = ''; |
|
|
| const startTime = performance.now(); |
| |
| |
| const sendBtn = document.getElementById('send-btn'); |
| const originalBtnText = sendBtn?.textContent; |
| if (sendBtn) { |
| sendBtn.disabled = true; |
| sendBtn.textContent = 'Sending...'; |
| } |
|
|
| try { |
| const options = { |
| method: this.currentMethod, |
| headers: {} |
| }; |
|
|
| if ((this.currentMethod === 'POST' || this.currentMethod === 'PUT') && bodyInput && bodyInput.value.trim()) { |
| try { |
| JSON.parse(bodyInput.value); |
| options.body = bodyInput.value; |
| options.headers['Content-Type'] = 'application/json'; |
| } catch (e) { |
| responseContent.textContent = JSON.stringify({ error: 'Invalid JSON in request body' }, null, 2); |
| responseStatus.textContent = 'Error'; |
| responseStatus.className = 'status status-error'; |
| return; |
| } |
| } |
|
|
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 30000); |
|
|
| const response = await fetch(url, { |
| ...options, |
| signal: controller.signal |
| }); |
| clearTimeout(timeoutId); |
|
|
| const endTime = performance.now(); |
| const duration = Math.round(endTime - startTime); |
|
|
| responseTime.textContent = `${duration}ms`; |
| responseStatus.textContent = `${response.status} ${response.statusText}`; |
| responseStatus.className = `status ${response.ok ? 'status-success' : 'status-error'}`; |
|
|
| const contentType = response.headers.get('content-type'); |
| let data; |
| |
| if (contentType && contentType.includes('application/json')) { |
| data = await response.json(); |
| responseContent.textContent = JSON.stringify(data, null, 2); |
| } else { |
| const text = await response.text(); |
| responseContent.textContent = text; |
| } |
|
|
| this.addToHistory({ |
| method: this.currentMethod, |
| endpoint, |
| status: response.status, |
| duration, |
| timestamp: new Date().toISOString() |
| }); |
| |
| |
| if (sendBtn) { |
| sendBtn.disabled = false; |
| sendBtn.textContent = originalBtnText; |
| } |
| } catch (error) { |
| const endTime = performance.now(); |
| const duration = Math.round(endTime - startTime); |
| |
| responseTime.textContent = `${duration}ms`; |
| responseStatus.textContent = 'Error'; |
| responseStatus.className = 'status status-error'; |
|
|
| let errorMessage; |
| if (error.name === 'AbortError') { |
| errorMessage = { |
| error: 'Request timeout (30s)', |
| suggestion: 'The request took too long. Try a different endpoint or check your connection.' |
| }; |
| } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { |
| errorMessage = { |
| error: 'Network error', |
| message: error.message, |
| suggestion: 'Check your internet connection and CORS settings. The endpoint might not be accessible.' |
| }; |
| } else { |
| errorMessage = { |
| error: error.message, |
| suggestion: 'This might be due to CORS policy, network issues, or an invalid endpoint.' |
| }; |
| } |
| |
| responseContent.textContent = JSON.stringify(errorMessage, null, 2); |
| |
| |
| if (sendBtn) { |
| sendBtn.disabled = false; |
| sendBtn.textContent = originalBtnText; |
| } |
| } |
| } |
|
|
| copyResponse() { |
| const responseContent = document.getElementById('response-content'); |
| if (responseContent) { |
| navigator.clipboard.writeText(responseContent.textContent) |
| .then(() => this.showToast('Response copied to clipboard')) |
| .catch(() => this.showToast('Failed to copy', 'error')); |
| } |
| } |
|
|
| clearResponse() { |
| const responseContent = document.getElementById('response-content'); |
| const responseStatus = document.getElementById('response-status'); |
| const responseTime = document.getElementById('response-time'); |
|
|
| if (responseContent) { |
| responseContent.textContent = JSON.stringify({ message: 'Select an endpoint and click \'Send Request\'' }, null, 2); |
| } |
| if (responseStatus) { |
| responseStatus.textContent = '--'; |
| responseStatus.className = 'status'; |
| } |
| if (responseTime) { |
| responseTime.textContent = '--'; |
| } |
| } |
|
|
| addToHistory(entry) { |
| this.history.unshift(entry); |
| if (this.history.length > 10) { |
| this.history.pop(); |
| } |
| this.saveHistory(); |
| this.renderHistory(); |
| } |
|
|
| saveHistory() { |
| try { |
| localStorage.setItem('api-explorer-history', JSON.stringify(this.history)); |
| } catch (e) { |
| console.error('Failed to save history:', e); |
| } |
| } |
|
|
| loadHistory() { |
| try { |
| const saved = localStorage.getItem('api-explorer-history'); |
| if (saved) { |
| this.history = JSON.parse(saved); |
| this.renderHistory(); |
| } |
| } catch (e) { |
| console.error('Failed to load history:', e); |
| } |
| } |
|
|
| renderHistory() { |
| const historyList = document.getElementById('history-list'); |
| if (!historyList) return; |
|
|
| if (this.history.length === 0) { |
| historyList.innerHTML = '<div class="empty-state">No requests yet</div>'; |
| return; |
| } |
|
|
| historyList.innerHTML = this.history.map(entry => ` |
| <div class="history-item"> |
| <div class="history-method method-${entry.method.toLowerCase()}">${entry.method}</div> |
| <div class="history-endpoint">${entry.endpoint}</div> |
| <div class="history-status status-${entry.status < 400 ? 'success' : 'error'}">${entry.status}</div> |
| <div class="history-time">${entry.duration}ms</div> |
| </div> |
| `).join(''); |
| } |
|
|
| clearHistory() { |
| this.history = []; |
| this.saveHistory(); |
| this.renderHistory(); |
| this.showToast('History cleared'); |
| } |
|
|
| showToast(message, type = 'success') { |
| const container = document.getElementById('toast-container'); |
| if (!container) return; |
|
|
| const toast = document.createElement('div'); |
| toast.className = `toast toast-${type}`; |
| toast.textContent = message; |
| container.appendChild(toast); |
|
|
| setTimeout(() => { |
| toast.classList.add('show'); |
| }, 10); |
|
|
| setTimeout(() => { |
| toast.classList.remove('show'); |
| setTimeout(() => toast.remove(), 300); |
| }, 3000); |
| } |
|
|
| |
| |
| |
| async loadProviders() { |
| const grid = document.getElementById('providers-grid'); |
| const countBadge = document.getElementById('providers-count'); |
| |
| if (!grid) return; |
|
|
| try { |
| const response = await fetch(`${window.location.origin}/api/providers`); |
| const data = await response.json(); |
|
|
| if (!response.ok || !data.success) { |
| throw new Error(data.error || 'Failed to load providers'); |
| } |
|
|
| const providers = data.providers || []; |
| |
| if (countBadge) { |
| countBadge.textContent = data.total || providers.length; |
| } |
|
|
| this.renderProviders(providers); |
| } catch (error) { |
| console.error('[APIExplorer] Error loading providers:', error); |
| grid.innerHTML = `<div class="empty-state error">Failed to load providers: ${error.message}</div>`; |
| if (countBadge) { |
| countBadge.textContent = '0'; |
| } |
| } |
| } |
|
|
| |
| |
| |
| renderProviders(providers) { |
| const grid = document.getElementById('providers-grid'); |
| if (!grid) return; |
|
|
| if (providers.length === 0) { |
| grid.innerHTML = '<div class="empty-state">No providers available</div>'; |
| return; |
| } |
|
|
| grid.innerHTML = providers.map(provider => { |
| const statusClass = this.getProviderStatusClass(provider.status); |
| const hasApiKey = provider.has_api_key || provider.has_api_token; |
| const authBadge = hasApiKey |
| ? '<span class="badge badge-warning">API Key</span>' |
| : '<span class="badge badge-success">No Auth</span>'; |
|
|
| |
| const capabilities = provider.capabilities || []; |
| const capabilitiesHtml = capabilities.length > 0 |
| ? `<div class="provider-capabilities"> |
| ${capabilities.map(cap => `<span class="capability-tag">${this.escapeHtml(cap)}</span>`).join('')} |
| </div>` |
| : ''; |
|
|
| return ` |
| <div class="provider-card"> |
| <div class="provider-header"> |
| <div class="provider-info"> |
| <h4 class="provider-name">${this.escapeHtml(provider.name)}</h4> |
| <span class="badge badge-category">${this.escapeHtml(provider.category)}</span> |
| </div> |
| <div class="provider-badges"> |
| ${authBadge} |
| </div> |
| </div> |
| <div class="provider-body"> |
| ${provider.endpoint || provider.base_url ? `<div class="provider-url">${this.escapeHtml(provider.endpoint || provider.base_url)}</div>` : ''} |
| ${capabilitiesHtml} |
| ${provider.status ? `<div class="provider-status ${statusClass}">${this.escapeHtml(provider.status)}</div>` : ''} |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
|
|
| |
| |
| |
| getProviderStatusClass(status) { |
| if (!status) return 'status-unknown'; |
| const statusLower = status.toLowerCase(); |
| if (statusLower.includes('valid') || statusLower === 'available' || statusLower === 'online') { |
| return 'status-success'; |
| } |
| if (statusLower.includes('invalid') || statusLower === 'offline') { |
| return 'status-error'; |
| } |
| if (statusLower.includes('conditional') || statusLower === 'degraded') { |
| return 'status-warning'; |
| } |
| return 'status-unknown'; |
| } |
|
|
| |
| |
| |
| escapeHtml(text) { |
| if (typeof text !== 'string') return ''; |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| } |
|
|
| export default APIExplorerPage; |
|
|