mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 23:25:43 -03:00
Add model description in lora details
This commit is contained in:
@@ -41,6 +41,7 @@ class ApiRoutes:
|
|||||||
app.router.add_post('/api/download-lora', routes.download_lora)
|
app.router.add_post('/api/download-lora', routes.download_lora)
|
||||||
app.router.add_post('/api/settings', routes.update_settings)
|
app.router.add_post('/api/settings', routes.update_settings)
|
||||||
app.router.add_post('/api/move_model', routes.move_model)
|
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_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_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)
|
app.router.add_post('/api/move_models_bulk', routes.move_models_bulk)
|
||||||
@@ -691,3 +692,61 @@ class ApiRoutes:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error moving models in bulk: {e}", exc_info=True)
|
logger.error(f"Error moving models in bulk: {e}", exc_info=True)
|
||||||
return web.Response(text=str(e), status=500)
|
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 "<p>No model description available.</p>"
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -163,6 +163,41 @@ class CivitaiClient:
|
|||||||
logger.error(f"Error fetching model version info: {e}")
|
logger.error(f"Error fetching model version info: {e}")
|
||||||
return None
|
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):
|
async def close(self):
|
||||||
"""Close the session if it exists"""
|
"""Close the session if it exists"""
|
||||||
if self._session is not None:
|
if self._session is not None:
|
||||||
|
|||||||
@@ -479,4 +479,224 @@
|
|||||||
/* Ensure close button is accessible */
|
/* Ensure close button is accessible */
|
||||||
.modal-content .close {
|
.modal-content .close {
|
||||||
z-index: 10; /* Ensure close button is above other elements */
|
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);
|
||||||
}
|
}
|
||||||
@@ -325,4 +325,19 @@ export async function refreshSingleLoraMetadata(filePath) {
|
|||||||
state.loadingManager.hide();
|
state.loadingManager.hide();
|
||||||
state.loadingManager.restoreProgressBar();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -81,10 +81,35 @@ export function showLoraModal(lora) {
|
|||||||
<div class="description-text">${lora.description || 'N/A'}</div>
|
<div class="description-text">${lora.description || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${renderShowcaseImages(lora.civitai.images)}
|
<div class="showcase-section" data-lora-id="${lora.civitai?.modelId || ''}">
|
||||||
|
<div class="showcase-tabs">
|
||||||
|
<button class="tab-btn active" data-tab="showcase">Examples</button>
|
||||||
|
<button class="tab-btn" data-tab="description">Model Description</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<div id="showcase-tab" class="tab-pane active">
|
||||||
|
${renderShowcaseContent(lora.civitai?.images)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="description-tab" class="tab-pane">
|
||||||
|
<div class="model-description-container">
|
||||||
|
<div class="model-description-loading">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
||||||
|
</div>
|
||||||
|
<div class="model-description-content">
|
||||||
|
${lora.modelDescription || ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="back-to-top" onclick="scrollToTop(this)">
|
||||||
|
<i class="fas fa-arrow-up"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -92,6 +117,143 @@ export function showLoraModal(lora) {
|
|||||||
modalManager.showModal('loraModal', content);
|
modalManager.showModal('loraModal', content);
|
||||||
setupEditableFields();
|
setupEditableFields();
|
||||||
setupShowcaseScroll();
|
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 '<div class="no-examples">No example images available</div>';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="scroll-indicator" onclick="toggleShowcase(this)">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
<span>Scroll or click to show ${images.length} examples</span>
|
||||||
|
</div>
|
||||||
|
<div class="carousel collapsed">
|
||||||
|
<div class="carousel-container">
|
||||||
|
${images.map(img => {
|
||||||
|
// 计算适当的展示高度:
|
||||||
|
// 1. 保持原始宽高比
|
||||||
|
// 2. 限制最大高度为视窗高度的60%
|
||||||
|
// 3. 确保最小高度为容器宽度的40%
|
||||||
|
const aspectRatio = (img.height / img.width) * 100;
|
||||||
|
const containerWidth = 800; // modal content的最大宽度
|
||||||
|
const minHeightPercent = 40; // 最小高度为容器宽度的40%
|
||||||
|
const maxHeightPercent = (window.innerHeight * 0.6 / containerWidth) * 100;
|
||||||
|
const heightPercent = Math.max(
|
||||||
|
minHeightPercent,
|
||||||
|
Math.min(maxHeightPercent, aspectRatio)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (img.type === 'video') {
|
||||||
|
return `
|
||||||
|
<div class="media-wrapper" style="padding-bottom: ${heightPercent}%">
|
||||||
|
<video controls autoplay muted loop crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer" data-src="${img.url}"
|
||||||
|
class="lazy">
|
||||||
|
<source data-src="${img.url}" type="video/mp4">
|
||||||
|
Your browser does not support video playback
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div class="media-wrapper" style="padding-bottom: ${heightPercent}%">
|
||||||
|
<img data-src="${img.url}"
|
||||||
|
alt="Preview"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
width="${img.width}"
|
||||||
|
height="${img.height}"
|
||||||
|
class="lazy">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = `<div class="error-message">Failed to load model description. ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加复制文件名的函数
|
// 添加复制文件名的函数
|
||||||
@@ -350,61 +512,7 @@ function renderTriggerWords(words) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderShowcaseImages(images) {
|
function renderShowcaseImages(images) {
|
||||||
if (!images?.length) return '';
|
return renderShowcaseContent(images);
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="showcase-section">
|
|
||||||
<div class="scroll-indicator" onclick="toggleShowcase(this)">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
<span>Scroll or click to show ${images.length} examples</span>
|
|
||||||
</div>
|
|
||||||
<div class="carousel collapsed">
|
|
||||||
<div class="carousel-container">
|
|
||||||
${images.map(img => {
|
|
||||||
// 计算适当的展示高度:
|
|
||||||
// 1. 保持原始宽高比
|
|
||||||
// 2. 限制最大高度为视窗高度的60%
|
|
||||||
// 3. 确保最小高度为容器宽度的40%
|
|
||||||
const aspectRatio = (img.height / img.width) * 100;
|
|
||||||
const containerWidth = 800; // modal content的最大宽度
|
|
||||||
const minHeightPercent = 40; // 最小高度为容器宽度的40%
|
|
||||||
const maxHeightPercent = (window.innerHeight * 0.6 / containerWidth) * 100;
|
|
||||||
const heightPercent = Math.max(
|
|
||||||
minHeightPercent,
|
|
||||||
Math.min(maxHeightPercent, aspectRatio)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (img.type === 'video') {
|
|
||||||
return `
|
|
||||||
<div class="media-wrapper" style="padding-bottom: ${heightPercent}%">
|
|
||||||
<video controls autoplay muted loop crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer" data-src="${img.url}"
|
|
||||||
class="lazy">
|
|
||||||
<source data-src="${img.url}" type="video/mp4">
|
|
||||||
Your browser does not support video playback
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return `
|
|
||||||
<div class="media-wrapper" style="padding-bottom: ${heightPercent}%">
|
|
||||||
<img data-src="${img.url}"
|
|
||||||
alt="Preview"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
width="${img.width}"
|
|
||||||
height="${img.height}"
|
|
||||||
class="lazy">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="back-to-top" onclick="scrollToTop(this)">
|
|
||||||
<i class="fas fa-arrow-up"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleShowcase(element) {
|
export function toggleShowcase(element) {
|
||||||
@@ -558,4 +666,4 @@ function formatFileSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user