From d6e175c1f190bbd5589d8e03c5d0f9acc7fc15a5 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 13 May 2025 15:14:25 +0800 Subject: [PATCH] Add API endpoints for retrieving LoRA notes and trigger words; enhance context menu with copy options. Supports #177 --- py/routes/api_routes.py | 82 ++++++++++++++++++++ web/comfyui/loras_widget_events.js | 116 +++++++++++++++++++++++------ web/comfyui/loras_widget_utils.js | 57 ++++++++++++++ 3 files changed, 231 insertions(+), 24 deletions(-) diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index b268744c..c94394a0 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -72,6 +72,10 @@ class ApiRoutes: # Add new endpoint for letter counts app.router.add_get('/api/loras/letter-counts', routes.get_letter_counts) + + # Add new endpoints for copying lora data + app.router.add_get('/api/loras/get-notes', routes.get_lora_notes) + app.router.add_get('/api/loras/get-trigger-words', routes.get_lora_trigger_words) # Add update check routes UpdateRoutes.setup_routes(app) @@ -1084,3 +1088,81 @@ class ApiRoutes: 'success': False, 'error': str(e) }, status=500) + + async def get_lora_notes(self, request: web.Request) -> web.Response: + """Get notes for a specific LoRA file""" + try: + if self.scanner is None: + self.scanner = await ServiceRegistry.get_lora_scanner() + + # Get lora file name from query parameters + lora_name = request.query.get('name') + if not lora_name: + return web.Response(text='Lora file name is required', status=400) + + # Get cache data + cache = await self.scanner.get_cached_data() + + # Search for the lora in cache data + for lora in cache.raw_data: + file_name = lora['file_name'] + if file_name == lora_name: + notes = lora.get('notes', '') + + return web.json_response({ + 'success': True, + 'notes': notes + }) + + # If lora not found + return web.json_response({ + 'success': False, + 'error': 'LoRA not found in cache' + }, status=404) + + except Exception as e: + logger.error(f"Error getting lora notes: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + async def get_lora_trigger_words(self, request: web.Request) -> web.Response: + """Get trigger words for a specific LoRA file""" + try: + if self.scanner is None: + self.scanner = await ServiceRegistry.get_lora_scanner() + + # Get lora file name from query parameters + lora_name = request.query.get('name') + if not lora_name: + return web.Response(text='Lora file name is required', status=400) + + # Get cache data + cache = await self.scanner.get_cached_data() + + # Search for the lora in cache data + for lora in cache.raw_data: + file_name = lora['file_name'] + if file_name == lora_name: + # Get trigger words from civitai data + civitai_data = lora.get('civitai', {}) + trigger_words = civitai_data.get('trainedWords', []) + + return web.json_response({ + 'success': True, + 'trigger_words': trigger_words + }) + + # If lora not found + return web.json_response({ + 'success': False, + 'error': 'LoRA not found in cache' + }, status=404) + + except Exception as e: + logger.error(f"Error getting lora trigger words: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/web/comfyui/loras_widget_events.js b/web/comfyui/loras_widget_events.js index 238a6f66..21feca87 100644 --- a/web/comfyui/loras_widget_events.js +++ b/web/comfyui/loras_widget_events.js @@ -1,6 +1,6 @@ import { api } from "../../scripts/api.js"; import { createMenuItem } from "./loras_widget_components.js"; -import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly } from "./loras_widget_utils.js"; +import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast } from "./loras_widget_utils.js"; // Function to handle strength adjustment via dragging export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) { @@ -172,29 +172,11 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render window.open(data.civitai_url, '_blank'); } else { // Show error message if no Civitai URL - if (app && app.extensionManager && app.extensionManager.toast) { - app.extensionManager.toast.add({ - severity: 'warning', - summary: 'Not Found', - detail: 'This LoRA has no associated Civitai URL', - life: 3000 - }); - } else { - alert('This LoRA has no associated Civitai URL'); - } + showToast('This LoRA has no associated Civitai URL', 'warning'); } } catch (error) { console.error('Error getting Civitai URL:', error); - if (app && app.extensionManager && app.extensionManager.toast) { - app.extensionManager.toast.add({ - severity: 'error', - summary: 'Error', - detail: error.message || 'Failed to get Civitai URL', - life: 5000 - }); - } else { - alert('Error: ' + (error.message || 'Failed to get Civitai URL')); - } + showToast(error.message || 'Failed to get Civitai URL', 'error'); } } ); @@ -221,6 +203,82 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render } ); + // New option: Copy Notes with note icon + const copyNotesOption = createMenuItem( + 'Copy Notes', + '', + async () => { + menu.remove(); + document.removeEventListener('click', closeMenu); + + try { + // Get notes from API + const response = await api.fetchApi(`/loras/get-notes?name=${encodeURIComponent(loraName)}`, { + method: 'GET' + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Failed to get notes'); + } + + const data = await response.json(); + if (data.success) { + const notes = data.notes || ''; + if (notes.trim()) { + await copyToClipboard(notes, 'Notes copied to clipboard'); + } else { + showToast('No notes available for this LoRA', 'info'); + } + } else { + throw new Error(data.error || 'Failed to get notes'); + } + } catch (error) { + console.error('Error getting notes:', error); + showToast(error.message || 'Failed to get notes', 'error'); + } + } + ); + + // New option: Copy Trigger Words with tag icon + const copyTriggerWordsOption = createMenuItem( + 'Copy Trigger Words', + '', + async () => { + menu.remove(); + document.removeEventListener('click', closeMenu); + + try { + // Get trigger words from API + const response = await api.fetchApi(`/loras/get-trigger-words?name=${encodeURIComponent(loraName)}`, { + method: 'GET' + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Failed to get trigger words'); + } + + const data = await response.json(); + if (data.success) { + const triggerWords = data.trigger_words || []; + if (triggerWords.length > 0) { + // Join trigger words with commas + const triggerWordsText = triggerWords.join(', '); + await copyToClipboard(triggerWordsText, 'Trigger words copied to clipboard'); + } else { + showToast('No trigger words available for this LoRA', 'info'); + } + } else { + throw new Error(data.error || 'Failed to get trigger words'); + } + } catch (error) { + console.error('Error getting trigger words:', error); + showToast(error.message || 'Failed to get trigger words', 'error'); + } + } + ); + // Save recipe option with bookmark icon const saveOption = createMenuItem( 'Save Recipe', @@ -233,15 +291,25 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render ); // Add separator - const separator = document.createElement('div'); - Object.assign(separator.style, { + const separator1 = document.createElement('div'); + Object.assign(separator1.style, { + margin: '4px 0', + borderTop: '1px solid rgba(255, 255, 255, 0.1)', + }); + + // Add second separator + const separator2 = document.createElement('div'); + Object.assign(separator2.style, { margin: '4px 0', borderTop: '1px solid rgba(255, 255, 255, 0.1)', }); menu.appendChild(viewOnCivitaiOption); menu.appendChild(deleteOption); - menu.appendChild(separator); + menu.appendChild(separator1); + menu.appendChild(copyNotesOption); + menu.appendChild(copyTriggerWordsOption); + menu.appendChild(separator2); menu.appendChild(saveOption); document.body.appendChild(menu); diff --git a/web/comfyui/loras_widget_utils.js b/web/comfyui/loras_widget_utils.js index 7eb662f8..d5dc30df 100644 --- a/web/comfyui/loras_widget_utils.js +++ b/web/comfyui/loras_widget_utils.js @@ -58,6 +58,7 @@ export function syncClipStrengthIfCollapsed(loraData) { export async function saveRecipeDirectly() { try { const prompt = await app.graphToPrompt(); + console.log('Prompt:', prompt); // for debugging purposes // Show loading toast if (app && app.extensionManager && app.extensionManager.toast) { app.extensionManager.toast.add({ @@ -107,3 +108,59 @@ export async function saveRecipeDirectly() { } } } + +/** + * 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; + } +} + +/** + * Show a toast notification + * @param {string} message - The message to display + * @param {string} type - The type of toast (success, error, info, warning) + */ +export function showToast(message, type = 'info') { + if (app && app.extensionManager && app.extensionManager.toast) { + app.extensionManager.toast.add({ + severity: type, + summary: type.charAt(0).toUpperCase() + type.slice(1), + detail: message, + life: 3000 + }); + } else { + console.log(`${type.toUpperCase()}: ${message}`); + // Fallback alert for critical errors only + if (type === 'error') { + alert(message); + } + } +}