From 600afdcd925678c38134c783ef627ddd81c8f68b Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sun, 26 Oct 2025 10:11:04 +0800 Subject: [PATCH] feat: add update badge to model modal versions tab - Add CSS styling for tab badges with update indicator animation - Include update_available flag in model data parsing - Display animated badge on versions tab when updates are available - Improve tab button layout with flexbox alignment and spacing --- .../css/components/lora-modal/lora-modal.css | 49 +++++++++++++++++++ static/js/components/shared/ModelCard.js | 1 + static/js/components/shared/ModelModal.js | 33 +++++++++---- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index 073617d5..a13a84b4 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -323,6 +323,10 @@ } .tab-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-1); padding: var(--space-1) var(--space-2); background: transparent; border: none; @@ -346,6 +350,51 @@ font-weight: 600; } +.tab-btn .tab-label { + display: inline-flex; + align-items: center; + line-height: 1.2; +} + +.tab-btn .tab-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: var(--border-radius-xs); + background: var(--badge-update-bg); + color: var(--badge-update-text); + font-size: 0.68em; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + box-shadow: 0 3px 10px var(--badge-update-glow); + border: 1px solid color-mix(in oklab, var(--badge-update-bg) 55%, transparent); + line-height: 1; +} + +.tab-badge--update { + animation: tab-badge-pulse 2.8s ease-in-out infinite; +} + +.tab-btn--has-update:not(.active) { + color: color-mix(in oklch, var(--text-color) 70%, var(--badge-update-bg) 30%); +} + +.tab-btn--has-update.active { + border-bottom-color: var(--badge-update-bg); +} + +@keyframes tab-badge-pulse { + 0%, 100% { + box-shadow: 0 3px 10px color-mix(in oklch, var(--badge-update-glow) 100%, transparent); + transform: translateY(0); + } + 50% { + box-shadow: 0 5px 14px color-mix(in oklch, var(--badge-update-glow) 90%, transparent); + transform: translateY(-1px); + } +} + .tab-content { position: relative; min-height: 100px; diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index d8b1cd5e..f53a61a2 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -287,6 +287,7 @@ async function showModelModalFromCard(card, modelType) { // Parse civitai metadata from the card's dataset civitai: JSON.parse(card.dataset.meta || '{}'), tags: JSON.parse(card.dataset.tags || '[]'), + update_available: card.dataset.update_available === 'true', modelDescription: card.dataset.modelDescription || '', // LoRA specific fields ...(modelType === MODEL_TYPES.LORA && { diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index b39b1a38..30164a8f 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -47,6 +47,7 @@ export async function showModelModal(model, modelType) { ...model, civitai: completeCivitaiData }; + const hasUpdateAvailable = Boolean(modelWithFullData.update_available); // Prepare LoRA specific data with complete civitai data const escapedWords = (modelType === 'loras' || modelType === 'embeddings') && modelWithFullData.civitai?.trainedWords?.length ? @@ -67,15 +68,27 @@ export async function showModelModal(model, modelType) { const descriptionText = translate('modals.model.tabs.description', {}, 'Model Description'); const recipesText = translate('modals.model.tabs.recipes', {}, 'Recipes'); const versionsText = translate('modals.model.tabs.versions', {}, 'Versions'); - + const versionsBadgeLabel = translate('modelCard.badges.update', {}, 'Update'); + const versionsTabBadge = hasUpdateAvailable + ? `${versionsBadgeLabel}` + : ''; + const versionsTabClasses = ['tab-btn']; + if (hasUpdateAvailable) { + versionsTabClasses.push('tab-btn--has-update'); + } + const versionsTabButton = ``.trim(); + const tabsContent = modelType === 'loras' ? ` - - ` : + ${versionsTabButton} + ` : ` - `; + ${versionsTabButton}`; const loadingExampleImagesText = translate('modals.model.loading.exampleImages', {}, 'Loading example images...'); const loadingDescriptionText = translate('modals.model.loading.description', {}, 'Loading model description...'); @@ -102,12 +115,6 @@ export async function showModelModal(model, modelType) { - -
-
- ${loadingRecipesText} -
-
@@ -115,6 +122,12 @@ export async function showModelModal(model, modelType) { ${loadingVersionsText}
+ + +
+
+ ${loadingRecipesText} +
` : `