/** * API Explorer Page */ 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; // Show loading state with spinner responseContent.innerHTML = `

Sending request...

`; responseStatus.textContent = 'Loading...'; responseStatus.className = 'status status-loading'; responseTime.textContent = ''; const startTime = performance.now(); // Disable send button during request 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() }); // Re-enable send button 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); // Re-enable send button 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 = '
No requests yet
'; return; } historyList.innerHTML = this.history.map(entry => `
${entry.method}
${entry.endpoint}
${entry.status}
${entry.duration}ms
`).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); } /** * Load and display available providers */ 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 = `
Failed to load providers: ${error.message}
`; if (countBadge) { countBadge.textContent = '0'; } } } /** * Render providers grid */ renderProviders(providers) { const grid = document.getElementById('providers-grid'); if (!grid) return; if (providers.length === 0) { grid.innerHTML = '
No providers available
'; 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 ? 'API Key' : 'No Auth'; // Build capabilities list const capabilities = provider.capabilities || []; const capabilitiesHtml = capabilities.length > 0 ? `
${capabilities.map(cap => `${this.escapeHtml(cap)}`).join('')}
` : ''; return `

${this.escapeHtml(provider.name)}

${this.escapeHtml(provider.category)}
${authBadge}
${provider.endpoint || provider.base_url ? `
${this.escapeHtml(provider.endpoint || provider.base_url)}
` : ''} ${capabilitiesHtml} ${provider.status ? `
${this.escapeHtml(provider.status)}
` : ''}
`; }).join(''); } /** * Get CSS class for provider status */ 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'; } /** * Escape HTML to prevent XSS */ escapeHtml(text) { if (typeof text !== 'string') return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } export default APIExplorerPage;