From 201a5ec550e718341e114c753203f78f5ddb230a Mon Sep 17 00:00:00 2001 From: tshasan Date: Sun, 27 Apr 2025 20:01:34 -0700 Subject: [PATCH 01/20] Refactor CI workflow: remove commented-out steps and clean up linting section --- .github/workflows/ci.yml | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d35654..c947365 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,25 +26,4 @@ jobs: run: npm ci - name: Lint (ESLint) - run: npm run lint - - # We dont have any test yet - # - name: Run tests (Jest) - # run: npm test - - - name: Build (Webpack) - run: npm run build - - # This doesnt really work yet we need to figure this out - # - name: Package Firefox Extension - # run: npm run build:ext - - - name: Generate docs (JSDoc) - run: npm run docs - - # This doesnt work yet need to figure this out - # - name: Upload extension artifact - # uses: actions/upload-artifact@v3 - # with: - # name: firefox-recap-extension - # path: web-ext-artifacts/*.zip \ No newline at end of file + run: npm run lint \ No newline at end of file From b7f2d12384168eb9c2530b183211c11a14c2a526 Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 27 Apr 2025 21:56:42 -0700 Subject: [PATCH 02/20] manifest was not in dist --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index 9493fd3..dfc94fe 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,6 +30,7 @@ module.exports = { plugins: [ new CopyWebpackPlugin({ patterns: [ + { from: 'src/manifest.json', to: 'manifest.json' }, { from: 'src/popup/popup.html', to: 'popup.html' }, { from: 'src/popup/recap.html', to: 'recap.html' } ] From 87dedb3cad1999a5d71f9909e926002ffc9204cc Mon Sep 17 00:00:00 2001 From: tshasan Date: Sun, 27 Apr 2025 22:36:06 -0700 Subject: [PATCH 03/20] Update build command to overwrite destination and clean up background script handlers --- package.json | 2 +- src/background/background.js | 43 +++++++++++++++++++---------------- src/background/services/ml.js | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index b07ab3d..4b91ac0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev": "webpack --mode development", "build": "webpack --mode production", "test": "jest --env=jsdom", - "build:ext": "npm run build && web-ext build --source-dir ./dist/" + "build:ext": "npm run build && web-ext build --overwrite-dest --source-dir ./dist/" }, "repository": { "type": "git", diff --git a/src/background/background.js b/src/background/background.js index c35dab9..62b5c2a 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -8,7 +8,23 @@ */ import { initDB } from './initdb.js'; -import handlers from './handlers/index.js'; +import handlers from './handlers/index.js'; // Keep the default import for window assignment + +// Destructure the specific handlers needed for the message listener +const { + fetchAndStoreHistory, + getMostVisitedSites, + getVisitsPerHour, + getLabelCounts, + //getTimeSpentPerSite, + getCategoryTrends, + getCOCounts, + getDailyVisitCounts, + getRecencyFrequency, + getTransitionPatterns, + getUniqueWebsites +} = handlers; + /** * Initialize the extension’s database on startup. @@ -54,10 +70,11 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; } - if (action === "getTimeSpentPerSite") { - getTimeSpentPerSite(days, limit).then(sendResponse); - return true; - } + // Note: 'getTimeSpentPerSite' is not defined in handlers/index.js + // if (action === "getTimeSpentPerSite") { + // getTimeSpentPerSite(days, limit).then(sendResponse); + // return true; + // } if (action === "getCategoryTrends") { getCategoryTrends(days).then(sendResponse); @@ -91,19 +108,5 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { console.warn("[Background] No handler for action:", action); sendResponse(null); - return true; + return true; // Keep true here for async sendResponse }); - - -/** - * Expose background handler functions on the global `window` object. - * - * This allows you to call e.g. - * ``` - * getMostVisitedSites(7).then(console.log) - * ``` - * directly from the console for debugging or ad‐hoc testing. - * - * @type {Object.} - */ -Object.assign(window, handlers); diff --git a/src/background/services/ml.js b/src/background/services/ml.js index 16f5e25..33354f8 100644 --- a/src/background/services/ml.js +++ b/src/background/services/ml.js @@ -110,7 +110,7 @@ export async function classifyURLAndTitle( console.log('ML classify:', textToClassify); const result = await mlApi.runEngine({ args: [textToClassify], - options: { top_k: null }, + options: { top_k: null }, // mutli-label classification we apply threshold later this might be better at 2 }); const mapped = result From 7352edf14180f327c712a2848e11b3ba0e224469 Mon Sep 17 00:00:00 2001 From: tshasan Date: Sun, 27 Apr 2025 22:42:52 -0700 Subject: [PATCH 04/20] Update manifest.json: set background script to persistent and add browser-specific settings --- .DS_Store | Bin 6148 -> 0 bytes src/manifest.json | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index d871d82946bd2bb9d98f20038fb6b61a1c6ca179..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}N774E|#GP_Ty{JN%r}!s zHZ$L0vH>8QeR~V60W7JC_|jo6y01EutFSag(Qmx8!|ti?J1))@jcw546$3tx<5ze` z&y8n1;LT=xbX*OSlV(^j>H|Bb?WVk8n!J8+lyi@qe#t;GkPIXP$-qT35Vdi6ebKkb z!zTmDz;7_1??a&~*1*xxo(>My0uc2#-h^}OC5S~0#2Pp{(n1jrC3>htiy7V0B)68f4u2P?%yE5%%BDZV-KD>~ Date: Sun, 27 Apr 2025 22:51:48 -0700 Subject: [PATCH 05/20] Improve UX by showing a fallback screen to handle empty or insufficient data (#37) * Improve UX by showing a fallback screen to handle empty or insufficient browsing history * Add .DS_Store to .gitignore * Remove accidentally committed .DS_Store file --- .gitignore | 1 + src/popup/SlideShow.jsx | 30 +++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2d6fd38..887de55 100644 --- a/.gitignore +++ b/.gitignore @@ -705,3 +705,4 @@ fabric.properties # Exclude web-ext build artifacts /web-ext-artifacts/ /*.zip +.DS_Store diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 3f4fe73..065da69 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -17,11 +17,13 @@ const safeCallBackground = async (action, payload = {}) => { } }; + const SlideShow = ({ setView, timeRange }) => { const [slides, setSlides] = useState([]); const [index, setIndex] = useState(0); const [loading, setLoading] = useState(true); const [progress, setProgress] = useState(0); + const [notEnoughData, setNotEnoughData] = useState(false); const videoRef = useRef(null); const backgroundVideos = [ @@ -81,6 +83,16 @@ const SlideShow = ({ setView, timeRange }) => { }); const totalUnique = await safeCallBackground("getUniqueWebsites", { days }); + + // see if theres any data, if not skip slides + if (!totalUnique || totalUnique === 0) { + console.log("[SlideShow] Not enough data (totalUnique=0)."); + setNotEnoughData(true); + setLoading(false); + setProgress(100); + return; + } + slides.push({ id: 'totalWebsites', video: videos[2], @@ -179,12 +191,12 @@ const SlideShow = ({ setView, timeRange }) => { video: videos[7], prompt: pickPrompt("recapOutro", { x: timeRangeMap[timeRange] }) }); - setSlides(slides); + setNotEnoughData(false); setLoading(false); setProgress(100); }; - + loadSlides(); }, [timeRange]); @@ -207,11 +219,14 @@ const SlideShow = ({ setView, timeRange }) => { }, [index]); useEffect(() => { + if (loading || notEnoughData) return; + const timer = setTimeout(() => { setIndex(prev => (prev < slides.length - 1 ? prev + 1 : prev)); }, 5000); + return () => clearTimeout(timer); - }, [index, slides.length]); + }, [index, slides.length, loading, notEnoughData]); // πŸš€ LOADING SCREEN while slides are being fetched if (loading || progress < 100) { @@ -244,6 +259,15 @@ const SlideShow = ({ setView, timeRange }) => { ); } + if (notEnoughData) { + return ( +
+

Not enough browsing history yet. Your recap will be ready once you’ve explored a bit more!

+
+ ); + } + + // πŸš€ SLIDESHOW UI after loading return (
From f6e4835f8c66284dc49b984b2b3fd1a7b382a206 Mon Sep 17 00:00:00 2001 From: tshasan Date: Sun, 27 Apr 2025 23:04:23 -0700 Subject: [PATCH 06/20] Update CI workflow: improve Node.js setup and caching steps --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++-- src/manifest.json | 2 +- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c947365..4519cad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,14 +16,35 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Use Node.js 18.x + - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18.x' cache: 'npm' + - name: Cache node_modules + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Install dependencies run: npm ci - name: Lint (ESLint) - run: npm run lint \ No newline at end of file + run: npm run lint + + # What tests ! + # - name: Run Tests + # run: npm test + + - name: Build Extension + run: npm run build:ext + + - name: Upload Extension Artifact + uses: actions/upload-artifact@v4 + with: + name: firefox-recap-extension + path: web-ext-artifacts/ \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index d8e5139..fd325e9 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Firefox Recap", - "version": "2.0.0", + "version": "1.0.0", "description": "Categorize and analyze browsing history for productivity insights.", "permissions": [ "history", From f4804dc7fb3df40fe250b96eeb6af4d9569c0c80 Mon Sep 17 00:00:00 2001 From: tshasan Date: Sun, 27 Apr 2025 23:08:48 -0700 Subject: [PATCH 07/20] remove artifacts --- .github/workflows/ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4519cad..695829a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,12 +39,4 @@ jobs: # What tests ! # - name: Run Tests # run: npm test - - - name: Build Extension - run: npm run build:ext - - - name: Upload Extension Artifact - uses: actions/upload-artifact@v4 - with: - name: firefox-recap-extension - path: web-ext-artifacts/ \ No newline at end of file + From 1b80d60ec805424d6ed4e4bcfa1754d32b19419c Mon Sep 17 00:00:00 2001 From: katesawtell <125850781+katesawtell@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:44:22 -0700 Subject: [PATCH 08/20] Feature/cooler loading screen (#38) * backgrounds working * centered --- src/manifest.json | 3 ++- src/popup/SlideShow.jsx | 21 ++++++--------------- webpack.config.js | 3 ++- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index fd325e9..ae7e559 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -32,7 +32,8 @@ "128": "assets/icon128.png" }, "web_accessible_resources": [ - "recap.html" + "recap.html", + "assets/videos/*.mp4" ], "browser_specific_settings": { "gecko": { diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 065da69..d3ee653 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -55,7 +55,7 @@ const SlideShow = ({ setView, timeRange }) => { return array; }; - useEffect(() => { + useEffect(() => { const loadSlides = async () => { setLoading(true); const daysMap = { day: 1, week: 7, month: 30 }; @@ -231,22 +231,15 @@ const SlideShow = ({ setView, timeRange }) => { // πŸš€ LOADING SCREEN while slides are being fetched if (loading || progress < 100) { return ( -
-

Preparing your recap...

+
+

Preparing your recap...

{ {slides[index]?.video && } - -
{slides[index]?.chart ? ( <> diff --git a/webpack.config.js b/webpack.config.js index dfc94fe..1cb3f85 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -32,7 +32,8 @@ module.exports = { patterns: [ { from: 'src/manifest.json', to: 'manifest.json' }, { from: 'src/popup/popup.html', to: 'popup.html' }, - { from: 'src/popup/recap.html', to: 'recap.html' } + { from: 'src/popup/recap.html', to: 'recap.html' }, + { from: 'src/assets', to: 'assets' } ] }) ], From 6e2967ee33da771157c174b7bf0ce9588ebd2e1c Mon Sep 17 00:00:00 2001 From: katesawtell <125850781+katesawtell@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:50:23 -0700 Subject: [PATCH 09/20] Feature/cooler loading screen (#40) * backgrounds working * centered * centered load with animation * loading bar is back --- src/popup/SlideShow.jsx | 27 ++++++---------- src/popup/WavyText.jsx | 33 +++++++++++++++++++ src/popup/popup.css | 71 ++++++++++++++++++++++++++++++----------- 3 files changed, 95 insertions(+), 36 deletions(-) create mode 100644 src/popup/WavyText.jsx diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index d3ee653..49dffbe 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -4,6 +4,7 @@ import { FaArrowRight, FaArrowLeft } from 'react-icons/fa'; import promptsData from "./prompts.json"; import RadarCategoryChart from './RadarCategoryChart'; import TimeOfDayHistogram from './TimeOfDayHistogram'; +import WavyText from './WavyText'; import CategoryTrendsLineChart from './CategoryTrendsLineChart'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer as LineContainer } from 'recharts'; @@ -228,30 +229,20 @@ const SlideShow = ({ setView, timeRange }) => { return () => clearTimeout(timer); }, [index, slides.length, loading, notEnoughData]); - // πŸš€ LOADING SCREEN while slides are being fetched + // LOADING SCREEN if (loading || progress < 100) { return ( -
-

Preparing your recap...

-
-
+
+
+ +
+
+
); } - + if (notEnoughData) { return (
diff --git a/src/popup/WavyText.jsx b/src/popup/WavyText.jsx new file mode 100644 index 0000000..534a89b --- /dev/null +++ b/src/popup/WavyText.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import './popup.css'; + + +const WavyText = ({ text }) => { + return ( +
+ {text.split(/(\s+)/).map((segment, index) => { + if (segment.trim() === '') { + // Render spaces without animation + return {segment}; + } else { + // Render each character in the word with animation + return ( + + {segment.split('').map((char, charIndex) => ( + + {char} + + ))} + + ); + } + })} +
+ ); + }; + + export default WavyText; \ No newline at end of file diff --git a/src/popup/popup.css b/src/popup/popup.css index b704a03..831d467 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -119,7 +119,7 @@ body { padding: 15px; font-size: 30px; background-color: #6F84B3; - color: white; + color: #fff; border: none; margin-bottom: 15px; border-radius: 15px; @@ -137,27 +137,62 @@ body { button:disabled { display: none; } - -.spinner-overlay { - position: fixed; +/* Container that covers the entire viewport */ +.loading-screen { + position: absolute; top: 0; left: 0; - width: 100vw; - height: 100vh; - background-color: black; - opacity: 20%; + width: 100%; + height: 100%; + background-color: black; /* Optional: set background color */ display: flex; - align-items: center; justify-content: center; - z-index: 9999; /* ensures it stays on top */ + align-items: center; +} + +/* Container that centers its content vertically */ +.center-container { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Wavy text styling */ +.wavy-text { + display: inline-block; + font-size: 2rem; + font-weight: bold; + color: white; + text-align: center; + margin-bottom: 20px; /* Space between text and progress bar */ +} + +.wavy-char { + display: inline-block; + animation: wave 1.5s infinite; + animation-delay: calc(0.1s * var(--i)); +} + +@keyframes wave { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +/* Progress bar styling */ +.progress-bar { + width: 100%; + height: 8px; + background-color: #555; + border-radius: 5px; + overflow: hidden; } -.spinner { - border: 6px solid #fff; - border-top: 6px solid #444; - border-radius: 50%; - width: 60px; - height: 60px; - animation: spin 1s linear infinite; +.progress-bar-fill { + height: 100%; + background-color: #00C853; + transition: width 0.5s ease-in-out; } -@keyframes spin { to { transform: rotate(360deg); } } \ No newline at end of file From 75b0055f7b0e2a1ddc4312b976dc4721a6a8e797 Mon Sep 17 00:00:00 2001 From: tshasan Date: Mon, 28 Apr 2025 19:12:50 -0700 Subject: [PATCH 10/20] Add settings page with permission management and update manifest --- src/manifest.json | 4 +++ src/popup/HomeView.jsx | 2 +- src/popup/popup.jsx | 6 +++- src/settings/Settings.jsx | 59 ++++++++++++++++++++++++++++++++++++++ src/settings/index.jsx | 11 +++++++ src/settings/settings.html | 16 +++++++++++ webpack.config.js | 4 ++- 7 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/settings/Settings.jsx create mode 100644 src/settings/index.jsx create mode 100644 src/settings/settings.html diff --git a/src/manifest.json b/src/manifest.json index ae7e559..c5d9c72 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -31,6 +31,10 @@ "48": "assets/icon48.png", "128": "assets/icon128.png" }, + "options_ui": { + "page": "settings.html", + "open_in_tab": true + }, "web_accessible_resources": [ "recap.html", "assets/videos/*.mp4" diff --git a/src/popup/HomeView.jsx b/src/popup/HomeView.jsx index c91e119..3fe88bd 100644 --- a/src/popup/HomeView.jsx +++ b/src/popup/HomeView.jsx @@ -61,7 +61,7 @@ const HomeView = ({ onSelectTimeRange, loading, onOpenSettings}) => ( HomeView.propTypes = { onSelectTimeRange: PropTypes.func.isRequired, loading: PropTypes.bool, - onOpenSettings: PropTypes.func // optional for now + onOpenSettings: PropTypes.func.isRequired }; export default HomeView; \ No newline at end of file diff --git a/src/popup/popup.jsx b/src/popup/popup.jsx index 9570b47..be5309a 100644 --- a/src/popup/popup.jsx +++ b/src/popup/popup.jsx @@ -11,8 +11,12 @@ const Popup = () => { browser.tabs.create({ url }); }; + const handleOpenSettings = () => { + browser.runtime.openOptionsPage(); + }; + return view === 'home' - ? + ? : ; }; diff --git a/src/settings/Settings.jsx b/src/settings/Settings.jsx new file mode 100644 index 0000000..b895fd4 --- /dev/null +++ b/src/settings/Settings.jsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from 'react'; + +const Settings = () => { + const [hasPermission, setHasPermission] = useState(null); + const [requesting, setRequesting] = useState(false); + const [error, setError] = useState(''); + + const checkPermission = async () => { + try { + const granted = await browser.permissions.contains({ permissions: ['trialML'] }); + setHasPermission(granted); + } catch (err) { + setError('Could not check permission status.'); + setHasPermission(false); + } + }; + + useEffect(() => { + checkPermission(); + }, []); + + const handleRequestPermission = async () => { + setRequesting(true); + setError(''); + try { + const granted = await browser.permissions.request({ permissions: ['trialML'] }); + setHasPermission(granted); + if (!granted) setError('Permission was not granted.'); + } catch (err) { + setError('An error occurred while requesting permission.'); + setHasPermission(false); + } finally { + setRequesting(false); + } + }; + + return ( +
+

Settings

+

ML Engine Permission

+ {hasPermission === null &&

Checking permission status...

} + {hasPermission &&

βœ… trialML permission granted.

} + {hasPermission === false && ( +
+

❌ trialML permission is required for ML features.

+ + {error &&

{error}

} +
+ )} +

+ Note: You might need to enable browser.ml.enable and extensions.ml.enabled in about:config in Firefox Nightly. +

+
+ ); +}; + +export default Settings; diff --git a/src/settings/index.jsx b/src/settings/index.jsx new file mode 100644 index 0000000..f1e491b --- /dev/null +++ b/src/settings/index.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import Settings from './Settings.jsx'; + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(); +} else { + console.error('Could not find #root container for settings'); +} \ No newline at end of file diff --git a/src/settings/settings.html b/src/settings/settings.html new file mode 100644 index 0000000..523a0cb --- /dev/null +++ b/src/settings/settings.html @@ -0,0 +1,16 @@ + + + + + Firefox Recap Settings + + + +
+ + + \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 1cb3f85..99b031c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,8 @@ module.exports = { entry: { popup: './src/popup/index.jsx', recap: './src/popup/recap.jsx', - background: './src/background/background.js' + background: './src/background/background.js', + settings: './src/settings/index.jsx' }, output: { filename: '[name].js', @@ -33,6 +34,7 @@ module.exports = { { from: 'src/manifest.json', to: 'manifest.json' }, { from: 'src/popup/popup.html', to: 'popup.html' }, { from: 'src/popup/recap.html', to: 'recap.html' }, + { from: 'src/settings/settings.html', to: 'settings.html' }, { from: 'src/assets', to: 'assets' } ] }) From 3e956a15f9c46aa79e6a176f4f35c7a85501dcf9 Mon Sep 17 00:00:00 2001 From: dvaldez-olympiah Date: Tue, 29 Apr 2025 00:27:25 -0700 Subject: [PATCH 11/20] Aligned prompt text, cleaned slideshow and added documentation --- src/popup/SlideShow.jsx | 103 ++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 49dffbe..6e69a8c 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -5,9 +5,9 @@ import promptsData from "./prompts.json"; import RadarCategoryChart from './RadarCategoryChart'; import TimeOfDayHistogram from './TimeOfDayHistogram'; import WavyText from './WavyText'; -import CategoryTrendsLineChart from './CategoryTrendsLineChart'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer as LineContainer } from 'recharts'; +// Utility function for making safe background calls const safeCallBackground = async (action, payload = {}) => { try { const response = await browser.runtime.sendMessage({ action, ...payload }); @@ -18,6 +18,30 @@ const safeCallBackground = async (action, payload = {}) => { } }; +// Constants for time ranges +const timeRangeMap = { day: "today", week: "this week", month: "this month" }; + +// Helper function to pick a random prompt from the promptsData +const pickPrompt = (section, replacements = {}) => { + const options = promptsData.prompts[section] || []; + if (!options.length) return ''; + const template = options[Math.floor(Math.random() * options.length)].text; + return Object.entries(replacements).reduce( + (result, [key, val]) => result.replaceAll(`[${key}]`, val), + template + ); +}; + +// Helper function for shuffling an array +const shuffle = (array) => { + let currentIndex = array.length, randomIndex; + while (currentIndex !== 0) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; + } + return array; +}; const SlideShow = ({ setView, timeRange }) => { const [slides, setSlides] = useState([]); @@ -27,6 +51,7 @@ const SlideShow = ({ setView, timeRange }) => { const [notEnoughData, setNotEnoughData] = useState(false); const videoRef = useRef(null); + // Background videos to be shuffled for each slideshow const backgroundVideos = [ '/assets/videos/1.mp4', '/assets/videos/2.mp4', '/assets/videos/3.mp4', '/assets/videos/4.mp4', '/assets/videos/5.mp4', '/assets/videos/6.mp4', @@ -34,29 +59,8 @@ const SlideShow = ({ setView, timeRange }) => { '/assets/videos/10.mp4' ]; - const timeRangeMap = { day: "today", week: "this week", month: "this month" }; - - const pickPrompt = (section, replacements = {}) => { - const options = promptsData.prompts[section] || []; - if (!options.length) return ''; - const template = options[Math.floor(Math.random() * options.length)].text; - return Object.entries(replacements).reduce( - (result, [key, val]) => result.replaceAll(`[${key}]`, val), - template - ); - }; - - const shuffle = (array) => { - let currentIndex = array.length, randomIndex; - while (currentIndex !== 0) { - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; - } - return array; - }; - - useEffect(() => { + // Fetch and display slideshow data based on the selected time range + useEffect(() => { const loadSlides = async () => { setLoading(true); const daysMap = { day: 1, week: 7, month: 30 }; @@ -69,6 +73,7 @@ const SlideShow = ({ setView, timeRange }) => { const slides = []; const videos = shuffle([...backgroundVideos]); + // Adding intro and total visits slides slides.push({ id: 'intro', video: videos[0], @@ -83,9 +88,9 @@ const SlideShow = ({ setView, timeRange }) => { metric: false, }); + // Fetch unique websites visited and add corresponding slide const totalUnique = await safeCallBackground("getUniqueWebsites", { days }); - // see if theres any data, if not skip slides if (!totalUnique || totalUnique === 0) { console.log("[SlideShow] Not enough data (totalUnique=0)."); setNotEnoughData(true); @@ -93,7 +98,7 @@ const SlideShow = ({ setView, timeRange }) => { setProgress(100); return; } - + slides.push({ id: 'totalWebsites', video: videos[2], @@ -101,6 +106,7 @@ const SlideShow = ({ setView, timeRange }) => { metric: true, }); + // Adding daily visit count chart if the time range is not 'day' if (timeRange !== 'day') { const dailyData = await safeCallBackground("getDailyVisitCounts", { days }) || []; if (dailyData.length) { @@ -123,6 +129,7 @@ const SlideShow = ({ setView, timeRange }) => { } } + // Fetching top 3 visited websites and adding a slide for them const topSitesRaw = await safeCallBackground("getMostVisitedSites", { days, limit: 3 }) || []; const topDomains = topSitesRaw.map(s => { try { return new URL(s.url).hostname; } catch { return null; } @@ -139,6 +146,7 @@ const SlideShow = ({ setView, timeRange }) => { }); } + // Fetching visit times per hour and adding slides for peak hour and histogram const visitsPerHour = await safeCallBackground("getVisitsPerHour", { days }) || []; let peakHour = visitsPerHour.length ? visitsPerHour.reduce((a, b) => a.totalVisits > b.totalVisits ? a : b) : { hour: 0, totalVisits: 0 }; @@ -161,6 +169,7 @@ const SlideShow = ({ setView, timeRange }) => { }); } + // Fetching the busiest day and adding corresponding slide const dailyCounts = await safeCallBackground("getDailyVisitCounts", { days }) || []; const busiestDay = dailyCounts.sort((a, b) => b.count - a.count)[0]; if (busiestDay) { @@ -171,6 +180,7 @@ const SlideShow = ({ setView, timeRange }) => { }); } + // Fetching category data and adding radar chart for top category const labelCounts = await safeCallBackground("getLabelCounts", { days }) || []; const topCategory = labelCounts[0]; if (topCategory) { @@ -187,49 +197,47 @@ const SlideShow = ({ setView, timeRange }) => { }); } + // Adding recap outro slide slides.push({ id: 'recapOutro', video: videos[7], prompt: pickPrompt("recapOutro", { x: timeRangeMap[timeRange] }) }); + setSlides(slides); setNotEnoughData(false); setLoading(false); setProgress(100); }; - + loadSlides(); }, [timeRange]); + // Simulating loading progress useEffect(() => { if (!loading) return; const interval = setInterval(() => { - setProgress(prev => { - if (prev < 90) { - return prev + Math.random() * 5; - } else { - return prev; - } - }); + setProgress(prev => (prev < 90 ? prev + Math.random() * 5 : prev)); }, 200); return () => clearInterval(interval); }, [loading]); + // Handling video load useEffect(() => { if (videoRef.current) videoRef.current.load(); }, [index]); + // Automatically transition to the next slide after 5 seconds useEffect(() => { if (loading || notEnoughData) return; - const timer = setTimeout(() => { setIndex(prev => (prev < slides.length - 1 ? prev + 1 : prev)); }, 5000); - + return () => clearTimeout(timer); }, [index, slides.length, loading, notEnoughData]); - // LOADING SCREEN + // Loading screen if (loading || progress < 100) { return (
@@ -242,17 +250,17 @@ const SlideShow = ({ setView, timeRange }) => {
); } - + + // "Not enough data" screen if (notEnoughData) { return (

Not enough browsing history yet. Your recap will be ready once you’ve explored a bit more!

-
+
); } - - // πŸš€ SLIDESHOW UI after loading + // Main slideshow UI return (
) : ( -

{slides[index]?.prompt}

+

{slides[index]?.prompt}

)}
From 0317e1f2820d5f12e2de786b832ad8b606411ef9 Mon Sep 17 00:00:00 2001 From: Kate Date: Tue, 29 Apr 2025 08:28:01 -0700 Subject: [PATCH 12/20] styling for settings page --- src/settings/Settings.jsx | 44 +++++++++++++++++----------- src/settings/settings.css | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 src/settings/settings.css diff --git a/src/settings/Settings.jsx b/src/settings/Settings.jsx index b895fd4..77c2292 100644 --- a/src/settings/Settings.jsx +++ b/src/settings/Settings.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import './settings.css'; const Settings = () => { const [hasPermission, setHasPermission] = useState(null); @@ -35,25 +36,34 @@ const Settings = () => { }; return ( -
-

Settings

-

ML Engine Permission

- {hasPermission === null &&

Checking permission status...

} - {hasPermission &&

βœ… trialML permission granted.

} - {hasPermission === false && ( -
-

❌ trialML permission is required for ML features.

- - {error &&

{error}

} -
- )} -

- Note: You might need to enable browser.ml.enable and extensions.ml.enabled in about:config in Firefox Nightly. -

+
+
+

Settings

+

ML Engine Permission

+ {hasPermission === null &&

Checking permission status...

} + {hasPermission &&

βœ… trialML permission granted.

} + {hasPermission === false && ( +
+

❌ trialML permission is required for ML features.

+ + {error &&

{error}

} +
+ )} +

+ Note:
+ If ML features aren’t showing up, follow these steps: +

    +
  1. Open about:config in Firefox Nightly.
  2. +
  3. Search for browser.ml.enable and set it to true.
  4. +
  5. Search for extensions.ml.enabled and set it to true.
  6. +
+

+
); + }; export default Settings; diff --git a/src/settings/settings.css b/src/settings/settings.css new file mode 100644 index 0000000..f29aef6 --- /dev/null +++ b/src/settings/settings.css @@ -0,0 +1,60 @@ +.settings-page { + background-color: black; + color: white; + min-height: 100vh; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Inter', 'Arial', sans-serif; + } + + .settings-container { + text-align: center; + padding: 2rem; + } + + .settings-container h1 { + font-size: 2.5rem; + margin-bottom: 1rem; + } + + .settings-container h2 { + font-size: 1.8rem; + margin-bottom: 1rem; + } + + .success { + color: #4caf50; /* Green */ + font-size: 2rem; + } + + .error { + color: #f44336; /* Red */ + font-size: 2rem; + } + + button { + margin-top: 1rem; + padding: 0.6rem 1.2rem; + font-size: 1rem; + font-weight: bold; + border: none; + border-radius: 8px; + background-color: white; + color: black; + cursor: pointer; + transition: background-color 0.3s ease; + } + + button:hover { + background-color: #dddddd; + } + + .note { + margin-top: 2rem; + font-size: 1rem; + color: #cccccc; + } + \ No newline at end of file From fd219b21035d0ff76d7b883c6abfe8c256716acc Mon Sep 17 00:00:00 2001 From: tshasan Date: Tue, 29 Apr 2025 12:44:08 -0700 Subject: [PATCH 13/20] version update --- src/.DS_Store | Bin 6148 -> 0 bytes src/manifest.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 src/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index ebbb88b1025f88aa41e04dd11857c08312382980..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKK~BR!3>-s|D!BB>algP1R#kaHKLBk62PzVQde4n-ckFdhP=F%`G$VN@o!E{t zhqwk{>tlBZtN<(-&iL?S4!`d)!$m5WWQ!ipctejPUeV36zZbmr8aq52jqYpkOOky7YBT_63d&$I^}>IkOQ9%`1heO zoNMyfI6fUbVg(>CC`e~ENhZLCuc$bqj8`0h=teE#1ve%Sv#N00+@;Ga5Ri_Lbk=F8=5oxP^d+QN8X roWa~k=Mkfr9HW>&d=%eK@`_*ceoY=5M>+K Date: Wed, 30 Apr 2025 12:38:11 -0700 Subject: [PATCH 14/20] Refactor RadarCategoryChart to normalize data and enhance tooltip display (#43) --- src/popup/RadarCategoryChart.jsx | 39 ++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/popup/RadarCategoryChart.jsx b/src/popup/RadarCategoryChart.jsx index 06de1e9..5e8929b 100644 --- a/src/popup/RadarCategoryChart.jsx +++ b/src/popup/RadarCategoryChart.jsx @@ -1,21 +1,46 @@ import React from 'react'; import { - Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer + Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer, Tooltip } from 'recharts'; const RadarCategoryChart = ({ data }) => { + // Find the maximum count to normalize the data + const maxCount = Math.max(...data.map(item => item.count), 0); + + // Normalize the data (scale counts between 0 and 1) + const normalizedData = data.map(item => ({ + ...item, + normalizedCount: maxCount > 0 ? item.count / maxCount : 0, + originalCount: item.count + })); + return (
- - - - - + + + + + + [`Original Count: ${props.payload.originalCount}`, null]} // Show original count + labelFormatter={(label) => `Category: ${label}`} + />
); }; -export default RadarCategoryChart; +export default RadarCategoryChart; \ No newline at end of file From 3e61ea2510a4c44d86b46854f4c25b9e62eeea21 Mon Sep 17 00:00:00 2001 From: katesawtell <125850781+katesawtell@users.noreply.github.com> Date: Fri, 2 May 2025 11:49:18 -0700 Subject: [PATCH 15/20] co occurance slide (#44) --- src/popup/SlideShow.jsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 6e69a8c..8cbda58 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -197,6 +197,23 @@ const SlideShow = ({ setView, timeRange }) => { }); } + // Fetching co-occurrence counts and adding a text summary slide + const coCounts = await safeCallBackground("getCOCounts", { days }) || []; + const topCoPairs = coCounts + .filter(([, , count]) => count > 0) + .sort((a, b) => b[2] - a[2]) + .slice(0, 3); // Top 5 pairs + + if (topCoPairs.length) { + const [catA, catB, count] = topCoPairs[0]; + + slides.push({ + id: 'topCoOccurrenceText', + video: videos[8], + prompt: `Your strongest category pair πŸ”— ${catA} and ${catB} showed up together ${count} times in your browsing β€” your most frequent pairing!` + }); + } + // Adding recap outro slide slides.push({ id: 'recapOutro', From a00de793af96312449ac3504b289590dc9472bbf Mon Sep 17 00:00:00 2001 From: katesawtell <125850781+katesawtell@users.noreply.github.com> Date: Fri, 2 May 2025 15:41:48 -0700 Subject: [PATCH 16/20] new handlers, lots of issue fixing (#45) added slides for all of the handlers fix: address multiple recap issues use prompts for unique websites deduplicate top sites fix recency-frequency slide display show domain-only in transition patterns correct top category visit counts --- src/popup/SlideShow.jsx | 174 ++++++++++++++++++++++------------------ src/popup/prompts.json | 67 ++++++++++++++-- 2 files changed, 159 insertions(+), 82 deletions(-) diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 8cbda58..0a7b7b9 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -65,91 +65,95 @@ const SlideShow = ({ setView, timeRange }) => { setLoading(true); const daysMap = { day: 1, week: 7, month: 30 }; const days = daysMap[timeRange] || 1; - + console.log("[SlideShow] Fetching and storing history..."); await safeCallBackground("fetchAndStoreHistory", { days }); console.log("[SlideShow] History fetch complete, loading slides..."); - + const slides = []; const videos = shuffle([...backgroundVideos]); - - // Adding intro and total visits slides + slides.push({ id: 'intro', video: videos[0], - prompt: pickPrompt("introRecap", { x: timeRangeMap[timeRange] }), - metric: false, + prompt: pickPrompt("introRecap", { x: timeRangeMap[timeRange] }) }); - + slides.push({ id: 'totalVisits', video: videos[1], - prompt: pickPrompt("introToTotalWebsites", { x: timeRangeMap[timeRange] }), - metric: false, + prompt: pickPrompt("introToTotalWebsites", { x: timeRangeMap[timeRange] }) }); - - // Fetch unique websites visited and add corresponding slide + + // Unique websites const totalUnique = await safeCallBackground("getUniqueWebsites", { days }); - if (!totalUnique || totalUnique === 0) { - console.log("[SlideShow] Not enough data (totalUnique=0)."); setNotEnoughData(true); setLoading(false); setProgress(100); return; } - slides.push({ id: 'totalWebsites', video: videos[2], - prompt: `You visited ${typeof totalUnique === 'number' ? totalUnique.toLocaleString() : '0'} unique websites ${timeRangeMap[timeRange]}.`, - metric: true, + prompt: pickPrompt("totalWebsites", { + x: totalUnique.toLocaleString(), + d: timeRangeMap[timeRange] + }) }); - - // Adding daily visit count chart if the time range is not 'day' - if (timeRange !== 'day') { - const dailyData = await safeCallBackground("getDailyVisitCounts", { days }) || []; - if (dailyData.length) { - slides.push({ - id: 'dailyVisitsChart', - video: null, - prompt: 'Your daily visit counts over time πŸ“…', - chart: ( - - - - - - - - - - ) - }); - } - } - - // Fetching top 3 visited websites and adding a slide for them - const topSitesRaw = await safeCallBackground("getMostVisitedSites", { days, limit: 3 }) || []; - const topDomains = topSitesRaw.map(s => { + + // Top 3 visited websites, deduplicated + const topSitesRaw = await safeCallBackground("getMostVisitedSites", { days, limit: 10 }) || []; + const topDomains = [...new Set(topSitesRaw.map(s => { try { return new URL(s.url).hostname; } catch { return null; } - }).filter(Boolean).slice(0, 3); - + }).filter(Boolean))].slice(0, 3); + if (topDomains.length) { - const template = (promptsData.prompts.top3Websites || [{ text: "Your top sites: [TopSites]" }])[0].text; - const list = topDomains.join(', '); slides.push({ id: 'topSites', video: videos[3], - prompt: template.replace('[TopSites]', list), - metric: false, + prompt: pickPrompt("top3Websites", { TopSites: topDomains.join(', ') }) }); } - - // Fetching visit times per hour and adding slides for peak hour and histogram + + // Recency-Frequency + const rfStats = await safeCallBackground("getRecencyFrequency", { days, limit: 1 }) || []; + if (rfStats.length) { + const topDomain = rfStats[0]; + slides.push({ + id: 'recencyFrequency', + video: videos[0], + prompt: pickPrompt("recencyFrequency", { + Domain: topDomain.domain, + Count: topDomain.count, + DaysSince: topDomain.daysSince.toFixed(1) + }) + }); + } + + // Most common jump + const transitions = await safeCallBackground("getTransitionPatterns", { days }) || {}; + if (transitions.summary?.topPattern) { + const { from, to, count } = transitions.summary.topPattern; + let fromDomain, toDomain; + try { fromDomain = new URL(from).hostname; } catch { fromDomain = from; } + try { toDomain = new URL(to).hostname; } catch { toDomain = to; } + + slides.push({ + id: 'topTransition', + video: videos[1], + prompt: pickPrompt("mostCommonJump", { + From: fromDomain, + To: toDomain, + Count: count + }) + }); + } + + // Peak hour const visitsPerHour = await safeCallBackground("getVisitsPerHour", { days }) || []; let peakHour = visitsPerHour.length ? visitsPerHour.reduce((a, b) => a.totalVisits > b.totalVisits ? a : b) : { hour: 0, totalVisits: 0 }; - + slides.push({ id: 'visitsPerHour', video: videos[4], @@ -157,9 +161,9 @@ const SlideShow = ({ setView, timeRange }) => { Start: `${(peakHour.hour % 12) || 12}${peakHour.hour < 12 ? 'am' : 'pm'}`, End: `${((peakHour.hour + 1) % 12) || 12}${(peakHour.hour + 1) < 12 ? 'am' : 'pm'}`, Count: peakHour.totalVisits - }), + }) }); - + if (visitsPerHour.length) { slides.push({ id: 'visitsPerHourChart', @@ -168,8 +172,8 @@ const SlideShow = ({ setView, timeRange }) => { chart: }); } - - // Fetching the busiest day and adding corresponding slide + + // Busiest day const dailyCounts = await safeCallBackground("getDailyVisitCounts", { days }) || []; const busiestDay = dailyCounts.sort((a, b) => b.count - a.count)[0]; if (busiestDay) { @@ -179,15 +183,18 @@ const SlideShow = ({ setView, timeRange }) => { prompt: pickPrompt("busiestDay", { Date: busiestDay.date, Count: busiestDay.count }) }); } - - // Fetching category data and adding radar chart for top category + + // Top category const labelCounts = await safeCallBackground("getLabelCounts", { days }) || []; const topCategory = labelCounts[0]; - if (topCategory) { + if (topCategory && topCategory.categories?.length) { slides.push({ id: 'topCategory', video: videos[6], - prompt: pickPrompt("topCategory", { Category: topCategory.categories[0], Count: topCategory.count }) + prompt: pickPrompt("topCategory", { + Category: topCategory.categories[0], + Count: topCategory.count + }) }); slides.push({ id: 'topCategoryRadar', @@ -196,25 +203,40 @@ const SlideShow = ({ setView, timeRange }) => { chart: ({ category: c.categories[0], count: c.count }))} /> }); } - - // Fetching co-occurrence counts and adding a text summary slide - const coCounts = await safeCallBackground("getCOCounts", { days }) || []; - const topCoPairs = coCounts - .filter(([, , count]) => count > 0) - .sort((a, b) => b[2] - a[2]) - .slice(0, 3); // Top 5 pairs - - if (topCoPairs.length) { - const [catA, catB, count] = topCoPairs[0]; - + + // Category trends + const trends = await safeCallBackground("getCategoryTrends", { days }) || []; + if (trends.length) { + const topDay = trends.reduce((max, day) => + day.categories[0].count > (max.categories[0]?.count || 0) ? day : max + ); slides.push({ - id: 'topCoOccurrenceText', - video: videos[8], - prompt: `Your strongest category pair πŸ”— ${catA} and ${catB} showed up together ${count} times in your browsing β€” your most frequent pairing!` + id: 'categoryTrends', + video: videos[9], + prompt: pickPrompt("trendingCategory", { + Category: topDay.categories[0].label, + Date: topDay.date, + Count: topDay.categories[0].count + }) }); } - - // Adding recap outro slide + + // Co-occurrence + const coCounts = await safeCallBackground("getCOCounts", { days }) || []; + const topCoPairs = coCounts.filter(([, , count]) => count > 0).sort((a, b) => b[2] - a[2]); + if (topCoPairs.length) { + const [catA, catB, count] = topCoPairs[0]; + if (topCoPairs.length) { + const [catA, catB, count] = topCoPairs[0]; // Only the top pair + + slides.push({ + id: 'topCoOccurrenceText', + video: videos[8], + prompt: `Your strongest category pair πŸ”— ${catA} and ${catB} showed up together ${count} times in your browsing β€” your most frequent pairing!` + }); + } + } + slides.push({ id: 'recapOutro', video: videos[7], diff --git a/src/popup/prompts.json b/src/popup/prompts.json index c8f72fe..01c6a63 100644 --- a/src/popup/prompts.json +++ b/src/popup/prompts.json @@ -59,23 +59,24 @@ "totalWebsites": [ { "id": "totalSites1", - "text": "You've visited [x] unique websites todayβ€”explorer of the digital universe! πŸŒŒπŸš€" + "text": "You've visited [x] unique websites [d]β€”explorer of the digital universe! πŸŒŒπŸš€" }, { + "id": "totalSites2", - "text": "You clicked [x] URLs this [d,w,m]β€”curious, unstoppable, and maybe just a little too online. πŸŒπŸ‘€" + "text": "You clicked [x] URLs this [d]β€”curious, unstoppable, and maybe just a little too online. πŸŒπŸ‘€" }, { "id": "totalSites3", - "text": "You navigated through [X] unique websites this [x]β€”a true net navigator! 🧭🌐" + "text": "You navigated through [x] unique websites this [d]β€”a true net navigator! 🧭🌐" }, { "id": "totalSites4", - "text": "You launched [X] website visits this [x]β€”digital explorer status: Expert! πŸš€πŸŒŸ" + "text": "You launched [x] website visits this [d]β€”digital explorer status: Expert! πŸš€πŸŒŸ" }, { "id": "totalSites5", - "text": "Your browser saw [X] sites this [x]β€”you're surfing the web waves! πŸ„β€β™€οΈπŸŒŠ" + "text": "Your browser saw [x] sites this [d]β€”you're surfing the web waves! πŸ„β€β™€οΈπŸŒŠ" } ], "top3Websites": [ @@ -224,7 +225,61 @@ "id": "recapOutro4", "text": "That was your recap [x]β€”keep exploring, and we’ll be watching πŸ‘€πŸŒ" } - ] + ], + "trendingCategory": [ + { + "id": "trend1", + "text": "[Category] spiked on [Date] β€” trending in your personal internet bubble. πŸ“ˆπŸŒ" + }, + { + "id": "trend2", + "text": "Your clicks shifted to [Category] on [Date] β€” a change of pace? πŸ”πŸ“š" + }, + { + "id": "trend3", + "text": "[Date] was all about [Category] β€” what sparked the streak? πŸ’‘πŸ’₯" + }, + { + "id": "trend4", + "text": "A noticeable surge in [Category] on [Date] β€” looks like something caught your interest. πŸ“ŠπŸ‘€" + } + ], + "recencyFrequency": [ + { + "id": "rf1", + "text": "You've been all over [Domain] β€” [Count] visits, and last seen just [DaysSince] days ago! πŸ”₯🌐" + }, + { + "id": "rf2", + "text": "[Domain] has your attention: [Count] visits, with your most recent stop just [DaysSince] days back. πŸ‘€πŸ•’" + }, + { + "id": "rf3", + "text": "Frequent flyer alert! [Domain] saw [Count] visits, last checked in [DaysSince] days ago. βœˆοΈπŸ’»" + }, + { + "id": "rf4", + "text": "You can’t stay away from [Domain]: [Count] visits, most recently [DaysSince] days ago. πŸš€πŸ“Ά" + } + ], + "mostCommonJump": [ + { + "id": "jump1", + "text": "You most often jumped from [From] to [To] β€” a digital two-step. πŸ©°πŸ”" + }, + { + "id": "jump2", + "text": "[From] β†’ [To] was your go-to combo. Muscle memory? πŸ“Žβž‘οΈπŸ“Ž" + }, + { + "id": "jump3", + "text": "A familiar path: [From] to [To] β€” one click to the next. πŸ§­πŸ”—" + }, + { + "id": "jump4", + "text": "[From] to [To] formed your most traveled route. What’s the story? πŸ“˜πŸ›£οΈ" + } + ] } } \ No newline at end of file From 3930c2a270ad1d5eaa5ed8183d742cec5730be69 Mon Sep 17 00:00:00 2001 From: Kate Date: Fri, 2 May 2025 15:51:20 -0700 Subject: [PATCH 17/20] fix for top category number and top 3 --- src/popup/SlideShow.jsx | 42 +++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 8cbda58..035a73b 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -130,21 +130,28 @@ const SlideShow = ({ setView, timeRange }) => { } // Fetching top 3 visited websites and adding a slide for them - const topSitesRaw = await safeCallBackground("getMostVisitedSites", { days, limit: 3 }) || []; - const topDomains = topSitesRaw.map(s => { - try { return new URL(s.url).hostname; } catch { return null; } - }).filter(Boolean).slice(0, 3); + const topSitesRaw = await safeCallBackground("getMostVisitedSites", { days, limit: 50 }) || []; + const domainCounts = {}; + for (const site of topSitesRaw) { + let domain; + try { domain = new URL(site.url).hostname; } catch { continue; } + domainCounts[domain] = (domainCounts[domain] || 0) + site.count; + } + + const topDomains = Object.entries(domainCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([domain]) => domain); + if (topDomains.length) { - const template = (promptsData.prompts.top3Websites || [{ text: "Your top sites: [TopSites]" }])[0].text; - const list = topDomains.join(', '); slides.push({ id: 'topSites', video: videos[3], - prompt: template.replace('[TopSites]', list), - metric: false, + prompt: pickPrompt("top3Websites", { TopSites: topDomains.join(', ') }) }); } + // Fetching visit times per hour and adding slides for peak hour and histogram const visitsPerHour = await safeCallBackground("getVisitsPerHour", { days }) || []; @@ -182,20 +189,31 @@ const SlideShow = ({ setView, timeRange }) => { // Fetching category data and adding radar chart for top category const labelCounts = await safeCallBackground("getLabelCounts", { days }) || []; - const topCategory = labelCounts[0]; + const topCategory = labelCounts.find(c => c.categories?.length && c.count > 0); + if (topCategory) { slides.push({ id: 'topCategory', video: videos[6], - prompt: pickPrompt("topCategory", { Category: topCategory.categories[0], Count: topCategory.count }) + prompt: pickPrompt("topCategory", { + Category: topCategory.categories[0], + Count: topCategory.count + }) }); + slides.push({ id: 'topCategoryRadar', video: null, prompt: "Here's how your categories stack up πŸ“Š", - chart: ({ category: c.categories[0], count: c.count }))} /> + chart: ({ + category: c.categories[0], + count: c.count + }))} /> }); - } + } else { + console.warn("[SlideShow] No top category with nonzero count found."); + } + // Fetching co-occurrence counts and adding a text summary slide const coCounts = await safeCallBackground("getCOCounts", { days }) || []; From d254030b9681e51820cdc6c741cdb4d23aa732c9 Mon Sep 17 00:00:00 2001 From: Kate Date: Fri, 2 May 2025 16:31:45 -0700 Subject: [PATCH 18/20] summary slide --- src/popup/SlideShow.jsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index e4db492..e57eca1 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -265,12 +265,27 @@ const SlideShow = ({ setView, timeRange }) => { }); } } - + + //SUMMARY + let summaryLines = []; + summaryLines.push(`✨ Recap Summary ✨`); + summaryLines.push(`🌐 Unique websites: ${totalUnique.toLocaleString()}`); + if (topCategory) summaryLines.push(`πŸ† Favorite category: ${topCategory.categories[0]}`); + if (topDomains.length) summaryLines.push(`πŸ”₯ Top site: ${topDomains[0]}`); + summaryLines.push(`⏰ Peak hour: ${(peakHour.hour % 12) || 12}${peakHour.hour < 12 ? 'am' : 'pm'}`); + slides.push({ - id: 'recapOutro', - video: videos[7], - prompt: pickPrompt("recapOutro", { x: timeRangeMap[timeRange] }) + id: 'recapSummary', + video: videos[6], + prompt: ( +
+ {summaryLines.map((line, idx) => ( +
{line}
+ ))} +
+ ) }); + setSlides(slides); setNotEnoughData(false); From 1a72861690a273f49ac7c2913fcd85d015ee42f1 Mon Sep 17 00:00:00 2001 From: taimur Date: Sat, 3 May 2025 13:47:46 -0700 Subject: [PATCH 19/20] v0.2.0-alpha --- package.json | 2 +- src/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4b91ac0..317052b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firefox-recap", - "version": "1.0.0", + "version": "0.2.0", "description": "**Firefox Recap** is a powerful browser extension designed to help users analyze and understand their browsing habits. It categorizes your browsing history using **AI-powered topic classification** and a **frequency + recency algorithm**, providing **insightful reports** on how you spend time online.", "main": "webpack.config.js", "scripts": { diff --git a/src/manifest.json b/src/manifest.json index 92453a9..d860bf8 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Firefox Recap", - "version": "0.1.0", + "version": "0.2.0", "description": "Categorize and analyze browsing history for productivity insights.", "permissions": [ "history", From 4e3efb861d240eb0dd2197a20cdaf9b36c4a802e Mon Sep 17 00:00:00 2001 From: katesawtell <125850781+katesawtell@users.noreply.github.com> Date: Wed, 7 May 2025 10:08:59 -0700 Subject: [PATCH 20/20] popup closes on button click (#48) --- src/popup/popup.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/popup/popup.jsx b/src/popup/popup.jsx index be5309a..05ceaac 100644 --- a/src/popup/popup.jsx +++ b/src/popup/popup.jsx @@ -9,6 +9,7 @@ const Popup = () => { const onSelectTimeRange = (range) => { const url = browser.runtime.getURL(`recap.html?range=${range}`); browser.tabs.create({ url }); + window.close(); }; const handleOpenSettings = () => {