diff --git a/py/lora_manager.py b/py/lora_manager.py index 2b04da36..08b254a4 100644 --- a/py/lora_manager.py +++ b/py/lora_manager.py @@ -5,6 +5,8 @@ from .routes.lora_routes import LoraRoutes from .routes.api_routes import ApiRoutes from .routes.recipe_routes import RecipeRoutes from .routes.checkpoints_routes import CheckpointsRoutes +from .routes.update_routes import UpdateRoutes +from .routes.usage_stats_routes import UsageStatsRoutes from .services.service_registry import ServiceRegistry import logging @@ -92,6 +94,8 @@ class LoraManager: checkpoints_routes.setup_routes(app) ApiRoutes.setup_routes(app) RecipeRoutes.setup_routes(app) + UpdateRoutes.setup_routes(app) + UsageStatsRoutes.setup_routes(app) # Register usage stats routes # Schedule service initialization app.on_startup.append(lambda app: cls._initialize_services()) diff --git a/py/metadata_collector/constants.py b/py/metadata_collector/constants.py index c1109580..9a3ba95f 100644 --- a/py/metadata_collector/constants.py +++ b/py/metadata_collector/constants.py @@ -1,12 +1,14 @@ """Constants used by the metadata collector""" -# Individual category constants +# Metadata collection constants + +# Metadata categories MODELS = "models" PROMPTS = "prompts" SAMPLING = "sampling" LORAS = "loras" SIZE = "size" -IMAGES = "images" # Added new category for image results +IMAGES = "images" -# Collection of categories for iteration -METADATA_CATEGORIES = [MODELS, PROMPTS, SAMPLING, LORAS, SIZE, IMAGES] # Added IMAGES to categories +# Complete list of categories to track +METADATA_CATEGORIES = [MODELS, PROMPTS, SAMPLING, LORAS, SIZE, IMAGES] diff --git a/py/routes/usage_stats_routes.py b/py/routes/usage_stats_routes.py new file mode 100644 index 00000000..8a90e6c5 --- /dev/null +++ b/py/routes/usage_stats_routes.py @@ -0,0 +1,69 @@ +import logging +from aiohttp import web +from ..utils.usage_stats import UsageStats + +logger = logging.getLogger(__name__) + +class UsageStatsRoutes: + """Routes for handling usage statistics updates""" + + @staticmethod + def setup_routes(app): + """Register usage stats routes""" + app.router.add_post('/loras/api/update-usage-stats', UsageStatsRoutes.update_usage_stats) + app.router.add_get('/loras/api/get-usage-stats', UsageStatsRoutes.get_usage_stats) + + @staticmethod + async def update_usage_stats(request): + """ + Update usage statistics based on a prompt_id + + Expects a JSON body with: + { + "prompt_id": "string" + } + """ + try: + # Parse the request body + data = await request.json() + prompt_id = data.get('prompt_id') + + if not prompt_id: + return web.json_response({ + 'success': False, + 'error': 'Missing prompt_id' + }, status=400) + + # Call the UsageStats to process this prompt_id synchronously + usage_stats = UsageStats() + await usage_stats.process_execution(prompt_id) + + return web.json_response({ + 'success': True + }) + + except Exception as e: + logger.error(f"Failed to update usage stats: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + @staticmethod + async def get_usage_stats(request): + """Get current usage statistics""" + try: + usage_stats = UsageStats() + stats = await usage_stats.get_stats() + + return web.json_response({ + 'success': True, + 'data': stats + }) + + except Exception as e: + logger.error(f"Failed to get usage stats: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/py/utils/usage_stats.py b/py/utils/usage_stats.py new file mode 100644 index 00000000..1d365120 --- /dev/null +++ b/py/utils/usage_stats.py @@ -0,0 +1,267 @@ +import os +import json +import time +import asyncio +import logging +from typing import Dict, Set + +from ..config import config +from ..services.service_registry import ServiceRegistry +from ..metadata_collector.metadata_registry import MetadataRegistry +from ..metadata_collector.constants import MODELS, LORAS + +logger = logging.getLogger(__name__) + +class UsageStats: + """Track usage statistics for models and save to JSON""" + + _instance = None + _lock = asyncio.Lock() # For thread safety + + # Default stats file name + STATS_FILENAME = "lora_manager_stats.json" + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + # Initialize stats storage + self.stats = { + "checkpoints": {}, # sha256 -> count + "loras": {}, # sha256 -> count + "total_executions": 0, + "last_save_time": 0 + } + + # Queue for prompt_ids to process + self.pending_prompt_ids = set() + + # Load existing stats if available + self._stats_file_path = self._get_stats_file_path() + self._load_stats() + + # Save interval in seconds + self.save_interval = 90 # 1.5 minutes + + # Start background task to process queued prompt_ids + self._bg_task = asyncio.create_task(self._background_processor()) + + self._initialized = True + logger.info("Usage statistics tracker initialized") + + def _get_stats_file_path(self) -> str: + """Get the path to the stats JSON file""" + if not config.loras_roots or len(config.loras_roots) == 0: + # Fallback to temporary directory if no lora roots + return os.path.join(config.temp_directory, self.STATS_FILENAME) + + # Use the first lora root + return os.path.join(config.loras_roots[0], self.STATS_FILENAME) + + def _load_stats(self): + """Load existing statistics from file""" + try: + if os.path.exists(self._stats_file_path): + with open(self._stats_file_path, 'r', encoding='utf-8') as f: + loaded_stats = json.load(f) + + # Update our stats with loaded data + if isinstance(loaded_stats, dict): + # Update individual sections to maintain structure + if "checkpoints" in loaded_stats and isinstance(loaded_stats["checkpoints"], dict): + self.stats["checkpoints"] = loaded_stats["checkpoints"] + + if "loras" in loaded_stats and isinstance(loaded_stats["loras"], dict): + self.stats["loras"] = loaded_stats["loras"] + + if "total_executions" in loaded_stats: + self.stats["total_executions"] = loaded_stats["total_executions"] + + logger.info(f"Loaded usage statistics from {self._stats_file_path}") + except Exception as e: + logger.error(f"Error loading usage statistics: {e}") + + async def save_stats(self, force=False): + """Save statistics to file""" + try: + # Only save if it's been at least save_interval since last save or force is True + current_time = time.time() + if not force and (current_time - self.stats.get("last_save_time", 0)) < self.save_interval: + return False + + # Use a lock to prevent concurrent writes + async with self._lock: + # Update last save time + self.stats["last_save_time"] = current_time + + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(self._stats_file_path), exist_ok=True) + + # Write to a temporary file first, then move it to avoid corruption + temp_path = f"{self._stats_file_path}.tmp" + with open(temp_path, 'w', encoding='utf-8') as f: + json.dump(self.stats, f, indent=2, ensure_ascii=False) + + # Replace the old file with the new one + os.replace(temp_path, self._stats_file_path) + + logger.debug(f"Saved usage statistics to {self._stats_file_path}") + return True + except Exception as e: + logger.error(f"Error saving usage statistics: {e}", exc_info=True) + return False + + def register_execution(self, prompt_id): + """Register a completed execution by prompt_id for later processing""" + if prompt_id: + self.pending_prompt_ids.add(prompt_id) + + async def _background_processor(self): + """Background task to process queued prompt_ids""" + try: + while True: + # Wait a short interval before checking for new prompt_ids + await asyncio.sleep(5) # Check every 5 seconds + + # Process any pending prompt_ids + if self.pending_prompt_ids: + async with self._lock: + # Get a copy of the set and clear original + prompt_ids = self.pending_prompt_ids.copy() + self.pending_prompt_ids.clear() + + # Process each prompt_id + registry = MetadataRegistry() + for prompt_id in prompt_ids: + try: + metadata = registry.get_metadata(prompt_id) + await self._process_metadata(metadata) + except Exception as e: + logger.error(f"Error processing prompt_id {prompt_id}: {e}") + + # Periodically save stats + await self.save_stats() + except asyncio.CancelledError: + # Task was cancelled, clean up + await self.save_stats(force=True) + except Exception as e: + logger.error(f"Error in background processing task: {e}", exc_info=True) + # Restart the task after a delay if it fails + asyncio.create_task(self._restart_background_task()) + + async def _restart_background_task(self): + """Restart the background task after a delay""" + await asyncio.sleep(30) # Wait 30 seconds before restarting + self._bg_task = asyncio.create_task(self._background_processor()) + + async def _process_metadata(self, metadata): + """Process metadata from an execution""" + if not metadata or not isinstance(metadata, dict): + return + + # Increment total executions count + self.stats["total_executions"] += 1 + + # Process checkpoints + if MODELS in metadata and isinstance(metadata[MODELS], dict): + await self._process_checkpoints(metadata[MODELS]) + + # Process loras + if LORAS in metadata and isinstance(metadata[LORAS], dict): + await self._process_loras(metadata[LORAS]) + + async def _process_checkpoints(self, models_data): + """Process checkpoint models from metadata""" + try: + # Get checkpoint scanner service + checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner() + if not checkpoint_scanner: + logger.warning("Checkpoint scanner not available for usage tracking") + return + + for node_id, model_info in models_data.items(): + if not isinstance(model_info, dict): + continue + + # Check if this is a checkpoint model + model_type = model_info.get("type") + if model_type == "checkpoint": + model_name = model_info.get("name") + if not model_name: + continue + + # Clean up filename (remove extension if present) + model_filename = os.path.splitext(os.path.basename(model_name))[0] + + # Get hash for this checkpoint + model_hash = checkpoint_scanner.get_hash_by_filename(model_filename) + if model_hash: + # Update stats for this checkpoint + self.stats["checkpoints"][model_hash] = self.stats["checkpoints"].get(model_hash, 0) + 1 + except Exception as e: + logger.error(f"Error processing checkpoint usage: {e}", exc_info=True) + + async def _process_loras(self, loras_data): + """Process LoRA models from metadata""" + try: + # Get LoRA scanner service + lora_scanner = await ServiceRegistry.get_lora_scanner() + if not lora_scanner: + logger.warning("LoRA scanner not available for usage tracking") + return + + for node_id, lora_info in loras_data.items(): + if not isinstance(lora_info, dict): + continue + + # Get the list of LoRAs from standardized format + lora_list = lora_info.get("lora_list", []) + for lora in lora_list: + if not isinstance(lora, dict): + continue + + lora_name = lora.get("name") + if not lora_name: + continue + + # Get hash for this LoRA + lora_hash = lora_scanner.get_hash_by_filename(lora_name) + if lora_hash: + # Update stats for this LoRA + self.stats["loras"][lora_hash] = self.stats["loras"].get(lora_hash, 0) + 1 + except Exception as e: + logger.error(f"Error processing LoRA usage: {e}", exc_info=True) + + async def get_stats(self): + """Get current usage statistics""" + return self.stats + + async def get_model_usage_count(self, model_type, sha256): + """Get usage count for a specific model by hash""" + if model_type == "checkpoint": + return self.stats["checkpoints"].get(sha256, 0) + elif model_type == "lora": + return self.stats["loras"].get(sha256, 0) + return 0 + + async def process_execution(self, prompt_id): + """Process a prompt execution immediately (synchronous approach)""" + if not prompt_id: + return + + try: + # Process metadata for this prompt_id + registry = MetadataRegistry() + metadata = registry.get_metadata(prompt_id) + if metadata: + await self._process_metadata(metadata) + # Save stats if needed + await self.save_stats() + except Exception as e: + logger.error(f"Error processing prompt_id {prompt_id}: {e}", exc_info=True) diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js index 4984d2ca..96df010a 100644 --- a/static/js/components/CheckpointCard.js +++ b/static/js/components/CheckpointCard.js @@ -1,4 +1,4 @@ -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showCheckpointModal } from './checkpointModal/index.js'; import { NSFW_LEVELS } from '../utils/constants.js'; @@ -204,21 +204,7 @@ export function createCheckpointCard(checkpoint) { const checkpointName = card.dataset.file_name; try { - // Modern clipboard API - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(checkpointName); - } else { - // Fallback for older browsers - const textarea = document.createElement('textarea'); - textarea.value = checkpointName; - textarea.style.position = 'absolute'; - textarea.style.left = '-99999px'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - } - showToast('Checkpoint name copied', 'success'); + await copyToClipboard(checkpointName, 'Checkpoint name copied'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index fa5e8a0b..fb97aadb 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -1,4 +1,4 @@ -import { showToast, openCivitai } from '../utils/uiHelpers.js'; +import { showToast, openCivitai, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showLoraModal } from './loraModal/index.js'; import { bulkManager } from '../managers/BulkManager.js'; @@ -205,26 +205,7 @@ export function createLoraCard(lora) { const strength = usageTips.strength || 1; const loraSyntax = ``; - try { - // Modern clipboard API - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(loraSyntax); - } else { - // Fallback for older browsers - const textarea = document.createElement('textarea'); - textarea.value = loraSyntax; - textarea.style.position = 'absolute'; - textarea.style.left = '-99999px'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - } - showToast('LoRA syntax copied', 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } + await copyToClipboard(loraSyntax, 'LoRA syntax copied'); }); // Civitai button click event diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index d4285b21..c87d52bb 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -1,5 +1,5 @@ // Recipe Card Component -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { modalManager } from '../managers/ModalManager.js'; class RecipeCard { @@ -109,14 +109,11 @@ class RecipeCard { .then(response => response.json()) .then(data => { if (data.success && data.syntax) { - return navigator.clipboard.writeText(data.syntax); + return copyToClipboard(data.syntax, 'Recipe syntax copied to clipboard'); } else { throw new Error(data.error || 'No syntax returned'); } }) - .then(() => { - showToast('Recipe syntax copied to clipboard', 'success'); - }) .catch(err => { console.error('Failed to copy: ', err); showToast('Failed to copy recipe syntax', 'error'); @@ -279,4 +276,4 @@ class RecipeCard { } } -export { RecipeCard }; \ No newline at end of file +export { RecipeCard }; \ No newline at end of file diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index ec09ec0c..5c81b242 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -1,5 +1,5 @@ // Recipe Modal Component -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js'; @@ -747,9 +747,8 @@ class RecipeModal { const data = await response.json(); if (data.success && data.syntax) { - // Copy to clipboard - await navigator.clipboard.writeText(data.syntax); - showToast('Recipe syntax copied to clipboard', 'success'); + // Use the centralized copyToClipboard utility function + await copyToClipboard(data.syntax, 'Recipe syntax copied to clipboard'); } else { throw new Error(data.error || 'No syntax returned from server'); } @@ -761,12 +760,7 @@ class RecipeModal { // Helper method to copy text to clipboard copyToClipboard(text, successMessage) { - navigator.clipboard.writeText(text).then(() => { - showToast(successMessage, 'success'); - }).catch(err => { - console.error('Failed to copy text: ', err); - showToast('Failed to copy text', 'error'); - }); + copyToClipboard(text, successMessage); } // Add new method to handle downloading missing LoRAs diff --git a/static/js/components/checkpointModal/ShowcaseView.js b/static/js/components/checkpointModal/ShowcaseView.js index d9843fc3..0ee80079 100644 --- a/static/js/components/checkpointModal/ShowcaseView.js +++ b/static/js/components/checkpointModal/ShowcaseView.js @@ -2,7 +2,7 @@ * ShowcaseView.js * Handles showcase content (images, videos) display for checkpoint modal */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; @@ -307,8 +307,7 @@ function initMetadataPanelHandlers(container) { if (!promptElement) return; try { - await navigator.clipboard.writeText(promptElement.textContent); - showToast('Prompt copied to clipboard', 'success'); + await copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/loraModal/RecipeTab.js b/static/js/components/loraModal/RecipeTab.js index 264d24e5..412b4532 100644 --- a/static/js/components/loraModal/RecipeTab.js +++ b/static/js/components/loraModal/RecipeTab.js @@ -1,7 +1,7 @@ /** * RecipeTab - Handles the recipes tab in the Lora Modal */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; /** @@ -172,14 +172,11 @@ function copyRecipeSyntax(recipeId) { .then(response => response.json()) .then(data => { if (data.success && data.syntax) { - return navigator.clipboard.writeText(data.syntax); + return copyToClipboard(data.syntax, 'Recipe syntax copied to clipboard'); } else { throw new Error(data.error || 'No syntax returned'); } }) - .then(() => { - showToast('Recipe syntax copied to clipboard', 'success'); - }) .catch(err => { console.error('Failed to copy: ', err); showToast('Failed to copy recipe syntax', 'error'); diff --git a/static/js/components/loraModal/ShowcaseView.js b/static/js/components/loraModal/ShowcaseView.js index 2e2858e9..9bd7ee3d 100644 --- a/static/js/components/loraModal/ShowcaseView.js +++ b/static/js/components/loraModal/ShowcaseView.js @@ -2,7 +2,7 @@ * ShowcaseView.js * 处理LoRA模型展示内容(图片、视频)的功能模块 */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; @@ -311,8 +311,7 @@ function initMetadataPanelHandlers(container) { if (!promptElement) return; try { - await navigator.clipboard.writeText(promptElement.textContent); - showToast('Prompt copied to clipboard', 'success'); + await copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/loraModal/TriggerWords.js b/static/js/components/loraModal/TriggerWords.js index 537bd37a..e80c9e39 100644 --- a/static/js/components/loraModal/TriggerWords.js +++ b/static/js/components/loraModal/TriggerWords.js @@ -2,7 +2,7 @@ * TriggerWords.js * 处理LoRA模型触发词相关的功能模块 */ -import { showToast } from '../../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { saveModelMetadata } from './ModelMetadata.js'; /** @@ -336,23 +336,7 @@ async function saveTriggerWords() { */ window.copyTriggerWord = async function(word) { try { - // Modern clipboard API - with fallback for non-secure contexts - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(word); - } else { - // Fallback for older browsers or non-secure contexts - const textarea = document.createElement('textarea'); - textarea.value = word; - textarea.style.position = 'absolute'; - textarea.style.left = '-99999px'; - document.body.appendChild(textarea); - textarea.select(); - const success = document.execCommand('copy'); - document.body.removeChild(textarea); - - if (!success) throw new Error('Copy command failed'); - } - showToast('Trigger word copied', 'success'); + await copyToClipboard(word, 'Trigger word copied'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/components/loraModal/index.js b/static/js/components/loraModal/index.js index 017a1b2a..69e71675 100644 --- a/static/js/components/loraModal/index.js +++ b/static/js/components/loraModal/index.js @@ -3,8 +3,7 @@ * * 将原始的LoraModal.js拆分成多个功能模块后的主入口文件 */ -import { showToast } from '../../utils/uiHelpers.js'; -import { state } from '../../state/index.js'; +import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { modalManager } from '../../managers/ModalManager.js'; import { renderShowcaseContent, toggleShowcase, setupShowcaseScroll, scrollToTop } from './ShowcaseView.js'; import { setupTabSwitching, loadModelDescription } from './ModelDescription.js'; @@ -174,8 +173,7 @@ export function showLoraModal(lora) { // Copy file name function window.copyFileName = async function(fileName) { try { - await navigator.clipboard.writeText(fileName); - showToast('File name copied', 'success'); + await copyToClipboard(fileName, 'File name copied'); } catch (err) { console.error('Copy failed:', err); showToast('Copy failed', 'error'); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 76fa5043..7826841d 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,5 +1,5 @@ import { state } from '../state/index.js'; -import { showToast } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { updateCardsForBulkMode } from '../components/LoraCard.js'; export class BulkManager { @@ -205,13 +205,7 @@ export class BulkManager { return; } - try { - await navigator.clipboard.writeText(loraSyntaxes.join(', ')); - showToast(`Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`, 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } + await copyToClipboard(loraSyntaxes.join(', '), `Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`); } // Create and show the thumbnail strip of selected LoRAs diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 9c2a6dd1..cd2edde0 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -2,6 +2,40 @@ import { state } from '../state/index.js'; import { resetAndReload } from '../api/loraApi.js'; import { getStorageItem, setStorageItem } from './storageHelpers.js'; +/** + * Utility function to copy text to clipboard with fallback for older browsers + * @param {string} text - The text to copy to clipboard + * @param {string} successMessage - Optional success message to show in toast + * @returns {Promise} - Promise that resolves to true if copy was successful + */ +export async function copyToClipboard(text, successMessage = 'Copied to clipboard') { + try { + // Modern clipboard API + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'absolute'; + textarea.style.left = '-99999px'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } + + if (successMessage) { + showToast(successMessage, 'success'); + } + return true; + } catch (err) { + console.error('Copy failed:', err); + showToast('Copy failed', 'error'); + return false; + } +} + export function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; @@ -108,12 +142,6 @@ export function toggleFolder(tag) { resetAndReload(); } -export function copyTriggerWord(word) { - navigator.clipboard.writeText(word).then(() => { - showToast('Trigger word copied', 'success'); - }); -} - function filterByFolder(folderPath) { document.querySelectorAll('.lora-card').forEach(card => { card.style.display = card.dataset.folder === folderPath ? '' : 'none'; diff --git a/web/comfyui/usage_stats.js b/web/comfyui/usage_stats.js new file mode 100644 index 00000000..97fcda74 --- /dev/null +++ b/web/comfyui/usage_stats.js @@ -0,0 +1,37 @@ +// ComfyUI extension to track model usage statistics +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Register the extension +app.registerExtension({ + name: "ComfyUI-Lora-Manager.UsageStats", + + init() { + // Listen for successful executions + api.addEventListener("execution_success", ({ detail }) => { + if (detail && detail.prompt_id) { + this.updateUsageStats(detail.prompt_id); + } + }); + }, + + async updateUsageStats(promptId) { + try { + console.log("Updating usage statistics for prompt ID:", promptId); + // Call backend endpoint with the prompt_id + const response = await fetch(`/loras/api/update-usage-stats`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ prompt_id: promptId }), + }); + + if (!response.ok) { + console.warn("Failed to update usage statistics:", response.statusText); + } + } catch (error) { + console.error("Error updating usage statistics:", error); + } + } +});