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);
+ }
+ }
+}