diff --git a/eslint.config.mjs b/eslint.config.mjs index c4ab17c..f2afb26 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -82,6 +82,7 @@ export default [ FLASHCARD_LABELS: 'readonly', BOOKMARK_LABELS: 'readonly', RESUME_LABELS: 'readonly', + DASHBOARD_LABELS: 'readonly', TOC_LABELS: 'readonly', MENU_LABELS: 'readonly', PDF_EXPORT_LABELS: 'readonly', diff --git a/manifest.json b/manifest.json index 8888d02..cc49235 100644 --- a/manifest.json +++ b/manifest.json @@ -48,6 +48,7 @@ "src/content/chat-flashcards.js", "src/content/bookmarks.js", "src/content/resume.js", + "src/content/dashboard.js", "src/content/reading-aid.js", "src/content/keyboard-shortcuts.js" ], diff --git a/src/content/bookmarks.js b/src/content/bookmarks.js index 20b84f2..4b5c0c5 100644 --- a/src/content/bookmarks.js +++ b/src/content/bookmarks.js @@ -125,7 +125,7 @@ } // Another sub-panel may be open; restore the chat first so savedChatHTML // captures the chat, not the other panel. - if (_state.historyPanelOpen || _state.flashcardPanelOpen || _state.recentPanelOpen) { + if (_state.historyPanelOpen || _state.flashcardPanelOpen || _state.recentPanelOpen || _state.dashboardPanelOpen) { sb._chat.closeSubPanel(); } diff --git a/src/content/chat-flashcards.js b/src/content/chat-flashcards.js index 9b849a4..bf35eb5 100644 --- a/src/content/chat-flashcards.js +++ b/src/content/chat-flashcards.js @@ -96,7 +96,7 @@ } // Close history if open — they share `savedChatHTML`, so closing first // restores the chat panel before we save it again. - if (_state.historyPanelOpen || _state.bookmarksPanelOpen || _state.recentPanelOpen) { + if (_state.historyPanelOpen || _state.bookmarksPanelOpen || _state.recentPanelOpen || _state.dashboardPanelOpen) { sb._chat.closeSubPanel(); } diff --git a/src/content/chat-history.js b/src/content/chat-history.js index 142962d..079a15e 100644 --- a/src/content/chat-history.js +++ b/src/content/chat-history.js @@ -185,7 +185,7 @@ sb._chat.closeSubPanel(); return; } - if (state.flashcardPanelOpen || state.bookmarksPanelOpen || state.recentPanelOpen) { + if (state.flashcardPanelOpen || state.bookmarksPanelOpen || state.recentPanelOpen || state.dashboardPanelOpen) { sb._chat.closeSubPanel(); } diff --git a/src/content/content.css b/src/content/content.css index 409cb65..58fa62f 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -3008,3 +3008,50 @@ html.si18n-dark .si18n-tp-en { html.si18n-dark .si18n-tp-tr { color: #888; } + +/* ============================================================ + LOCAL PROGRESS DASHBOARD (Tools ▸ My progress) + ============================================================ */ + +.si18n-dash-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + padding: 4px 0 12px; +} + +.si18n-dash-stat { + background: var(--si18n-bg); + border: 1px solid var(--si18n-border); + border-radius: 10px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.si18n-dash-num { + font-size: 18px; + font-weight: 700; + color: var(--si18n-accent); +} + +.si18n-dash-label { + font-size: 11px; + color: var(--si18n-text-secondary); +} + +.si18n-dash-recent-title { + font-size: 12px; + font-weight: 600; + color: var(--si18n-text-secondary); + margin: 6px 0 4px; +} + +html.si18n-dark .si18n-dash-stat { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.14); +} +html.si18n-dark .si18n-dash-num { + color: #aab0e0; +} diff --git a/src/content/dashboard.js b/src/content/dashboard.js new file mode 100644 index 0000000..330d927 --- /dev/null +++ b/src/content/dashboard.js @@ -0,0 +1,115 @@ +/** + * SkillBridge — Local Progress Dashboard + * + * A Tools-menu sub-panel aggregating the learner's LOCAL data only: + * recent lessons (sb_recent), bookmarks (sb_bookmarks), and flashcard + * progress (fc__ Leitner boxes). Everything is read from + * chrome.storage.local — nothing leaves the device, consistent with the + * privacy policy ("local storage only"). + * + * Loaded after sidebar-chat.js (uses sb._chat.state / closeSubPanel) and + * after resume/bookmarks/chat-flashcards (whose stores it reads). + */ + +(function () { + 'use strict'; + + const sb = window._sb; + if (!sb) { + console.warn('[SkillBridge] dashboard: _sb not ready'); + return; + } + const _state = sb._chat.state; + + function collectStats(cb) { + chrome.storage.local.get(null, (all) => { + all = all || {}; + const recent = Array.isArray(all.sb_recent) ? all.sb_recent : []; + const bookmarks = Array.isArray(all.sb_bookmarks) ? all.sb_bookmarks : []; + let decks = 0; + let tracked = 0; + let mastered = 0; + for (const [key, val] of Object.entries(all)) { + if (!key.startsWith('fc_') || !val || typeof val !== 'object') continue; + const boxes = val.boxes && typeof val.boxes === 'object' ? val.boxes : {}; + const terms = Object.keys(boxes); + if (terms.length === 0) continue; + decks++; + tracked += terms.length; + for (const t of terms) if (Number(boxes[t]) >= 3) mastered++; + } + cb({ recent, bookmarks, decks, tracked, mastered }); + }); + } + + function statRow(value, label) { + return ` +
+ ${value} + ${sb.escapeHtml(label)} +
`; + } + + function render(stats) { + const list = sb.$id('si18n-dash-body'); + if (!list) return; + const L = (m) => sb.t(m) || ''; + const recentRows = stats.recent + .slice(0, 3) + .map( + (r) => ` +
+ +
`, + ) + .join(''); + list.innerHTML = ` +
+ ${statRow(stats.recent.length, L(DASHBOARD_LABELS.lessons))} + ${statRow(stats.bookmarks.length, L(DASHBOARD_LABELS.bookmarks))} + ${statRow(stats.decks, L(DASHBOARD_LABELS.decks))} + ${statRow(`${stats.mastered}/${stats.tracked}`, L(DASHBOARD_LABELS.mastered))} +
+ ${stats.recent.length ? `
${sb.escapeHtml(L(RESUME_LABELS.title))}
${recentRows}` : `
${sb.escapeHtml(L(DASHBOARD_LABELS.empty))}
`} + `; + list.querySelectorAll('.si18n-dash-open').forEach((btn) => { + btn.addEventListener('click', () => { + const url = btn.dataset.url; + if (url && /^https?:/i.test(url)) window.location.href = url; + }); + }); + } + + function toggleDashboardPanel() { + const chatPanel = sb.$id('si18n-panel-chat'); + if (!chatPanel) return; + + if (_state.dashboardPanelOpen) { + sb._chat.closeSubPanel(); + return; + } + if (_state.historyPanelOpen || _state.flashcardPanelOpen || _state.bookmarksPanelOpen || _state.recentPanelOpen) { + sb._chat.closeSubPanel(); + } + + _state.dashboardPanelOpen = true; + _state.savedChatHTML = chatPanel.innerHTML; + chatPanel.replaceChildren(); + chatPanel.insertAdjacentHTML( + 'afterbegin', + ` +
+ + ${sb.t(DASHBOARD_LABELS.title)} +
+
+ `, + ); + sb.$id('si18n-dash-back')?.addEventListener('click', () => sb._chat.closeSubPanel()); + collectStats(render); + } + + sb._chat.toggleDashboardPanel = toggleDashboardPanel; +})(); diff --git a/src/content/resume.js b/src/content/resume.js index 4b647f4..fb67613 100644 --- a/src/content/resume.js +++ b/src/content/resume.js @@ -215,14 +215,19 @@ // ============================================================ function toggleRecentPanel() { - const chatPanel = document.getElementById('si18n-panel-chat'); + const chatPanel = sb.$id('si18n-panel-chat'); if (!chatPanel) return; if (_state.recentPanelOpen) { sb._chat.closeSubPanel(); return; } - if (_state.historyPanelOpen || _state.flashcardPanelOpen || _state.bookmarksPanelOpen) { + if ( + _state.historyPanelOpen || + _state.flashcardPanelOpen || + _state.bookmarksPanelOpen || + _state.dashboardPanelOpen + ) { sb._chat.closeSubPanel(); } @@ -241,7 +246,7 @@ `, ); - document.getElementById('si18n-recent-back')?.addEventListener('click', () => sb._chat.closeSubPanel()); + sb.$id('si18n-recent-back')?.addEventListener('click', () => sb._chat.closeSubPanel()); loadRecent(renderList); } @@ -263,7 +268,7 @@ } function renderList() { - const list = document.getElementById('si18n-recent-list'); + const list = sb.$id('si18n-recent-list'); if (!list) return; list.replaceChildren(); list.insertAdjacentHTML('afterbegin', rowsHTML()); diff --git a/src/content/sidebar-chat.js b/src/content/sidebar-chat.js index 04765a6..8afe6eb 100644 --- a/src/content/sidebar-chat.js +++ b/src/content/sidebar-chat.js @@ -23,6 +23,7 @@ flashcardPanelOpen: false, bookmarksPanelOpen: false, recentPanelOpen: false, + dashboardPanelOpen: false, }; const _state = sb._chat.state; @@ -240,6 +241,10 @@