Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
201a5ec
Refactor CI workflow: remove commented-out steps and clean up linting…
tshasan Apr 28, 2025
88ecaf8
Merge pull request #36 from Firefox-Recap/fix/ci
tshasan Apr 28, 2025
b7f2d12
manifest was not in dist
katesawtell Apr 28, 2025
87dedb3
Update build command to overwrite destination and clean up background…
tshasan Apr 28, 2025
7352edf
Update manifest.json: set background script to persistent and add bro…
tshasan Apr 28, 2025
a9995fe
Improve UX by showing a fallback screen to handle empty or insufficie…
katesawtell Apr 28, 2025
f6e4835
Update CI workflow: improve Node.js setup and caching steps
tshasan Apr 28, 2025
e504a89
Merge branch 'develop' of github.com:Firefox-Recap/Firefox-Recap into…
tshasan Apr 28, 2025
f4804dc
remove artifacts
tshasan Apr 28, 2025
1b80d60
Feature/cooler loading screen (#38)
katesawtell Apr 28, 2025
6e2967e
Feature/cooler loading screen (#40)
katesawtell Apr 28, 2025
75b0055
Add settings page with permission management and update manifest
tshasan Apr 29, 2025
5fd1b05
Merge pull request #41 from Firefox-Recap/feature/grantPerm
tshasan Apr 29, 2025
3e956a1
Aligned prompt text, cleaned slideshow and added documentation
dvaldez-olympiah Apr 29, 2025
0317e1f
styling for settings page
katesawtell Apr 29, 2025
fd219b2
version update
tshasan Apr 29, 2025
fa51bfb
Refactor RadarCategoryChart to normalize data and enhance tooltip dis…
tshasan Apr 30, 2025
3e61ea2
co occurance slide (#44)
katesawtell May 2, 2025
a00de79
new handlers, lots of issue fixing (#45)
katesawtell May 2, 2025
3930c2a
fix for top category number and top 3
katesawtell May 2, 2025
9b3a3a8
some more fixes for the prompts
katesawtell May 2, 2025
d254030
summary slide
katesawtell May 2, 2025
1a72861
v0.2.0-alpha
tshasan May 3, 2025
22ec64f
Merge branch 'main' into develop
tshasan May 3, 2025
4e3efb8
popup closes on button click (#48)
katesawtell May 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
39 changes: 32 additions & 7 deletions src/popup/RadarCategoryChart.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ width: '100%', height: 400 }}>
<ResponsiveContainer>
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={data}>
<PolarGrid />
<PolarAngleAxis dataKey="category" />
<PolarRadiusAxis />
<Radar name="Visits" dataKey="count" stroke="#8884d8" fill="#8884d8" fillOpacity={0.6} />
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={normalizedData} margin={{ top: 20, right: 30, left: 30, bottom: 5 }}>
<PolarGrid stroke="#e0e0e0" />
<PolarAngleAxis
dataKey="category"
tick={{ fill: '#666', fontSize: 12 }}
/>
<PolarRadiusAxis angle={30} domain={[0, 1]} tick={false} axisLine={false} />
<Radar
name="Visits"
dataKey="normalizedCount"
stroke="#8884d8"
fill="#8884d8"
fillOpacity={0.7}
strokeWidth={2}
/>
<Tooltip
contentStyle={{ backgroundColor: 'rgba(255, 255, 255, 0.8)', border: '1px solid #ccc', borderRadius: '4px' }} // Style tooltip
formatter={(value, name, props) => [`Original Count: ${props.payload.originalCount}`, null]} // Show original count
labelFormatter={(label) => `Category: ${label}`}
/>
</RadarChart>
</ResponsiveContainer>
</div>
);
};

export default RadarCategoryChart;
export default RadarCategoryChart;
171 changes: 126 additions & 45 deletions src/popup/SlideShow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) || [];
Expand All @@ -128,38 +124,71 @@ 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],
prompt: pickPrompt("peakBrowsingTime", {
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',
Expand All @@ -168,8 +197,8 @@ const SlideShow = ({ setView, timeRange }) => {
chart: <TimeOfDayHistogram data={visitsPerHour} />
});
}

// 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) {
Expand All @@ -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: <RadarCategoryChart data={labelCounts.map(c => ({ category: c.categories[0], count: c.count }))} />
chart: <RadarCategoryChart data={labelCounts.map(c => ({
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: (
<div style={{ color: '#fff', textAlign: 'center' }}>
{summaryLines.map((line, idx) => (
<div key={idx} style={{ marginBottom: '0.5rem' }}>{line}</div>
))}
</div>
)
});

setSlides(slides);
Expand Down Expand Up @@ -298,3 +378,4 @@ const SlideShow = ({ setView, timeRange }) => {
};

export default SlideShow;

1 change: 1 addition & 0 deletions src/popup/popup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Loading