| | |
| | |
| | |
| | |
| |
|
| | 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) { |
| | |
| | if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') { |
| | return AbortSignal.timeout(ms); |
| | } |
| | const controller = new AbortController(); |
| | setTimeout(() => controller.abort(), ms); |
| | return controller.signal; |
| | } |
| |
|
| | bindEvents() { |
| | |
| | const refreshBtn = document.getElementById('refresh-btn'); |
| | if (refreshBtn) { |
| | refreshBtn.addEventListener('click', () => { |
| | this.loadModels(); |
| | }); |
| | } |
| |
|
| | |
| | document.querySelectorAll('.tab-btn').forEach(btn => { |
| | btn.addEventListener('click', (e) => { |
| | const tabId = e.currentTarget.dataset.tab; |
| | this.switchTab(tabId); |
| | }); |
| | }); |
| |
|
| | |
| | const runTestBtn = document.getElementById('run-test-btn'); |
| | if (runTestBtn) { |
| | runTestBtn.addEventListener('click', () => { |
| | this.runTest(); |
| | }); |
| | } |
| |
|
| | |
| | const clearTestBtn = document.getElementById('clear-test-btn'); |
| | if (clearTestBtn) { |
| | clearTestBtn.addEventListener('click', () => { |
| | this.clearTest(); |
| | }); |
| | } |
| |
|
| | |
| | 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; |
| | } |
| | }); |
| | }); |
| |
|
| | |
| | const reinitBtn = document.getElementById('reinit-all-btn'); |
| | if (reinitBtn) { |
| | reinitBtn.addEventListener('click', () => { |
| | this.reinitializeAll(); |
| | }); |
| | } |
| |
|
| | |
| | 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) { |
| | |
| | document.querySelectorAll('.tab-btn').forEach(btn => { |
| | btn.classList.remove('active'); |
| | }); |
| | document.querySelectorAll('.tab-content').forEach(content => { |
| | content.classList.remove('active'); |
| | }); |
| |
|
| | |
| | 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'); |
| | |
| | |
| | if (container) { |
| | container.innerHTML = ` |
| | <div class="loading-state"> |
| | <div class="loading-spinner"></div> |
| | <p class="loading-text">Loading AI models...</p> |
| | </div> |
| | `; |
| | } |
| | |
| | try { |
| | logger.info('Models', 'Loading models data...'); |
| | let payload = null; |
| | let rawModels = []; |
| | |
| | |
| | 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(); |
| | |
| | |
| | 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'); |
| | } |
| |
|
| | |
| | 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; |
| | |
| | |
| | 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'); |
| | } |
| | } |
| |
|
| | |
| | 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; |
| | |
| | |
| | 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'); |
| | } |
| | } |
| |
|
| | |
| | if (Array.isArray(rawModels) && rawModels.length > 0) { |
| | this.models = rawModels.map((m, idx) => { |
| | |
| | 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'}`, |
| | |
| | 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(); |
| |
|
| | |
| | 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(); |
| | |
| | |
| | this.populateTestModelSelect(); |
| | |
| | } catch (error) { |
| | logger.error('Models', 'Load error:', error?.message || 'Unknown error'); |
| | |
| | |
| | this.showToast(`Failed to load models: ${error?.message || 'Unknown error'}`, 'error'); |
| | |
| | |
| | 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) { |
| | |
| | testModelSelect.innerHTML = '<option value="">Auto (best available)</option>'; |
| | |
| | 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 { |
| | |
| | this.renderModels(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | extractModelsArray(payload) { |
| | if (!payload) return []; |
| | |
| | |
| | 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 = ` |
| | <div class="empty-state glass-card" style="grid-column: 1 / -1;"> |
| | <div class="empty-icon">🤖</div> |
| | <h3>No models loaded</h3> |
| | <p>Models will be loaded on demand when needed for AI features.</p> |
| | <button class="btn-gradient" onclick="window.modelsPage?.loadModels()"> |
| | <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg> |
| | Retry |
| | </button> |
| | </div> |
| | `; |
| | 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 ` |
| | <div class="model-card ${statusClass}"> |
| | <div class="model-header"> |
| | <div class="model-icon"> |
| | <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 4.44-2z"></path><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-2z"></path></svg> |
| | </div> |
| | <div class="model-info"> |
| | <h3 class="model-name">${model.name}</h3> |
| | <p class="model-type">${model.category}</p> |
| | </div> |
| | <div class="model-status ${statusBadgeClass}"> |
| | ${statusText} |
| | </div> |
| | </div> |
| | |
| | <div class="model-body"> |
| | <div class="model-id">${model.model_id}</div> |
| | |
| | <div class="model-meta"> |
| | <span class="meta-badge"> |
| | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M2 12h20"></path></svg> |
| | ${model.task} |
| | </span> |
| | <span class="meta-badge"> |
| | ${model.requires_auth ? '🔒 Auth Required' : '🔓 Public'} |
| | </span> |
| | ${model.error_count > 0 ? `<span class="meta-badge">⚠️ ${model.error_count} errors</span>` : ''} |
| | </div> |
| | </div> |
| | |
| | <div class="model-footer"> |
| | <button class="btn" onclick="window.modelsPage?.testModel('${model.key}')" ${!model.loaded ? 'disabled' : ''} title="${model.loaded ? 'Test this model' : 'Model not loaded'}"> |
| | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> |
| | Test |
| | </button> |
| | <button class="btn" onclick="window.modelsPage?.viewModelDetails('${model.key}')" title="View model details"> |
| | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg> |
| | Info |
| | </button> |
| | ${model.failed ? `<button class="btn reinit" onclick="window.modelsPage?.reinitModel('${model.key}')" title="Retry loading this model"> |
| | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><path d="M21 14a9 9 0 0 1-18 0 9 9 0 0 1 18 0"></path></svg> |
| | Retry |
| | </button>` : ''} |
| | </div> |
| | </div> |
| | `; |
| | }).join(''); |
| | } |
| | |
| | reinitModel(modelKey) { |
| | this.showToast(`Reinitializing model: ${modelKey}...`, 'info'); |
| | |
| | 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(); |
| |
|
| | |
| | if (resultDiv) { |
| | resultDiv.classList.remove('hidden'); |
| | } |
| |
|
| | |
| | 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'; |
| | |
| | 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 = ` |
| | <div class="loading-state"> |
| | <div class="loading-spinner"></div> |
| | <p class="loading-text">Loading health data...</p> |
| | </div> |
| | `; |
| |
|
| | 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 = ` |
| | <div class="empty-state glass-card" style="grid-column: 1 / -1;"> |
| | <div class="empty-icon">🏥</div> |
| | <h3>No health data</h3> |
| | <p>Health registry is empty (models may be running in fallback mode).</p> |
| | </div> |
| | `; |
| | 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 ` |
| | <div class="model-card ${statusClass}"> |
| | <div class="model-header"> |
| | <div class="model-info"> |
| | <h3 class="model-name">${name}</h3> |
| | <p class="model-type">Health: ${status}</p> |
| | </div> |
| | <div class="model-status ${statusClass}">${status}</div> |
| | </div> |
| | <div class="model-body"> |
| | <div class="model-meta"> |
| | <span class="meta-badge">✅ ${Number(h.success_count || 0)} success</span> |
| | <span class="meta-badge">⚠️ ${Number(h.error_count || 0)} errors</span> |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | }).join(''); |
| | } catch (e) { |
| | container.innerHTML = ` |
| | <div class="empty-state glass-card" style="grid-column: 1 / -1;"> |
| | <div class="empty-icon">⚠️</div> |
| | <h3>Health data unavailable</h3> |
| | <p>${e?.message || 'Unable to fetch /api/models/health'}</p> |
| | </div> |
| | `; |
| | } |
| | } |
| |
|
| | renderCatalog() { |
| | |
| | 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) => ` |
| | <div class="catalog-model-item"> |
| | <div class="catalog-model-name">${m.name}</div> |
| | <div class="catalog-model-id">${m.model_id}</div> |
| | </div> |
| | `).join('') || '<div class="empty-state"><p>No models in this category.</p></div>'; |
| |
|
| | 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); |
| | } |
| | } |
| | } |
| |
|
| | |
| | const modelsPage = new ModelsPage(); |
| | modelsPage.init(); |
| |
|
| | |
| | window.modelsPage = modelsPage; |
| |
|