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) {