// Statistics page functionality import { appCore } from './core.js'; import { showToast } from './utils/uiHelpers.js'; // Chart.js import (assuming it's available globally or via CDN) // If Chart.js isn't available, we'll need to add it to the project export class StatisticsManager { constructor() { this.charts = {}; this.data = {}; this.initialized = false; this.listStates = { lora: { offset: 0, limit: 50, sort: 'desc', isLoading: false, hasMore: true }, checkpoint: { offset: 0, limit: 50, sort: 'desc', isLoading: false, hasMore: true }, embedding: { offset: 0, limit: 50, sort: 'desc', isLoading: false, hasMore: true } }; } async initialize() { if (this.initialized) return; console.log('StatisticsManager: Initializing...'); // Initialize tab functionality this.initializeTabs(); // Load initial data await this.loadAllData(); // Initialize charts and visualizations await this.initializeVisualizations(); this.initialized = true; } initializeTabs() { const tabButtons = document.querySelectorAll('.tab-button'); const tabPanels = document.querySelectorAll('.tab-panel'); tabButtons.forEach(button => { button.addEventListener('click', () => { const tabId = button.dataset.tab; // Update active tab button tabButtons.forEach(btn => btn.classList.remove('active')); button.classList.add('active'); // Update active tab panel tabPanels.forEach(panel => panel.classList.remove('active')); const targetPanel = document.getElementById(`${tabId}-panel`); if (targetPanel) { targetPanel.classList.add('active'); // Refresh charts when tab becomes visible this.refreshChartsInPanel(tabId); } }); }); } async loadAllData() { try { // Load all statistics data in parallel const [ collectionOverview, usageAnalytics, baseModelDistribution, tagAnalytics, storageAnalytics, insights ] = await Promise.all([ this.fetchData('/api/lm/stats/collection-overview'), this.fetchData('/api/lm/stats/usage-analytics'), this.fetchData('/api/lm/stats/base-model-distribution'), this.fetchData('/api/lm/stats/tag-analytics'), this.fetchData('/api/lm/stats/storage-analytics'), this.fetchData('/api/lm/stats/insights') ]); this.data = { collection: collectionOverview.data, usage: usageAnalytics.data, baseModels: baseModelDistribution.data, tags: tagAnalytics.data, storage: storageAnalytics.data, insights: insights.data }; console.log('Statistics data loaded:', this.data); } catch (error) { console.error('Error loading statistics data:', error); showToast('toast.general.statisticsLoadFailed', {}, 'error'); } } async fetchData(endpoint) { const response = await fetch(endpoint); if (!response.ok) { throw new Error(`Failed to fetch ${endpoint}: ${response.statusText}`); } return response.json(); } async initializeVisualizations() { // Initialize metrics cards this.renderMetricsCards(); // Initialize charts this.initializeCharts(); // Initialize lists and other components await this.initializeLists(); this.renderLargestModelsList(); this.renderTagCloud(); this.renderInsights(); } renderMetricsCards() { const metricsGrid = document.getElementById('metricsGrid'); if (!metricsGrid || !this.data.collection) return; const metrics = [ { icon: 'fas fa-magic', value: this.data.collection.total_models, label: 'Total Models', format: 'number' }, { icon: 'fas fa-database', value: this.data.collection.total_size, label: 'Total Storage', format: 'size' }, { icon: 'fas fa-play-circle', value: this.data.collection.total_generations, label: 'Total Generations', format: 'number' }, { icon: 'fas fa-chart-line', value: this.calculateUsageRate(), label: 'Usage Rate', format: 'percentage' }, { icon: 'fas fa-layer-group', value: this.data.collection.lora_count, label: 'LoRAs', format: 'number' }, { icon: 'fas fa-check-circle', value: this.data.collection.checkpoint_count, label: 'Checkpoints', format: 'number' }, { icon: 'fas fa-code', value: this.data.collection.embedding_count, label: 'Embeddings', format: 'number' } ]; metricsGrid.innerHTML = metrics.map(metric => this.createMetricCard(metric)).join(''); } createMetricCard(metric) { const formattedValue = this.formatValue(metric.value, metric.format); return `
${formattedValue}
${metric.label}
`; } formatValue(value, format) { switch (format) { case 'number': return new Intl.NumberFormat().format(value); case 'size': return this.formatFileSize(value); case 'percentage': return `${value.toFixed(1)}%`; default: return value; } } formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } calculateUsageRate() { if (!this.data.collection) return 0; const totalModels = this.data.collection.total_models; const unusedModels = this.data.collection.unused_loras + this.data.collection.unused_checkpoints + this.data.collection.unused_embeddings; const usedModels = totalModels - unusedModels; return totalModels > 0 ? (usedModels / totalModels) * 100 : 0; } initializeCharts() { // Check if Chart.js is available if (typeof Chart === 'undefined') { console.warn('Chart.js is not available. Charts will not be rendered.'); this.showChartPlaceholders(); return; } // Collection pie chart this.createCollectionPieChart(); // Base model distribution chart this.createBaseModelChart(); // Usage timeline chart this.createUsageTimelineChart(); // Usage distribution chart this.createUsageDistributionChart(); // Storage chart this.createStorageChart(); // Storage efficiency chart this.createStorageEfficiencyChart(); } createCollectionPieChart() { const ctx = document.getElementById('collectionPieChart'); if (!ctx || !this.data.collection) return; const data = { labels: ['LoRAs', 'Checkpoints', 'Embeddings'], datasets: [{ data: [ this.data.collection.lora_count, this.data.collection.checkpoint_count, this.data.collection.embedding_count ], backgroundColor: [ 'oklch(68% 0.28 256)', 'oklch(68% 0.28 200)', 'oklch(68% 0.28 120)' ], borderWidth: 2, borderColor: getComputedStyle(document.documentElement).getPropertyValue('--border-color') }] }; this.charts.collection = new Chart(ctx, { type: 'doughnut', data: data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); } createBaseModelChart() { const ctx = document.getElementById('baseModelChart'); if (!ctx || !this.data.baseModels) return; const loraData = this.data.baseModels.loras; const checkpointData = this.data.baseModels.checkpoints; const embeddingData = this.data.baseModels.embeddings; const allModels = new Set([ ...Object.keys(loraData), ...Object.keys(checkpointData), ...Object.keys(embeddingData) ]); const data = { labels: Array.from(allModels), datasets: [ { label: 'LoRAs', data: Array.from(allModels).map(model => loraData[model] || 0), backgroundColor: 'oklch(68% 0.28 256 / 0.7)' }, { label: 'Checkpoints', data: Array.from(allModels).map(model => checkpointData[model] || 0), backgroundColor: 'oklch(68% 0.28 200 / 0.7)' }, { label: 'Embeddings', data: Array.from(allModels).map(model => embeddingData[model] || 0), backgroundColor: 'oklch(68% 0.28 120 / 0.7)' } ] }; this.charts.baseModels = new Chart(ctx, { type: 'bar', data: data, options: { responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true }, y: { stacked: true } } } }); } createUsageTimelineChart() { const ctx = document.getElementById('usageTimelineChart'); if (!ctx || !this.data.usage) return; const timeline = this.data.usage.usage_timeline || []; const data = { labels: timeline.map(item => new Date(item.date).toLocaleDateString()), datasets: [ { label: 'LoRA Usage', data: timeline.map(item => item.lora_usage), borderColor: 'oklch(68% 0.28 256)', backgroundColor: 'oklch(68% 0.28 256 / 0.1)', fill: true }, { label: 'Checkpoint Usage', data: timeline.map(item => item.checkpoint_usage), borderColor: 'oklch(68% 0.28 200)', backgroundColor: 'oklch(68% 0.28 200 / 0.1)', fill: true }, { label: 'Embedding Usage', data: timeline.map(item => item.embedding_usage), borderColor: 'oklch(68% 0.28 120)', backgroundColor: 'oklch(68% 0.28 120 / 0.1)', fill: true } ] }; this.charts.timeline = new Chart(ctx, { type: 'line', data: data, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, scales: { x: { display: true, title: { display: true, text: 'Date' } }, y: { display: true, title: { display: true, text: 'Usage Count' } } } } }); } createUsageDistributionChart() { const ctx = document.getElementById('usageDistributionChart'); if (!ctx || !this.data.usage) return; const topLoras = this.data.usage.top_loras || []; const topCheckpoints = this.data.usage.top_checkpoints || []; const topEmbeddings = this.data.usage.top_embeddings || []; // Combine and sort all models by usage const allModels = [ ...topLoras.map(m => ({ ...m, type: 'LoRA' })), ...topCheckpoints.map(m => ({ ...m, type: 'Checkpoint' })), ...topEmbeddings.map(m => ({ ...m, type: 'Embedding' })) ].sort((a, b) => b.usage_count - a.usage_count).slice(0, 10); const data = { labels: allModels.map(model => model.name), datasets: [{ label: 'Usage Count', data: allModels.map(model => model.usage_count), backgroundColor: allModels.map(model => { switch(model.type) { case 'LoRA': return 'oklch(68% 0.28 256)'; case 'Checkpoint': return 'oklch(68% 0.28 200)'; case 'Embedding': return 'oklch(68% 0.28 120)'; default: return 'oklch(68% 0.28 256)'; } }) }] }; this.charts.distribution = new Chart(ctx, { type: 'bar', data: data, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { legend: { display: false } } } }); } createStorageChart() { const ctx = document.getElementById('storageChart'); if (!ctx || !this.data.collection) return; const data = { labels: ['LoRAs', 'Checkpoints', 'Embeddings'], datasets: [{ data: [ this.data.collection.lora_size, this.data.collection.checkpoint_size, this.data.collection.embedding_size ], backgroundColor: [ 'oklch(68% 0.28 256)', 'oklch(68% 0.28 200)', 'oklch(68% 0.28 120)' ] }] }; this.charts.storage = new Chart(ctx, { type: 'doughnut', data: data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: (context) => { const value = this.formatFileSize(context.raw); return `${context.label}: ${value}`; } } } } } }); } createStorageEfficiencyChart() { const ctx = document.getElementById('storageEfficiencyChart'); if (!ctx || !this.data.storage) return; const loraData = this.data.storage.loras || []; const checkpointData = this.data.storage.checkpoints || []; const embeddingData = this.data.storage.embeddings || []; const allData = [ ...loraData.map(item => ({ ...item, type: 'LoRA' })), ...checkpointData.map(item => ({ ...item, type: 'Checkpoint' })), ...embeddingData.map(item => ({ ...item, type: 'Embedding' })) ]; const data = { datasets: [{ label: 'Models', data: allData.map(item => ({ x: item.size, y: item.usage_count, name: item.name, type: item.type })), backgroundColor: allData.map(item => { switch(item.type) { case 'LoRA': return 'oklch(68% 0.28 256 / 0.6)'; case 'Checkpoint': return 'oklch(68% 0.28 200 / 0.6)'; case 'Embedding': return 'oklch(68% 0.28 120 / 0.6)'; default: return 'oklch(68% 0.28 256 / 0.6)'; } }) }] }; this.charts.efficiency = new Chart(ctx, { type: 'scatter', data: data, options: { responsive: true, maintainAspectRatio: false, scales: { x: { title: { display: true, text: 'File Size (bytes)' }, type: 'logarithmic' }, y: { title: { display: true, text: 'Usage Count' } } }, plugins: { tooltip: { callbacks: { label: (context) => { const point = context.raw; return `${point.name}: ${this.formatFileSize(point.x)}, ${point.y} uses`; } } } } } }); } async initializeLists() { const listTypes = [ { type: 'lora', containerId: 'topLorasList' }, { type: 'checkpoint', containerId: 'topCheckpointsList' }, { type: 'embedding', containerId: 'topEmbeddingsList' } ]; const promises = listTypes.map(({ type, containerId }) => { const container = document.getElementById(containerId); if (container) { // Handle infinite scrolling container.addEventListener('scroll', () => { if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) { this.fetchAndRenderList(type, container); } }); // Initial fetch return this.fetchAndRenderList(type, container); } return Promise.resolve(); }); await Promise.all(promises); } async fetchAndRenderList(type, container) { const state = this.listStates[type]; if (state.isLoading || !state.hasMore) return; state.isLoading = true; // Show loading indicator on initial load if (state.offset === 0) { container.innerHTML = '
Loading...
'; } try { const url = `/api/lm/stats/model-usage-list?type=${type}&sort=${state.sort}&offset=${state.offset}&limit=${state.limit}`; const result = await this.fetchData(url); if (result.success) { const items = result.data.items; // Remove loading indicator if it's the first page if (state.offset === 0) { container.innerHTML = ''; } if (items.length === 0 && state.offset === 0) { container.innerHTML = '
No models found
'; state.hasMore = false; } else if (items.length < state.limit) { state.hasMore = false; } const html = items.map(model => `
${model.name}
${model.name}
${model.base_model} • ${model.folder || 'Root'}
${model.usage_count}
`).join(''); container.insertAdjacentHTML('beforeend', html); state.offset += state.limit; } } catch (error) { console.error(`Error loading ${type} list:`, error); if (state.offset === 0) { container.innerHTML = '
Error loading data
'; } } finally { state.isLoading = false; } } renderLargestModelsList() { const container = document.getElementById('largestModelsList'); if (!container || !this.data.storage) return; const loraModels = this.data.storage.loras || []; const checkpointModels = this.data.storage.checkpoints || []; const embeddingModels = this.data.storage.embeddings || []; // Combine and sort by size const allModels = [ ...loraModels.map(m => ({ ...m, type: 'LoRA' })), ...checkpointModels.map(m => ({ ...m, type: 'Checkpoint' })), ...embeddingModels.map(m => ({ ...m, type: 'Embedding' })) ].sort((a, b) => b.size - a.size).slice(0, 10); if (allModels.length === 0) { container.innerHTML = '
No storage data available
'; return; } container.innerHTML = allModels.map(model => `
${model.name}
${model.type} • ${model.base_model}
${this.formatFileSize(model.size)}
`).join(''); } renderTagCloud() { const container = document.getElementById('tagCloud'); if (!container || !this.data.tags?.top_tags) return; const topTags = this.data.tags.top_tags.slice(0, 30); // Show top 30 tags const maxCount = Math.max(...topTags.map(tag => tag.count)); container.innerHTML = topTags.map(tagData => { const size = Math.ceil((tagData.count / maxCount) * 5); return ` ${tagData.tag} `; }).join(''); } renderInsights() { const container = document.getElementById('insightsList'); if (!container || !this.data.insights?.insights) return; const insights = this.data.insights.insights; if (insights.length === 0) { container.innerHTML = '
No insights available
'; return; } container.innerHTML = insights.map(insight => `
${insight.title}
${insight.description}
${insight.suggestion}
`).join(''); // Render collection analysis cards this.renderCollectionAnalysis(); } renderCollectionAnalysis() { const container = document.getElementById('collectionAnalysis'); if (!container || !this.data.collection) return; const analysis = [ { icon: 'fas fa-percentage', value: this.calculateUsageRate(), label: 'Usage Rate', format: 'percentage' }, { icon: 'fas fa-tags', value: this.data.tags?.total_unique_tags || 0, label: 'Unique Tags', format: 'number' }, { icon: 'fas fa-clock', value: this.data.collection.unused_loras + this.data.collection.unused_checkpoints, label: 'Unused Models', format: 'number' }, { icon: 'fas fa-chart-line', value: this.calculateAverageUsage(), label: 'Avg. Uses/Model', format: 'decimal' } ]; container.innerHTML = analysis.map(item => `
${this.formatValue(item.value, item.format)}
${item.label}
`).join(''); } calculateAverageUsage() { if (!this.data.usage || !this.data.collection) return 0; const totalGenerations = this.data.collection.total_generations; const totalModels = this.data.collection.total_models; return totalModels > 0 ? totalGenerations / totalModels : 0; } showChartPlaceholders() { const chartCanvases = document.querySelectorAll('canvas'); chartCanvases.forEach(canvas => { const container = canvas.parentElement; container.innerHTML = '
Chart requires Chart.js library
'; }); } refreshChartsInPanel(panelId) { // Refresh charts when panels become visible setTimeout(() => { Object.values(this.charts).forEach(chart => { if (chart && typeof chart.resize === 'function') { chart.resize(); } }); }, 100); } destroy() { // Clean up charts Object.values(this.charts).forEach(chart => { if (chart && typeof chart.destroy === 'function') { chart.destroy(); } }); this.charts = {}; this.initialized = false; } } // Initialize statistics page when DOM is ready document.addEventListener('DOMContentLoaded', async () => { // Wait for app core to initialize await appCore.initialize(); // Initialize statistics functionality const statsManager = new StatisticsManager(); await statsManager.initialize(); // Make statsManager globally available for debugging window.statsManager = statsManager; console.log('Statistics page initialized successfully'); }); // Handle page unload window.addEventListener('beforeunload', () => { if (window.statsManager) { window.statsManager.destroy(); } });