|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class APIHelper { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getHeaders() { |
|
|
const token = localStorage.getItem('HF_TOKEN'); |
|
|
const headers = { |
|
|
'Content-Type': 'application/json' |
|
|
}; |
|
|
|
|
|
if (token && token.trim()) { |
|
|
|
|
|
if (this.isTokenExpired(token)) { |
|
|
console.warn('[APIHelper] Token expired, removing from storage'); |
|
|
localStorage.removeItem('HF_TOKEN'); |
|
|
} else { |
|
|
headers['Authorization'] = `Bearer ${token}`; |
|
|
} |
|
|
} |
|
|
|
|
|
return headers; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static isTokenExpired(token) { |
|
|
try { |
|
|
|
|
|
const parts = token.split('.'); |
|
|
if (parts.length !== 3) return false; |
|
|
|
|
|
const payload = JSON.parse(atob(parts[1])); |
|
|
if (!payload.exp) return false; |
|
|
|
|
|
const now = Math.floor(Date.now() / 1000); |
|
|
return payload.exp < now; |
|
|
} catch (e) { |
|
|
console.warn('[APIHelper] Token validation error:', e); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async fetchAPI(url, options = {}) { |
|
|
const headers = this.getHeaders(); |
|
|
|
|
|
try { |
|
|
const response = await fetch(url, { |
|
|
...options, |
|
|
headers: { |
|
|
...headers, |
|
|
...options.headers |
|
|
} |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
const contentType = response.headers.get('content-type'); |
|
|
if (contentType && contentType.includes('application/json')) { |
|
|
return await response.json(); |
|
|
} |
|
|
|
|
|
return await response.text(); |
|
|
} catch (error) { |
|
|
console.error(`[APIHelper] Fetch error for ${url}:`, error); |
|
|
|
|
|
|
|
|
return this._getFallbackData(url, error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static _getFallbackData(url, error) { |
|
|
|
|
|
if (url.includes('/resources/summary') || url.includes('/resources')) { |
|
|
return { |
|
|
success: false, |
|
|
error: error.message, |
|
|
summary: { |
|
|
total_resources: 0, |
|
|
free_resources: 0, |
|
|
models_available: 0, |
|
|
total_api_keys: 0, |
|
|
categories: {} |
|
|
}, |
|
|
fallback: true |
|
|
}; |
|
|
} |
|
|
|
|
|
if (url.includes('/models/status')) { |
|
|
return { |
|
|
success: false, |
|
|
error: error.message, |
|
|
status: 'error', |
|
|
status_message: `Error: ${error.message}`, |
|
|
models_loaded: 0, |
|
|
models_failed: 0, |
|
|
hf_mode: 'unknown', |
|
|
transformers_available: false, |
|
|
fallback: true, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
|
|
|
if (url.includes('/models/summary') || url.includes('/models')) { |
|
|
return { |
|
|
ok: false, |
|
|
error: error.message, |
|
|
summary: { |
|
|
total_models: 0, |
|
|
loaded_models: 0, |
|
|
failed_models: 0, |
|
|
hf_mode: 'error', |
|
|
transformers_available: false |
|
|
}, |
|
|
categories: {}, |
|
|
health_registry: [], |
|
|
fallback: true, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
|
|
|
if (url.includes('/health') || url.includes('/status')) { |
|
|
return { |
|
|
status: 'offline', |
|
|
healthy: false, |
|
|
error: error.message, |
|
|
fallback: true |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
error: error.message, |
|
|
fallback: true, |
|
|
data: null |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static extractArray(data, keys = ['data', 'items', 'results', 'list']) { |
|
|
|
|
|
if (Array.isArray(data)) { |
|
|
return data; |
|
|
} |
|
|
|
|
|
|
|
|
for (const key of keys) { |
|
|
if (data && Array.isArray(data[key])) { |
|
|
return data[key]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (data && typeof data === 'object' && !Array.isArray(data)) { |
|
|
const values = Object.values(data); |
|
|
if (values.length > 0 && values.every(v => typeof v === 'object')) { |
|
|
return values; |
|
|
} |
|
|
} |
|
|
|
|
|
console.warn('[APIHelper] Could not extract array from:', data); |
|
|
return []; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async checkHealth() { |
|
|
try { |
|
|
const controller = new AbortController(); |
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000); |
|
|
|
|
|
const response = await fetch('/api/health', { |
|
|
signal: controller.signal, |
|
|
cache: 'no-cache' |
|
|
}); |
|
|
|
|
|
clearTimeout(timeoutId); |
|
|
|
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
return { |
|
|
status: 'online', |
|
|
healthy: true, |
|
|
data: data |
|
|
}; |
|
|
} else { |
|
|
return { |
|
|
status: 'degraded', |
|
|
healthy: false, |
|
|
httpStatus: response.status |
|
|
}; |
|
|
} |
|
|
} catch (error) { |
|
|
return { |
|
|
status: 'offline', |
|
|
healthy: false, |
|
|
error: error.message |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static monitorHealth(callback, interval = 30000) { |
|
|
|
|
|
this.checkHealth().then(callback); |
|
|
|
|
|
|
|
|
return setInterval(async () => { |
|
|
if (!document.hidden) { |
|
|
const health = await this.checkHealth(); |
|
|
callback(health); |
|
|
} |
|
|
}, interval); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static showToast(message, type = 'info', duration = 3000) { |
|
|
const colors = { |
|
|
success: '#22c55e', |
|
|
error: '#ef4444', |
|
|
warning: '#f59e0b', |
|
|
info: '#3b82f6' |
|
|
}; |
|
|
|
|
|
const toast = document.createElement('div'); |
|
|
toast.style.cssText = ` |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
padding: 12px 20px; |
|
|
border-radius: 8px; |
|
|
background: ${colors[type] || colors.info}; |
|
|
color: white; |
|
|
font-weight: 500; |
|
|
z-index: 9999; |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); |
|
|
animation: slideIn 0.3s ease; |
|
|
`; |
|
|
toast.textContent = message; |
|
|
|
|
|
document.body.appendChild(toast); |
|
|
setTimeout(() => { |
|
|
toast.style.animation = 'slideOut 0.3s ease'; |
|
|
setTimeout(() => toast.remove(), 300); |
|
|
}, duration); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static formatNumber(num, options = {}) { |
|
|
return new Intl.NumberFormat('en-US', options).format(num); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static formatCurrency(amount, currency = 'USD') { |
|
|
return this.formatNumber(amount, { |
|
|
style: 'currency', |
|
|
currency: currency, |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static formatPercentage(value, decimals = 2) { |
|
|
return `${value >= 0 ? '+' : ''}${value.toFixed(decimals)}%`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static debounce(func, wait = 300) { |
|
|
let timeout; |
|
|
return function executedFunction(...args) { |
|
|
const later = () => { |
|
|
clearTimeout(timeout); |
|
|
func(...args); |
|
|
}; |
|
|
clearTimeout(timeout); |
|
|
timeout = setTimeout(later, wait); |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static throttle(func, limit = 300) { |
|
|
let inThrottle; |
|
|
return function executedFunction(...args) { |
|
|
if (!inThrottle) { |
|
|
func(...args); |
|
|
inThrottle = true; |
|
|
setTimeout(() => (inThrottle = false), limit); |
|
|
} |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
export default APIHelper; |
|
|
|
|
|
|