mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 23:25:43 -03:00
Add API endpoints for retrieving LoRA notes and trigger words; enhance context menu with copy options. Supports #177
This commit is contained in:
@@ -72,6 +72,10 @@ class ApiRoutes:
|
|||||||
|
|
||||||
# Add new endpoint for letter counts
|
# Add new endpoint for letter counts
|
||||||
app.router.add_get('/api/loras/letter-counts', routes.get_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
|
# Add update check routes
|
||||||
UpdateRoutes.setup_routes(app)
|
UpdateRoutes.setup_routes(app)
|
||||||
@@ -1084,3 +1088,81 @@ class ApiRoutes:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, 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)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { createMenuItem } from "./loras_widget_components.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
|
// Function to handle strength adjustment via dragging
|
||||||
export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) {
|
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');
|
window.open(data.civitai_url, '_blank');
|
||||||
} else {
|
} else {
|
||||||
// Show error message if no Civitai URL
|
// Show error message if no Civitai URL
|
||||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
showToast('This LoRA has no associated Civitai URL', 'warning');
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting Civitai URL:', error);
|
console.error('Error getting Civitai URL:', error);
|
||||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
showToast(error.message || 'Failed to get Civitai URL', 'error');
|
||||||
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'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -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',
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>',
|
||||||
|
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',
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>',
|
||||||
|
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
|
// Save recipe option with bookmark icon
|
||||||
const saveOption = createMenuItem(
|
const saveOption = createMenuItem(
|
||||||
'Save Recipe',
|
'Save Recipe',
|
||||||
@@ -233,15 +291,25 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Add separator
|
// Add separator
|
||||||
const separator = document.createElement('div');
|
const separator1 = document.createElement('div');
|
||||||
Object.assign(separator.style, {
|
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',
|
margin: '4px 0',
|
||||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.appendChild(viewOnCivitaiOption);
|
menu.appendChild(viewOnCivitaiOption);
|
||||||
menu.appendChild(deleteOption);
|
menu.appendChild(deleteOption);
|
||||||
menu.appendChild(separator);
|
menu.appendChild(separator1);
|
||||||
|
menu.appendChild(copyNotesOption);
|
||||||
|
menu.appendChild(copyTriggerWordsOption);
|
||||||
|
menu.appendChild(separator2);
|
||||||
menu.appendChild(saveOption);
|
menu.appendChild(saveOption);
|
||||||
|
|
||||||
document.body.appendChild(menu);
|
document.body.appendChild(menu);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function syncClipStrengthIfCollapsed(loraData) {
|
|||||||
export async function saveRecipeDirectly() {
|
export async function saveRecipeDirectly() {
|
||||||
try {
|
try {
|
||||||
const prompt = await app.graphToPrompt();
|
const prompt = await app.graphToPrompt();
|
||||||
|
console.log('Prompt:', prompt); // for debugging purposes
|
||||||
// Show loading toast
|
// Show loading toast
|
||||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||||
app.extensionManager.toast.add({
|
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<boolean>} - 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user