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