|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|