Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| /** | |
| * API Helper Utilities | |
| * Shared utilities for API requests across all pages | |
| */ | |
| export class APIHelper { | |
| /** | |
| * Get request headers with optional authorization | |
| * @returns {Object} Headers object | |
| */ | |
| static getHeaders() { | |
| const token = localStorage.getItem('HF_TOKEN'); | |
| const headers = { | |
| 'Content-Type': 'application/json' | |
| }; | |
| if (token && token.trim()) { | |
| // Check if token is expired | |
| if (this.isTokenExpired(token)) { | |
| console.warn('[APIHelper] Token expired, removing from storage'); | |
| localStorage.removeItem('HF_TOKEN'); | |
| } else { | |
| headers['Authorization'] = `Bearer ${token}`; | |
| } | |
| } | |
| return headers; | |
| } | |
| /** | |
| * Check if JWT token is expired | |
| * @param {string} token - JWT token | |
| * @returns {boolean} True if expired | |
| */ | |
| static isTokenExpired(token) { | |
| try { | |
| // Basic JWT expiration check | |
| const parts = token.split('.'); | |
| if (parts.length !== 3) return false; // Not a JWT | |
| const payload = JSON.parse(atob(parts[1])); | |
| if (!payload.exp) return false; // No expiration | |
| const now = Math.floor(Date.now() / 1000); | |
| return payload.exp < now; | |
| } catch (e) { | |
| console.warn('[APIHelper] Token validation error:', e); | |
| return false; | |
| } | |
| } | |
| /** | |
| * Fetch data from API with automatic error handling | |
| * @param {string} url - API endpoint | |
| * @param {Object} options - Fetch options | |
| * @returns {Promise<any>} Response data | |
| */ | |
| 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 fallback data instead of throwing | |
| return this._getFallbackData(url, error); | |
| } | |
| } | |
| /** | |
| * Get fallback data for failed API requests | |
| * @private | |
| */ | |
| static _getFallbackData(url, error) { | |
| // Return appropriate fallback based on URL | |
| 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 | |
| }; | |
| } | |
| // Generic fallback | |
| return { | |
| error: error.message, | |
| fallback: true, | |
| data: null | |
| }; | |
| } | |
| /** | |
| * Extract array from various response formats | |
| * @param {any} data - API response data | |
| * @param {string[]} keys - Possible keys containing array data | |
| * @returns {Array} Extracted array or empty array | |
| */ | |
| static extractArray(data, keys = ['data', 'items', 'results', 'list']) { | |
| // Direct array | |
| if (Array.isArray(data)) { | |
| return data; | |
| } | |
| // Check common keys | |
| for (const key of keys) { | |
| if (data && Array.isArray(data[key])) { | |
| return data[key]; | |
| } | |
| } | |
| // Object values | |
| 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 []; | |
| } | |
| /** | |
| * Check API health | |
| * @returns {Promise<Object>} Health status | |
| */ | |
| 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 | |
| }; | |
| } | |
| } | |
| /** | |
| * Setup periodic health monitoring | |
| * @param {Function} callback - Callback function with health status | |
| * @param {number} interval - Check interval in ms (default: 30000) | |
| * @returns {number} Interval ID | |
| */ | |
| static monitorHealth(callback, interval = 30000) { | |
| // Initial check | |
| this.checkHealth().then(callback); | |
| // Periodic checks | |
| return setInterval(async () => { | |
| if (!document.hidden) { | |
| const health = await this.checkHealth(); | |
| callback(health); | |
| } | |
| }, interval); | |
| } | |
| /** | |
| * Show toast notification | |
| * @param {string} message - Message to display | |
| * @param {string} type - Type: success, error, warning, info | |
| * @param {number} duration - Display duration in ms | |
| */ | |
| 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); | |
| } | |
| /** | |
| * Format number with locale | |
| * @param {number} num - Number to format | |
| * @param {Object} options - Intl.NumberFormat options | |
| * @returns {string} Formatted number | |
| */ | |
| static formatNumber(num, options = {}) { | |
| return new Intl.NumberFormat('en-US', options).format(num); | |
| } | |
| /** | |
| * Format currency | |
| * @param {number} amount - Amount to format | |
| * @param {string} currency - Currency code (default: USD) | |
| * @returns {string} Formatted currency | |
| */ | |
| static formatCurrency(amount, currency = 'USD') { | |
| return this.formatNumber(amount, { | |
| style: 'currency', | |
| currency: currency, | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 2 | |
| }); | |
| } | |
| /** | |
| * Format percentage | |
| * @param {number} value - Value to format | |
| * @param {number} decimals - Decimal places | |
| * @returns {string} Formatted percentage | |
| */ | |
| static formatPercentage(value, decimals = 2) { | |
| return `${value >= 0 ? '+' : ''}${value.toFixed(decimals)}%`; | |
| } | |
| /** | |
| * Debounce function | |
| * @param {Function} func - Function to debounce | |
| * @param {number} wait - Wait time in ms | |
| * @returns {Function} Debounced function | |
| */ | |
| static debounce(func, wait = 300) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| /** | |
| * Throttle function | |
| * @param {Function} func - Function to throttle | |
| * @param {number} limit - Time limit in ms | |
| * @returns {Function} Throttled function | |
| */ | |
| static throttle(func, limit = 300) { | |
| let inThrottle; | |
| return function executedFunction(...args) { | |
| if (!inThrottle) { | |
| func(...args); | |
| inThrottle = true; | |
| setTimeout(() => (inThrottle = false), limit); | |
| } | |
| }; | |
| } | |
| } | |
| export default APIHelper; | |