/** * AI Models Page - Hugging Face Integration * Fixed version with proper error handling */ import { APIHelper } from '../../shared/js/utils/api-helper.js'; import { modelsClient } from '../../shared/js/core/models-client.js'; import { api } from '../../shared/js/core/api-client.js'; import logger from '../../shared/js/utils/logger.js'; class ModelsPage { constructor() { this.models = []; this.allModels = []; this.activeFilters = { category: 'all', status: 'all' }; this.refreshInterval = null; } async init() { try { console.log('[Models] Initializing...'); this.bindEvents(); await this.loadModels(); await this.loadHealth(); this.refreshInterval = setInterval(() => this.loadModels(), 60000); this.showToast('Models page ready', 'success'); } catch (error) { console.error('[Models] Init error:', error); this.showToast('Failed to load models', 'error'); } } createTimeoutSignal(ms = 10000) { // Prefer AbortSignal.timeout when available, fallback to AbortController. if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') { return AbortSignal.timeout(ms); } const controller = new AbortController(); setTimeout(() => controller.abort(), ms); return controller.signal; } bindEvents() { // Refresh button const refreshBtn = document.getElementById('refresh-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.loadModels(); }); } // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', (e) => { const tabId = e.currentTarget.dataset.tab; this.switchTab(tabId); }); }); // Test model button const runTestBtn = document.getElementById('run-test-btn'); if (runTestBtn) { runTestBtn.addEventListener('click', () => { this.runTest(); }); } // Clear test button const clearTestBtn = document.getElementById('clear-test-btn'); if (clearTestBtn) { clearTestBtn.addEventListener('click', () => { this.clearTest(); }); } // Example buttons document.querySelectorAll('.example-btn').forEach(btn => { btn.addEventListener('click', (e) => { const text = e.currentTarget.dataset.text; const testInput = document.getElementById('test-input'); if (testInput) { testInput.value = text; } }); }); // Re-initialize all button const reinitBtn = document.getElementById('reinit-all-btn'); if (reinitBtn) { reinitBtn.addEventListener('click', () => { this.reinitializeAll(); }); } // Filters const categoryFilter = document.getElementById('category-filter'); if (categoryFilter) { categoryFilter.addEventListener('change', (e) => { this.activeFilters.category = e.target.value || 'all'; this.applyFilters(); }); } const statusFilter = document.getElementById('status-filter'); if (statusFilter) { statusFilter.addEventListener('change', (e) => { this.activeFilters.status = e.target.value || 'all'; this.applyFilters(); }); } } switchTab(tabId) { // Remove active class from all tabs and contents document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Add active class to selected tab and content const selectedBtn = document.querySelector(`[data-tab="${tabId}"]`); const selectedContent = document.getElementById(`tab-${tabId}`); if (selectedBtn) { selectedBtn.classList.add('active'); } if (selectedContent) { selectedContent.classList.add('active'); } console.log(`[Models] Switched to tab: ${tabId}`); } async loadModels() { const container = document.getElementById('models-grid') || document.getElementById('models-container') || document.querySelector('.models-list'); // Show loading state if (container) { container.innerHTML = `

Loading AI models...

`; } try { logger.info('Models', 'Loading models data...'); let payload = null; let rawModels = []; // Strategy 1: Try /api/models/list endpoint try { logger.debug('Models', 'Attempting to load via /api/models/list...'); const response = await fetch('/api/models/list', { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: this.createTimeoutSignal(10000) }); if (response.ok) { payload = await response.json(); // Extract models array if (Array.isArray(payload.models)) { rawModels = payload.models; logger.info('Models', `Loaded ${rawModels.length} models via /api/models/list`); } } } catch (e) { logger.warn('Models', '/api/models/list failed:', e?.message || 'Unknown error'); } // Strategy 2: Try /api/models/status if first failed if (!payload || rawModels.length === 0) { try { logger.debug('Models', 'Attempting to load via /api/models/status...'); const response = await fetch('/api/models/status', { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: this.createTimeoutSignal(10000) }); if (response.ok) { const statusData = await response.json(); payload = statusData; // Try to get models from model_info if (statusData.model_info?.models) { rawModels = Object.values(statusData.model_info.models); logger.info('Models', `Loaded ${rawModels.length} models via /api/models/status`); } } } catch (e) { logger.warn('Models', '/api/models/status failed:', e?.message || 'Unknown error'); } } // Strategy 3: Try /api/models/summary endpoint if (!payload || rawModels.length === 0) { try { logger.debug('Models', 'Attempting to load via /api/models/summary...'); const response = await fetch('/api/models/summary', { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: this.createTimeoutSignal(10000) }); if (response.ok) { const summaryData = await response.json(); payload = summaryData; // Extract from categories if (summaryData.categories) { for (const [category, categoryModels] of Object.entries(summaryData.categories)) { if (Array.isArray(categoryModels)) { rawModels.push(...categoryModels); } } logger.info('Models', `Loaded ${rawModels.length} models via /api/models/summary`); } } } catch (e) { logger.warn('Models', '/api/models/summary failed:', e?.message || 'Unknown error'); } } // Process models if we got any data if (Array.isArray(rawModels) && rawModels.length > 0) { this.models = rawModels.map((m, idx) => { // تشخیص status با دقت بیشتر const isLoaded = m.loaded === true || m.status === 'ready' || m.status === 'healthy' || m.status === 'loaded'; const isFailed = m.failed === true || m.error || m.status === 'failed' || m.status === 'unavailable' || m.status === 'error'; return { key: m.key || m.id || m.model_id || `model_${idx}`, name: m.name || m.model_name || m.model_id?.split('/').pop() || 'AI Model', model_id: m.model_id || m.id || m.name || 'unknown/model', category: m.category || m.provider || 'Hugging Face', task: m.task || m.type || 'Sentiment Analysis', loaded: isLoaded, failed: isFailed, requires_auth: Boolean(m.requires_auth || m.authentication || m.needs_token), status: isLoaded ? 'loaded' : isFailed ? 'failed' : 'available', error_count: Number(m.error_count || m.errors || 0), description: m.description || m.desc || `${m.name || m.model_id || 'Model'} - ${m.task || 'AI Model'}`, // فیلدهای اضافی برای debug success_rate: m.success_rate || (isLoaded ? 100 : isFailed ? 0 : null), last_used: m.last_used || m.last_access || null }; }); logger.info('Models', `Successfully processed ${this.models.length} models`); logger.debug('Models', 'Sample model:', this.models[0]); } else { logger.warn('Models', 'No models found in any endpoint, using fallback data'); this.models = this.getFallbackModels(); } this.allModels = [...this.models]; this.applyFilters(false); this.renderCatalog(); // Update stats from payload or calculate from models const stats = { total_models: payload?.total || payload?.total_models || this.models.length, models_loaded: payload?.models_loaded || payload?.loaded_models || this.models.filter(m => m.loaded).length, models_failed: payload?.models_failed || payload?.failed_models || this.models.filter(m => m.failed).length, hf_mode: payload?.hf_mode || (payload ? 'API' : 'Fallback'), hf_status: payload ? 'Connected' : 'Using fallback data', transformers_available: payload?.transformers_available || false }; this.renderStats(stats); this.updateTimestamp(); // Populate test model select this.populateTestModelSelect(); } catch (error) { logger.error('Models', 'Load error:', error?.message || 'Unknown error'); // Show error message this.showToast(`Failed to load models: ${error?.message || 'Unknown error'}`, 'error'); // Fallback to demo data this.models = this.getFallbackModels(); this.allModels = [...this.models]; this.applyFilters(false); this.renderCatalog(); this.renderStats({ total_models: this.models.length, models_loaded: 0, models_failed: 0, hf_mode: 'Fallback', hf_status: 'API unavailable - using fallback data', transformers_available: false }); this.updateTimestamp(); } } populateTestModelSelect() { const testModelSelect = document.getElementById('test-model-select'); if (testModelSelect && this.models.length > 0) { // Allow testing any model key via backend (auto-fallback if unavailable) testModelSelect.innerHTML = ''; const sorted = [...this.models].sort((a, b) => (a.category || '').localeCompare(b.category || '') || (a.name || '').localeCompare(b.name || '')); sorted.forEach(model => { const option = document.createElement('option'); option.value = model.key; option.textContent = `${model.name} (${model.category})`; testModelSelect.appendChild(option); }); } } applyFilters(shouldRerender = true) { const category = this.activeFilters.category; const status = this.activeFilters.status; const filtered = (this.allModels || []).filter((m) => { const catOk = category === 'all' ? true : (m.category === category || (m.category || '').toLowerCase() === category.toLowerCase()); const statusOk = status === 'all' ? true : (m.status === status || (status === 'available' && !m.loaded && !m.failed)); return catOk && statusOk; }); this.models = filtered; if (shouldRerender) { this.renderModels(); } else { // For initial load path we still need to render once. this.renderModels(); } } /** * Extract models array from various payload structures */ extractModelsArray(payload) { if (!payload) return []; // Try different paths const paths = [ payload.models, payload.model_info, payload.data, payload.categories ? Object.values(payload.categories).flat() : null ]; for (const path of paths) { if (Array.isArray(path) && path.length > 0) { return path; } } return []; } getFallbackModels() { return [ { key: 'sentiment_model', name: 'Sentiment Analysis', model_id: 'cardiffnlp/twitter-roberta-base-sentiment-latest', category: 'Hugging Face', task: 'Text Classification', loaded: false, failed: false, requires_auth: false, status: 'unknown', description: 'Advanced sentiment analysis for crypto market text. (Fallback - API unavailable)' }, { key: 'market_analysis', name: 'Market Analysis', model_id: 'internal/coingecko-api', category: 'Market Data', task: 'Price Analysis', loaded: false, failed: false, requires_auth: false, status: 'unknown', description: 'Real-time market data analysis using CoinGecko API. (Fallback - API unavailable)' } ]; } renderStats(data) { try { const stats = { 'total-models': data.total_models ?? this.models.length, 'active-models': data.models_loaded ?? this.models.filter(m => m.loaded).length, 'failed-models': data.models_failed ?? this.models.filter(m => m.failed).length, 'hf-mode': data.hf_mode ?? 'unknown', 'hf-status': data.hf_status }; for (const [id, value] of Object.entries(stats)) { const el = document.getElementById(id); if (el && value !== undefined) { el.textContent = value; } } } catch (err) { console.warn('[Models] renderStats skipped:', err?.message || 'Unknown error'); } } renderModels() { const container = document.getElementById('models-grid') || document.getElementById('models-list'); if (!container) { console.warn('[Models] Container not found'); return; } if (!this.models || this.models.length === 0) { container.innerHTML = `
🤖

No models loaded

Models will be loaded on demand when needed for AI features.

`; return; } container.innerHTML = this.models.map(model => { const statusClass = model.loaded ? 'loaded' : model.failed ? 'failed' : 'available'; const statusText = model.loaded ? 'Loaded' : model.failed ? 'Failed' : 'Available'; const statusBadgeClass = model.loaded ? 'loaded' : model.failed ? 'failed' : 'available'; return `

${model.name}

${model.category}

${statusText}
${model.model_id}
${model.task} ${model.requires_auth ? '🔒 Auth Required' : '🔓 Public'} ${model.error_count > 0 ? `⚠️ ${model.error_count} errors` : ''}
`; }).join(''); } reinitModel(modelKey) { this.showToast(`Reinitializing model: ${modelKey}...`, 'info'); // TODO: Implement model reinitialization setTimeout(() => { this.showToast('Model reinitialization not yet implemented', 'warning'); }, 1000); } viewModelDetails(modelKey) { const model = this.models.find(m => m.key === modelKey); if (!model) return; this.showToast(`Model: ${model.name} - ${model.model_id}`, 'info'); } async testModel(modelId) { this.showToast('Testing model...', 'info'); try { const response = await fetch('/api/sentiment/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Bitcoin is going to the moon! 🚀', mode: 'crypto', model_key: modelId, use_ensemble: false }), signal: this.createTimeoutSignal(10000) }); if (response.ok) { const result = await response.json(); if (result && result.sentiment) { this.showToast( `Test successful: ${result.sentiment} (${(result.score * 100).toFixed(0)}%)`, 'success' ); } else { this.showToast('Test completed but no sentiment data returned', 'warning'); } } else { this.showToast('Test failed: API error', 'error'); } } catch (error) { console.error('[Models] Test failed:', error); this.showToast(`Test failed: ${error?.message || 'Unknown error'}`, 'error'); } } updateTimestamp() { const el = document.getElementById('last-update'); if (el) { el.textContent = `Updated: ${new Date().toLocaleTimeString()}`; } } async runTest() { const input = document.getElementById('test-input'); const resultDiv = document.getElementById('test-result'); const modelSelect = document.getElementById('test-model-select'); if (!input || !input.value.trim()) { this.showToast('Please enter text to analyze', 'warning'); return; } const text = input.value.trim(); const modelKey = modelSelect?.value || ''; this.showToast('Analyzing...', 'info'); try { const response = await fetch('/api/sentiment/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, mode: 'crypto', model_key: modelKey || undefined, use_ensemble: !modelKey }), signal: this.createTimeoutSignal(10000) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); // Show result if (resultDiv) { resultDiv.classList.remove('hidden'); } // Update sentiment display const emoji = this.getSentimentEmoji(result.sentiment); const emojiEl = document.getElementById('sentiment-emoji'); const labelEl = document.getElementById('sentiment-label'); const confidenceEl = document.getElementById('sentiment-confidence'); const displayEl = document.getElementById('sentiment-display'); const timeEl = document.getElementById('result-time'); const jsonPre = document.querySelector('.result-json'); if (emojiEl) emojiEl.textContent = emoji; const sentimentKey = (result.sentiment || 'unknown').toString().toLowerCase(); if (displayEl) { displayEl.setAttribute('data-sentiment', sentimentKey); const pct = (typeof result.score === 'number' ? result.score : 0) * 100; displayEl.style.setProperty('--confidence', `${Math.max(0, Math.min(100, pct)).toFixed(1)}%`); } if (labelEl) { labelEl.textContent = result.sentiment || 'Unknown'; // Ensure CSS sentiment variants can apply reliably labelEl.classList.remove('bullish', 'bearish', 'neutral', 'positive', 'negative', 'buy', 'sell', 'hold', 'unknown'); labelEl.classList.add(sentimentKey); } if (confidenceEl) { const pct = (typeof result.score === 'number' ? result.score : 0) * 100; confidenceEl.textContent = `Confidence: ${Math.max(0, Math.min(100, pct)).toFixed(1)}%`; } if (timeEl) timeEl.textContent = new Date().toLocaleTimeString(); if (jsonPre) jsonPre.textContent = JSON.stringify(result, null, 2); this.showToast('Analysis complete!', 'success'); } catch (error) { console.error('[Models] Test error:', error); this.showToast(`Analysis failed: ${error?.message || 'Unknown error'}`, 'error'); } } async loadHealth() { const container = document.getElementById('health-grid'); if (!container) return; container.innerHTML = `

Loading health data...

`; try { const res = await fetch('/api/models/health', { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: this.createTimeoutSignal(10000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const health = Array.isArray(data.health) ? data.health : (data.health ? Object.values(data.health) : []); if (!health.length) { container.innerHTML = `
🏥

No health data

Health registry is empty (models may be running in fallback mode).

`; return; } container.innerHTML = health.map((h) => { const status = h.status || 'unknown'; const statusClass = status === 'healthy' ? 'loaded' : status === 'unavailable' ? 'failed' : 'available'; const name = h.name || h.key || 'model'; return `

${name}

Health: ${status}

${status}
✅ ${Number(h.success_count || 0)} success ⚠️ ${Number(h.error_count || 0)} errors
`; }).join(''); } catch (e) { container.innerHTML = `
⚠️

Health data unavailable

${e?.message || 'Unable to fetch /api/models/health'}

`; } } renderCatalog() { // Best-effort catalog fill; only runs if the catalog containers exist on this page. const buckets = { crypto: document.getElementById('catalog-crypto'), financial: document.getElementById('catalog-financial'), social: document.getElementById('catalog-social'), trading: document.getElementById('catalog-trading'), generation: document.getElementById('catalog-generation'), summarization: document.getElementById('catalog-summarization') }; const hasAny = Object.values(buckets).some(Boolean); if (!hasAny) return; const byBucket = { crypto: [], financial: [], social: [], trading: [], generation: [], summarization: [] }; (this.allModels || []).forEach((m) => { const cat = (m.category || '').toLowerCase(); if (cat.includes('crypto')) byBucket.crypto.push(m); else if (cat.includes('financial')) byBucket.financial.push(m); else if (cat.includes('social')) byBucket.social.push(m); else if (cat.includes('trading')) byBucket.trading.push(m); else if (cat.includes('generation') || cat.includes('gen')) byBucket.generation.push(m); else if (cat.includes('summar')) byBucket.summarization.push(m); }); const renderList = (list) => list.map((m) => `
${m.name}
${m.model_id}
`).join('') || '

No models in this category.

'; if (buckets.crypto) buckets.crypto.innerHTML = renderList(byBucket.crypto); if (buckets.financial) buckets.financial.innerHTML = renderList(byBucket.financial); if (buckets.social) buckets.social.innerHTML = renderList(byBucket.social); if (buckets.trading) buckets.trading.innerHTML = renderList(byBucket.trading); if (buckets.generation) buckets.generation.innerHTML = renderList(byBucket.generation); if (buckets.summarization) buckets.summarization.innerHTML = renderList(byBucket.summarization); } getSentimentEmoji(sentiment) { const emojiMap = { 'positive': '😊', 'bullish': '📈', 'negative': '😟', 'bearish': '📉', 'neutral': '😐', 'buy': '🟢', 'sell': '🔴', 'hold': '🟡' }; return emojiMap[sentiment?.toLowerCase()] || '📊'; } clearTest() { const input = document.getElementById('test-input'); const resultDiv = document.getElementById('test-result'); if (input) { input.value = ''; } if (resultDiv) { resultDiv.classList.add('hidden'); } } async reinitializeAll() { this.showToast('Re-initializing all models...', 'info'); try { const response = await fetch('/api/models/reinitialize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: this.createTimeoutSignal(30000) }); if (response.ok) { this.showToast('Models re-initialized successfully!', 'success'); await this.loadModels(); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error('[Models] Re-initialize error:', error); this.showToast(`Re-initialization failed: ${error?.message || 'Unknown error'}`, 'error'); } } showToast(message, type = 'info') { if (typeof APIHelper !== 'undefined' && APIHelper.showToast) { APIHelper.showToast(message, type); } else { console.log(`[Toast ${type}]`, message); } } } // Initialize const modelsPage = new ModelsPage(); modelsPage.init(); // Expose globally for onclick handlers window.modelsPage = modelsPage;