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", 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 diff --git a/src/popup/SlideShow.jsx b/src/popup/SlideShow.jsx index 6e69a8c..671b439 100644 --- a/src/popup/SlideShow.jsx +++ b/src/popup/SlideShow.jsx @@ -65,47 +65,43 @@ 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 }) || []; @@ -128,28 +124,61 @@ 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 => { + + // 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 }; - + 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 +186,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 +197,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,29 +208,80 @@ 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]; + 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."); } - - // Adding recap outro slide + + // 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: 'categoryTrends', + video: videos[9], + prompt: pickPrompt("trendingCategory", { + Category: topDay.categories[0].label, + Date: topDay.date, + Count: topDay.categories[0].count + }) + }); + } + + // 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]; + 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!` + }); + } + + // 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); @@ -298,3 +378,4 @@ const SlideShow = ({ setView, timeRange }) => { }; export default SlideShow; + 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 = () => { 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