import { app } from "../../scripts/app.js"; /** * ModelPulse - Usage Tracker Extension * * Displays model usage statistics in a sidebar panel. */ // State management let currentTimeframe = "all"; let currentSort = "last_used"; let searchQuery = ""; let usageData = null; let isLoading = false; let showSettings = false; // Settings with defaults let settings = { staleThresholdDays: 30, }; // Load settings from localStorage function loadSettings() { try { const saved = localStorage.getItem("modelpulse_settings"); if (saved) { settings = { ...settings, ...JSON.parse(saved) }; } } catch (e) { console.warn("[ModelPulse] Failed to load settings:", e); } } // Save settings to localStorage function saveSettings() { try { localStorage.setItem("modelpulse_settings", JSON.stringify(settings)); } catch (e) { console.warn("[ModelPulse] Failed to save settings:", e); } } // Initialize settings on load loadSettings(); /** * Fetch usage data from the API */ async function fetchUsageData(timeframe = "all", sortBy = "last_used") { try { const response = await fetch( `/modelpulse/usage?timeframe=${timeframe}&sort=${sortBy}` ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error("[ModelPulse] Error fetching usage data:", error); return null; } } /** * Format file size to human readable string (GB or MB) */ function formatFileSize(bytes) { if (bytes === null || bytes === undefined) return null; const gb = bytes / (1024 * 1024 * 1024); if (gb >= 1) { return `${gb.toFixed(1)} GB`; } const mb = bytes / (1024 * 1024); return `${mb.toFixed(0)} MB`; } /** * Format ISO date to relative time string */ function formatLastUsed(isoDate) { if (!isoDate) return "Never"; const date = new Date(isoDate); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return "just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 30) return `${diffDays}d ago`; const diffMonths = Math.floor(diffDays / 30); if (diffMonths < 12) return `${diffMonths}mo ago`; const diffYears = Math.floor(diffDays / 365); return `${diffYears}y ago`; } /** * Get category display info */ function getCategoryInfo(category) { const categories = { checkpoint: { name: "Checkpoints", icon: "📦" }, lora: { name: "LoRAs", icon: "✨" }, vae: { name: "VAEs", icon: "⚙️" }, controlnet: { name: "ControlNets", icon: "🎛️" }, clip: { name: "CLIP", icon: "💬" }, unet: { name: "UNETs", icon: "🖥️" }, upscaler: { name: "Upscalers", icon: "🔍" }, style_model: { name: "Style Models", icon: "🎨" }, gligen: { name: "GLIGEN", icon: "📐" }, gguf: { name: "GGUF", icon: "🗜️" }, }; return categories[category] || { name: category, icon: "📁" }; } /** * Check if a model is stale (unused for > threshold days) */ function isStale(lastUsed) { if (!lastUsed) return true; const date = new Date(lastUsed); const now = new Date(); const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); return diffDays > settings.staleThresholdDays; } /** * Filter models by search query */ function filterModels(models, query) { if (!query || !query.trim()) return models; const lowerQuery = query.toLowerCase().trim(); return models.filter(model => model.name.toLowerCase().includes(lowerQuery) || model.category.toLowerCase().includes(lowerQuery) ); } /** * Group models by category */ function groupByCategory(models) { const groups = {}; for (const model of models) { const cat = model.category; if (!groups[cat]) { groups[cat] = []; } groups[cat].push(model); } return groups; } /** * Create the styles for the UI */ function createStyles() { const style = document.createElement("style"); style.textContent = ` .modelpulse-container { display: flex; flex-direction: column; height: 100%; font-family: var(--comfy-font, sans-serif); font-size: 13px; color: var(--input-text, #ddd); } .modelpulse-header { display: flex; justify-content: space-between; align-items: center; padding: 12px; border-bottom: 1px solid var(--border-color, #444); } .modelpulse-title { font-size: 15px; font-weight: 600; } .modelpulse-refresh { background: transparent; border: none; color: var(--input-text, #ddd); cursor: pointer; padding: 4px 8px; border-radius: 4px; font-size: 14px; } .modelpulse-refresh:hover { background: var(--comfy-menu-bg-hover, #333); } .modelpulse-refresh.loading { opacity: 0.5; cursor: wait; } .modelpulse-tabs { display: flex; border-bottom: 1px solid var(--border-color, #444); } .modelpulse-tab { flex: 1; padding: 10px; text-align: center; cursor: pointer; background: transparent; border: none; color: var(--input-text, #ddd); border-bottom: 2px solid transparent; transition: all 0.15s ease; } .modelpulse-tab:hover { background: var(--comfy-menu-bg-hover, #333); } .modelpulse-tab.active { border-bottom-color: var(--primary-color, #4a9eff); color: var(--primary-color, #4a9eff); } .modelpulse-controls { display: flex; padding: 8px 12px; gap: 8px; align-items: center; border-bottom: 1px solid var(--border-color, #444); } .modelpulse-sort-label { font-size: 12px; color: var(--descrip-text, #999); } .modelpulse-sort { flex: 1; padding: 6px 8px; background: var(--comfy-input-bg, #222); border: 1px solid var(--border-color, #444); border-radius: 4px; color: var(--input-text, #ddd); font-size: 12px; cursor: pointer; } .modelpulse-list { flex: 1; overflow-y: auto; padding: 8px 0; } .modelpulse-category { margin-bottom: 4px; } .modelpulse-category-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--comfy-menu-bg, #2a2a2a); cursor: pointer; user-select: none; } .modelpulse-category-header:hover { background: var(--comfy-menu-bg-hover, #333); } .modelpulse-category-icon { font-size: 14px; } .modelpulse-category-name { flex: 1; font-weight: 500; } .modelpulse-category-count { font-size: 11px; color: var(--descrip-text, #999); background: var(--comfy-input-bg, #222); padding: 2px 6px; border-radius: 10px; } .modelpulse-category-arrow { font-size: 10px; transition: transform 0.15s ease; } .modelpulse-category.collapsed .modelpulse-category-arrow { transform: rotate(-90deg); } .modelpulse-category-models { display: block; } .modelpulse-category.collapsed .modelpulse-category-models { display: none; } .modelpulse-model { display: flex; flex-direction: column; padding: 10px 12px 10px 32px; border-bottom: 1px solid var(--border-color, #333); cursor: default; } .modelpulse-model:hover { background: var(--comfy-menu-bg-hover, #333); } .modelpulse-model-name { font-weight: 500; margin-bottom: 4px; word-break: break-all; } .modelpulse-model-stats { display: flex; gap: 16px; font-size: 11px; color: var(--descrip-text, #999); } .modelpulse-model.stale .modelpulse-model-name { color: var(--error-text, #ff6b6b); } .modelpulse-model.stale .modelpulse-last-used { color: var(--error-text, #ff6b6b); } .modelpulse-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 200px; color: var(--descrip-text, #999); text-align: center; padding: 20px; } .modelpulse-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; } .modelpulse-loading { display: flex; align-items: center; justify-content: center; height: 100px; color: var(--descrip-text, #999); } .modelpulse-search-container { padding: 8px 12px; border-bottom: 1px solid var(--border-color, #444); } .modelpulse-search { width: 100%; padding: 8px 10px; background: var(--comfy-input-bg, #222); border: 1px solid var(--border-color, #444); border-radius: 4px; color: var(--input-text, #ddd); font-size: 13px; box-sizing: border-box; } .modelpulse-search:focus { outline: none; border-color: var(--primary-color, #4a9eff); } .modelpulse-search::placeholder { color: var(--descrip-text, #999); } .modelpulse-header-buttons { display: flex; gap: 4px; } .modelpulse-settings-btn { background: transparent; border: none; color: var(--input-text, #ddd); cursor: pointer; padding: 4px 8px; border-radius: 4px; font-size: 14px; } .modelpulse-settings-btn:hover { background: var(--comfy-menu-bg-hover, #333); } .modelpulse-settings-panel { padding: 12px; background: var(--comfy-menu-bg, #2a2a2a); border-bottom: 1px solid var(--border-color, #444); } .modelpulse-settings-title { font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; } .modelpulse-settings-close { background: transparent; border: none; color: var(--input-text, #ddd); cursor: pointer; font-size: 16px; padding: 2px 6px; border-radius: 4px; } .modelpulse-settings-close:hover { background: var(--comfy-menu-bg-hover, #333); } .modelpulse-setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .modelpulse-setting-row:last-child { margin-bottom: 0; } .modelpulse-setting-label { font-size: 12px; color: var(--input-text, #ddd); } .modelpulse-setting-input { width: 60px; padding: 4px 8px; background: var(--comfy-input-bg, #222); border: 1px solid var(--border-color, #444); border-radius: 4px; color: var(--input-text, #ddd); font-size: 12px; text-align: center; } .modelpulse-setting-input:focus { outline: none; border-color: var(--primary-color, #4a9eff); } .modelpulse-no-results { padding: 20px; text-align: center; color: var(--descrip-text, #999); font-size: 12px; } .modelpulse-reset-row { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-color, #444); } .modelpulse-reset-btn { padding: 4px 12px; background: var(--error-text, #ff6b6b); border: none; border-radius: 4px; color: #fff; font-size: 12px; cursor: pointer; } .modelpulse-reset-btn:hover { background: #ff5252; } .modelpulse-reset-btn:disabled { opacity: 0.5; cursor: not-allowed; } `; return style; } /** * Render the ModelPulse UI into the container element */ async function renderModelPulseUI(container) { // Add styles container.appendChild(createStyles()); // Create main container const main = document.createElement("div"); main.className = "modelpulse-container"; container.appendChild(main); // Header const header = document.createElement("div"); header.className = "modelpulse-header"; header.innerHTML = ` Usage
`; main.appendChild(header); // Settings panel (hidden by default) const settingsPanel = document.createElement("div"); settingsPanel.className = "modelpulse-settings-panel"; settingsPanel.style.display = "none"; settingsPanel.innerHTML = `