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