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