diff --git a/locales/de.json b/locales/de.json index 95365829..db847811 100644 --- a/locales/de.json +++ b/locales/de.json @@ -2028,7 +2028,8 @@ "settings": "Einstellungen & Konfiguration", "extensions": "Erweiterungen", "newBadge": "NEU" - } + }, + "newContentBadge": "NEU" }, "update": { "title": "Nach Updates suchen", diff --git a/locales/en.json b/locales/en.json index 57edd93a..cd684e19 100644 --- a/locales/en.json +++ b/locales/en.json @@ -2028,7 +2028,8 @@ "settings": "Settings & Configuration", "extensions": "Extensions", "newBadge": "NEW" - } + }, + "newContentBadge": "New" }, "update": { "title": "Check for Updates", diff --git a/locales/es.json b/locales/es.json index 7303f116..3050e768 100644 --- a/locales/es.json +++ b/locales/es.json @@ -2028,7 +2028,8 @@ "settings": "Configuración", "extensions": "Extensiones", "newBadge": "NUEVO" - } + }, + "newContentBadge": "NUEVO" }, "update": { "title": "Comprobar actualizaciones", diff --git a/locales/fr.json b/locales/fr.json index 8bdca5bc..cfa6c535 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -2028,7 +2028,8 @@ "settings": "Paramètres & Configuration", "extensions": "Extensions", "newBadge": "NOUVEAU" - } + }, + "newContentBadge": "NOUVEAU" }, "update": { "title": "Vérifier les mises à jour", diff --git a/locales/he.json b/locales/he.json index e3700643..093a47ab 100644 --- a/locales/he.json +++ b/locales/he.json @@ -2028,7 +2028,8 @@ "settings": "הגדרות ותצורה", "extensions": "הרחבות", "newBadge": "חדש" - } + }, + "newContentBadge": "חדש" }, "update": { "title": "בדוק עדכונים", diff --git a/locales/ja.json b/locales/ja.json index a3823fc9..e8370aa8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -2028,7 +2028,8 @@ "settings": "設定&構成", "extensions": "拡張機能", "newBadge": "新着" - } + }, + "newContentBadge": "新着" }, "update": { "title": "更新確認", diff --git a/locales/ko.json b/locales/ko.json index 62cb289d..4d827d77 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -2028,7 +2028,8 @@ "settings": "설정 & 구성", "extensions": "확장", "newBadge": "신규" - } + }, + "newContentBadge": "신규" }, "update": { "title": "업데이트 확인", diff --git a/locales/ru.json b/locales/ru.json index cd686bca..7a18a688 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -2028,7 +2028,8 @@ "settings": "Настройки и конфигурация", "extensions": "Расширения", "newBadge": "НОВОЕ" - } + }, + "newContentBadge": "НОВОЕ" }, "update": { "title": "Проверить обновления", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 6cd02def..877a0839 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -2028,7 +2028,8 @@ "settings": "设置与配置", "extensions": "扩展", "newBadge": "新" - } + }, + "newContentBadge": "新" }, "update": { "title": "检查更新", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index cf7bbef0..366f8d73 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -2028,7 +2028,8 @@ "settings": "設定與配置", "extensions": "擴充功能", "newBadge": "新" - } + }, + "newContentBadge": "新" }, "update": { "title": "檢查更新", diff --git a/static/css/components/modal/help-modal.css b/static/css/components/modal/help-modal.css index 9c6d91f8..2ae44a2e 100644 --- a/static/css/components/modal/help-modal.css +++ b/static/css/components/modal/help-modal.css @@ -167,6 +167,29 @@ box-shadow: var(--shadow-lg); } +/* Replay Tutorial button: badge hidden until the button is flagged as new content */ +.replay-tutorial-btn .new-content-badge { + display: none; + background-color: rgba(255, 255, 255, 0.22); + color: #fff; + box-shadow: none; + margin-left: 2px; +} + +.replay-tutorial-btn.has-new-content .new-content-badge { + display: inline-flex; +} + +/* One-time attention pulse when the button is flagged as new content */ +@keyframes new-content-glow { + 0% { box-shadow: 0 0 0 0 oklch(from var(--lora-accent) l c h / 55%); } + 100% { box-shadow: 0 0 0 16px transparent; } +} + +.replay-tutorial-btn.has-new-content { + animation: new-content-glow 1.2s ease-out 3; +} + /* Update video list styles */ .video-list { display: flex; diff --git a/static/js/managers/HelpManager.js b/static/js/managers/HelpManager.js index bd33dadc..b3097166 100644 --- a/static/js/managers/HelpManager.js +++ b/static/js/managers/HelpManager.js @@ -6,19 +6,11 @@ import { onboardingManager } from './OnboardingManager.js'; */ export class HelpManager { constructor() { - this.lastViewedTimestamp = getStorageItem('help_last_viewed', 0); - this.latestContentTimestamp = new Date('2025-10-11').getTime(); // Will be updated from server or config + // Version of the help content the user has seen. Compared against the + // data-help-content-version marker rendered into the help modal markup, + // so badge state is always derived from the content actually served. + this.viewedContentVersion = getStorageItem('help_viewed_content_version', null); this.isInitialized = false; - - // Default latest content data - could be fetched from server - this.latestVideoData = { - timestamp: new Date('2024-06-09').getTime(), // Default timestamp - walkthrough: { - id: 'hvKw31YpE-U', - title: 'Getting Started with LoRA Manager' - }, - playlistUpdated: true - }; } /** @@ -35,9 +27,6 @@ export class HelpManager { // Check if we need to show the badge this.updateHelpBadge(); - // Fetch latest video data (could be implemented to fetch from remote source) - this.fetchLatestVideoData(); - this.isInitialized = true; return this; } @@ -122,58 +111,81 @@ export class HelpManager { */ openHelpModal(tabId) { // Use modalManager to open the help modal - if (window.modalManager) { - window.modalManager.toggleModal('helpModal'); + if (!window.modalManager) return; - if (tabId) { - this.activateHelpTab(tabId); - } - - // Add visual indicator to Documentation tab if there's new content - this.updateDocumentationTabIndicator(); - - // Update the last viewed timestamp - this.markContentAsViewed(); - - // Hide the badge - this.hideHelpBadge(); + const hadNewContent = this.hasNewContent(); + + window.modalManager.toggleModal('helpModal'); + + if (tabId) { + this.activateHelpTab(tabId); } + + // Only acknowledge the content as viewed when the user opened the + // modal while it actually contained new content. Opening a stale + // (pre-upgrade) page must not suppress the badge after a refresh. + if (hadNewContent) { + this.updateNewContentTabIndicators(); + this.markContentAsViewed(); + } + + // Hide the badge + this.hideHelpBadge(); } /** - * Add visual indicator to Documentation tab for new content + * Add visual indicator to tabs that received new content */ - updateDocumentationTabIndicator() { - const docTab = document.querySelector('.tab-btn[data-tab="documentation"]'); - if (docTab && this.hasNewContent()) { - docTab.classList.add('has-new-content'); + updateNewContentTabIndicators() { + if (!this.hasNewContent()) return; + + // Tabs updated in the 2026-09-03 discoverability release: + // getting-started (Replay Tutorial button) and shortcuts (new cheat-sheet tab) + const NEW_CONTENT_TABS = ['getting-started', 'shortcuts']; + NEW_CONTENT_TABS.forEach(tabId => { + const tab = document.querySelector(`.help-tabs .tab-btn[data-tab="${tabId}"]`); + if (tab) { + tab.classList.add('has-new-content'); + } + }); + + // Point the indicator at the specific new element inside the + // Getting Started tab, and scroll it into view so it is not lost + // below the fold of the modal body. + const replayBtn = document.getElementById('replayTutorialBtn'); + if (replayBtn) { + replayBtn.classList.add('has-new-content'); + const gettingStartedActive = document.querySelector('#getting-started.tab-pane.active'); + if (gettingStartedActive && typeof replayBtn.scrollIntoView === 'function') { + replayBtn.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } } } /** - * Mark content as viewed by saving current timestamp + * Mark content as viewed by persisting the version rendered in the DOM. + * No-op when the served markup carries no version marker (stale assets), + * so viewing old content never suppresses the badge for new content. */ markContentAsViewed() { - this.lastViewedTimestamp = Date.now(); - setStorageItem('help_last_viewed', this.lastViewedTimestamp); + const currentVersion = this.getCurrentContentVersion(); + if (!currentVersion) return; + + this.viewedContentVersion = currentVersion; + setStorageItem('help_viewed_content_version', this.viewedContentVersion); } - + /** - * Fetch latest video data (could be implemented to actually fetch from a remote source) + * Read the help content version from the rendered modal markup + * @returns {string|null} Version marker, or null if the served markup has none */ - fetchLatestVideoData() { - // In a real implementation, you'd fetch this from your server - // For now, we'll just use the hardcoded data from constructor - - // Update the timestamp with the latest data - this.latestContentTimestamp = Math.max(this.latestContentTimestamp, this.latestVideoData.timestamp); - - // Check again if we need to show the badge with this new data - this.updateHelpBadge(); + getCurrentContentVersion() { + const marker = document.querySelector('[data-help-content-version]'); + return marker ? marker.getAttribute('data-help-content-version') : null; } - + /** - * Update help badge visibility based on timestamps + * Update help badge visibility based on viewed vs. served content version */ updateHelpBadge() { if (this.hasNewContent()) { @@ -182,13 +194,13 @@ export class HelpManager { this.hideHelpBadge(); } } - + /** - * Check if there's new content the user hasn't seen + * Check if the served help content is newer than what the user has viewed */ hasNewContent() { - // If user has never viewed the help, or the content is newer than last viewed - return this.lastViewedTimestamp === 0 || this.latestContentTimestamp > this.lastViewedTimestamp; + const currentVersion = this.getCurrentContentVersion(); + return Boolean(currentVersion) && currentVersion !== this.viewedContentVersion; } /** diff --git a/templates/components/modals/help_modal.html b/templates/components/modals/help_modal.html index 7a8303d1..20200f87 100644 --- a/templates/components/modals/help_modal.html +++ b/templates/components/modals/help_modal.html @@ -1,5 +1,5 @@ -