From 0069f8463011414ee8a9fc778bce37525f5c32c9 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 10 Mar 2025 00:20:31 +0800 Subject: [PATCH] Add model description in lora details --- py/routes/api_routes.py | 59 +++++++ py/services/civitai_client.py | 35 +++++ static/css/components/lora-modal.css | 220 ++++++++++++++++++++++++++ static/js/api/loraApi.js | 15 ++ static/js/components/LoraModal.js | 224 ++++++++++++++++++++------- 5 files changed, 495 insertions(+), 58 deletions(-) diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 01666146..49d760dd 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -41,6 +41,7 @@ class ApiRoutes: app.router.add_post('/api/download-lora', routes.download_lora) app.router.add_post('/api/settings', routes.update_settings) app.router.add_post('/api/move_model', routes.move_model) + app.router.add_get('/api/lora-model-description', routes.get_lora_model_description) # Add new route app.router.add_post('/loras/api/save-metadata', routes.save_metadata) app.router.add_get('/api/lora-preview-url', routes.get_lora_preview_url) # Add new route app.router.add_post('/api/move_models_bulk', routes.move_models_bulk) @@ -691,3 +692,61 @@ class ApiRoutes: except Exception as e: logger.error(f"Error moving models in bulk: {e}", exc_info=True) return web.Response(text=str(e), status=500) + + async def get_lora_model_description(self, request: web.Request) -> web.Response: + """Get model description for a Lora model""" + try: + # Get parameters + model_id = request.query.get('model_id') + file_path = request.query.get('file_path') + + if not model_id: + return web.json_response({ + 'success': False, + 'error': 'Model ID is required' + }, status=400) + + # Check if we already have the description stored in metadata + description = None + if file_path: + metadata_path = os.path.splitext(file_path)[0] + '.metadata.json' + if os.path.exists(metadata_path): + try: + with open(metadata_path, 'r', encoding='utf-8') as f: + metadata = json.load(f) + description = metadata.get('modelDescription') + except Exception as e: + logger.error(f"Error loading metadata from {metadata_path}: {e}") + + # If description is not in metadata, fetch from CivitAI + if not description: + logger.info(f"Fetching model description for model ID: {model_id}") + description = await self.civitai_client.get_model_description(model_id) + + # Save the description to metadata if we have a file path and got a description + if file_path and description: + try: + metadata_path = os.path.splitext(file_path)[0] + '.metadata.json' + if os.path.exists(metadata_path): + with open(metadata_path, 'r', encoding='utf-8') as f: + metadata = json.load(f) + + metadata['modelDescription'] = description + + with open(metadata_path, 'w', encoding='utf-8') as f: + json.dump(metadata, f, indent=2, ensure_ascii=False) + logger.info(f"Saved model description to metadata for {file_path}") + except Exception as e: + logger.error(f"Error saving model description to metadata: {e}") + + return web.json_response({ + 'success': True, + 'description': description or "

No model description available.

" + }) + + except Exception as e: + logger.error(f"Error getting model description: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/py/services/civitai_client.py b/py/services/civitai_client.py index ae08e56c..a264d45b 100644 --- a/py/services/civitai_client.py +++ b/py/services/civitai_client.py @@ -163,6 +163,41 @@ class CivitaiClient: logger.error(f"Error fetching model version info: {e}") return None + async def get_model_description(self, model_id: str) -> Optional[str]: + """Fetch the model description from Civitai API + + Args: + model_id: The Civitai model ID + + Returns: + Optional[str]: The model description HTML or None if not found + """ + try: + session = await self.session + headers = self._get_request_headers() + url = f"{self.base_url}/models/{model_id}" + + logger.info(f"Fetching model description from {url}") + + async with session.get(url, headers=headers) as response: + if response.status != 200: + logger.warning(f"Failed to fetch model description: Status {response.status}") + return None + + data = await response.json() + description = data.get('description') + + if description: + logger.info(f"Successfully retrieved description for model {model_id}") + return description + else: + logger.warning(f"No description found for model {model_id}") + return None + + except Exception as e: + logger.error(f"Error fetching model description: {e}", exc_info=True) + return None + async def close(self): """Close the session if it exists""" if self._session is not None: diff --git a/static/css/components/lora-modal.css b/static/css/components/lora-modal.css index 0e2f16a9..8cb990fb 100644 --- a/static/css/components/lora-modal.css +++ b/static/css/components/lora-modal.css @@ -479,4 +479,224 @@ /* Ensure close button is accessible */ .modal-content .close { z-index: 10; /* Ensure close button is above other elements */ +} + +/* Tab System Styling */ +.showcase-tabs { + display: flex; + border-bottom: 1px solid var(--lora-border); + margin-bottom: var(--space-2); + position: relative; + z-index: 2; +} + +.tab-btn { + padding: var(--space-1) var(--space-2); + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-color); + cursor: pointer; + font-size: 0.95em; + transition: all 0.2s; + opacity: 0.7; + position: relative; +} + +.tab-btn:hover { + opacity: 1; + background: oklch(var(--lora-accent) / 0.05); +} + +.tab-btn.active { + border-bottom: 2px solid var(--lora-accent); + opacity: 1; + font-weight: 600; +} + +.tab-content { + position: relative; + min-height: 100px; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} + +/* Model Description Styling */ +.model-description-container { + background: var(--lora-surface); + border-radius: var(--border-radius-sm); + overflow: hidden; + min-height: 200px; + position: relative; +} + +.model-description-loading { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-3); + color: var(--text-color); + opacity: 0.7; + font-size: 0.9em; +} + +.model-description-loading .fa-spinner { + margin-right: var(--space-1); +} + +.model-description-content { + padding: var(--space-2); + line-height: 1.5; + overflow-wrap: break-word; +} + +.model-description-content img { + max-width: 100%; + height: auto; +} + +.model-description-content pre { + background: rgba(0, 0, 0, 0.05); + border-radius: var(--border-radius-xs); + padding: var(--space-1); + white-space: pre-wrap; +} + +.model-description-content code { + font-family: monospace; +} + +.model-description-content hr { + border: 0; + border-top: 1px solid var(--lora-border); + margin: var(--space-2) 0; +} + +.model-description-content a { + color: var(--lora-accent); + text-decoration: none; +} + +.model-description-content a:hover { + text-decoration: underline; +} + +/* Adjust dark mode for model description */ +[data-theme="dark"] .model-description-content pre { + background: rgba(255, 255, 255, 0.05); +} + +.hidden { + display: none !important; +} + +.error-message { + color: var(--lora-error); + text-align: center; + padding: var(--space-2); +} + +.no-examples { + text-align: center; + padding: var(--space-3); + color: var(--text-color); + opacity: 0.7; +} + +/* Adjust the media wrapper for tab system */ +#showcase-tab .carousel-container { + margin-top: var(--space-2); +} + +/* Enhanced Model Description Styling */ +.model-description-container { + background: var(--lora-surface); + border-radius: var(--border-radius-sm); + overflow: hidden; + min-height: 200px; + position: relative; + /* Remove the max-height and overflow-y to allow content to expand naturally */ +} + +.model-description-content { + padding: var(--space-2); + line-height: 1.5; + overflow-wrap: break-word; + font-size: 0.95em; +} + +.model-description-content h1, +.model-description-content h2, +.model-description-content h3, +.model-description-content h4, +.model-description-content h5, +.model-description-content h6 { + margin-top: 1em; + margin-bottom: 0.5em; + font-weight: 600; +} + +.model-description-content p { + margin-bottom: 1em; +} + +.model-description-content img { + max-width: 100%; + height: auto; + border-radius: var(--border-radius-xs); + display: block; + margin: 1em 0; +} + +.model-description-content pre { + background: rgba(0, 0, 0, 0.05); + border-radius: var(--border-radius-xs); + padding: var(--space-1); + white-space: pre-wrap; + margin: 1em 0; + overflow-x: auto; +} + +.model-description-content code { + font-family: monospace; + font-size: 0.9em; + background: rgba(0, 0, 0, 0.05); + padding: 0.1em 0.3em; + border-radius: 3px; +} + +.model-description-content pre code { + background: transparent; + padding: 0; +} + +.model-description-content ul, +.model-description-content ol { + margin-left: 1.5em; + margin-bottom: 1em; +} + +.model-description-content li { + margin-bottom: 0.5em; +} + +.model-description-content blockquote { + border-left: 3px solid var(--lora-accent); + padding-left: 1em; + margin-left: 0; + margin-right: 0; + font-style: italic; + opacity: 0.8; +} + +/* Adjust dark mode for model description */ +[data-theme="dark"] .model-description-content pre, +[data-theme="dark"] .model-description-content code { + background: rgba(255, 255, 255, 0.05); } \ No newline at end of file diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 1ac6f012..132923f1 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -325,4 +325,19 @@ export async function refreshSingleLoraMetadata(filePath) { state.loadingManager.hide(); state.loadingManager.restoreProgressBar(); } +} + +export async function fetchModelDescription(modelId, filePath) { + try { + const response = await fetch(`/api/lora-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`); + + if (!response.ok) { + throw new Error(`Failed to fetch model description: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching model description:', error); + throw error; + } } \ No newline at end of file diff --git a/static/js/components/LoraModal.js b/static/js/components/LoraModal.js index 6e9653d1..ac55487d 100644 --- a/static/js/components/LoraModal.js +++ b/static/js/components/LoraModal.js @@ -81,10 +81,35 @@ export function showLoraModal(lora) {
${lora.description || 'N/A'}
- - ${renderShowcaseImages(lora.civitai.images)} +
+
+ + +
+ +
+
+ ${renderShowcaseContent(lora.civitai?.images)} +
+ +
+
+
+ Loading model description... +
+
+ ${lora.modelDescription || ''} +
+
+
+
+ + +
`; @@ -92,6 +117,143 @@ export function showLoraModal(lora) { modalManager.showModal('loraModal', content); setupEditableFields(); setupShowcaseScroll(); + setupTabSwitching(); + + // If we have a model ID but no description, fetch it + if (lora.civitai?.modelId && !lora.modelDescription) { + loadModelDescription(lora.civitai.modelId, lora.file_path); + } +} + +// Function to render showcase content +function renderShowcaseContent(images) { + if (!images?.length) return '
No example images available
'; + + return ` +
+ + Scroll or click to show ${images.length} examples +
+ + `; +} + +// New function to handle tab switching +function setupTabSwitching() { + const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn'); + + tabButtons.forEach(button => { + button.addEventListener('click', () => { + // Remove active class from all tabs + document.querySelectorAll('.showcase-tabs .tab-btn').forEach(btn => + btn.classList.remove('active') + ); + document.querySelectorAll('.tab-content .tab-pane').forEach(tab => + tab.classList.remove('active') + ); + + // Add active class to clicked tab + button.classList.add('active'); + const tabId = `${button.dataset.tab}-tab`; + document.getElementById(tabId).classList.add('active'); + + // If switching to description tab, make sure content is properly sized + if (button.dataset.tab === 'description') { + const descriptionContent = document.querySelector('.model-description-content'); + if (descriptionContent && descriptionContent.innerHTML.trim() !== '') { + document.querySelector('.model-description-loading')?.classList.add('hidden'); + } + } + }); + }); +} + +// New function to load model description +async function loadModelDescription(modelId, filePath) { + try { + const descriptionContainer = document.querySelector('.model-description-content'); + const loadingElement = document.querySelector('.model-description-loading'); + + if (!descriptionContainer || !loadingElement) return; + + // Show loading indicator + loadingElement.classList.remove('hidden'); + descriptionContainer.classList.add('hidden'); + + // Try to get model description from API + const response = await fetch(`/api/lora-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`); + + if (!response.ok) { + throw new Error(`Failed to fetch model description: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.success && data.description) { + // Update the description content + descriptionContainer.innerHTML = data.description; + + // Process any links in the description to open in new tab + const links = descriptionContainer.querySelectorAll('a'); + links.forEach(link => { + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + }); + + // Show the description and hide loading indicator + descriptionContainer.classList.remove('hidden'); + loadingElement.classList.add('hidden'); + } else { + throw new Error(data.error || 'No description available'); + } + } catch (error) { + console.error('Error loading model description:', error); + const loadingElement = document.querySelector('.model-description-loading'); + if (loadingElement) { + loadingElement.innerHTML = `
Failed to load model description. ${error.message}
`; + } + } } // 添加复制文件名的函数 @@ -350,61 +512,7 @@ function renderTriggerWords(words) { } function renderShowcaseImages(images) { - if (!images?.length) return ''; - - return ` -
-
- - Scroll or click to show ${images.length} examples -
- - -
- `; + return renderShowcaseContent(images); } export function toggleShowcase(element) { @@ -558,4 +666,4 @@ function formatFileSize(bytes) { } return `${size.toFixed(1)} ${units[unitIndex]}`; -} \ No newline at end of file +} \ No newline at end of file