Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions submissions/tracechain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Trace Chain
> A nifty lil browser extension that lets you trace the paths you take across your browsing sessions everyday



## Features
- **Path Tracking**: Automatically tracks the paths you take across your browsing sessions.
- **Path Visualization**: Visualize your browsing paths with a graph view>??
- **PDF Recaps**: Generate PDF recaps of your browsing paths for easy sharing and review.
- **AI Insights**: Get AI-generated insights on your browsing habits.
- **Customizable**: Review Board - For your research modes when you open too many tabs in a short period of time.
- **Privacy Focused**: Your data stays on your device, no external servers involved.
- **Open Source**: Fully open source, contributions are welcome!

#### AI Use Disclaimer
> I used ai for some debugging and fixing a lot of bug fixes and interacting with browser api. I used it to generate some of the code of graph-core since my iteration were unoptimized and slow comparatively.
208 changes: 208 additions & 0 deletions submissions/tracechain/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
let isIdle = false;

const todayKey = () => new Date().toISOString().slice(0, 10);
const lastURL = new Map();
chrome.idle.setDetectionInterval(120);
chrome.idle.onStateChanged.addListener((state) => {
isIdle = state !== "active";
});

function normalise(obj = {}) {
return { visits: obj.visits ?? [], edges: obj.edges ?? [] };
}
function push(key, field, obj) {
if (isIdle) return; // skip idle periods
chrome.storage.local.get([key], (d) => {
const day = normalise(d[key]);
day[field].push(obj);
chrome.storage.local.set({ [key]: day });
});
}
// ensure {visits:[],edges:[]} skeleton

function save(key, day) {
chrome.storage.local.set({ [key]: day });
}

function safePush(key, field, obj) {
chrome.storage.local.get([key], (data) => {
const day = normalise(data[key]);
day[field].push(obj);
save(key, day);
});
}

chrome.webNavigation.onCommitted.addListener((d) => {
if (d.frameId) return; // ignore iframes

const visit = {
url: d.url,
t: Date.now(),
tabId: d.tabId,
type: d.transitionType,
};
safePush(todayKey(), "visits", visit);

const prev = lastURL.get(d.tabId);
if (prev)
safePush(todayKey(), "edges", { from: prev, to: d.url, t: Date.now() });
lastURL.set(d.tabId, d.url);
});
chrome.webNavigation.onCreatedNavigationTarget.addListener((d) => {
const src = lastURL.get(d.sourceTabId);
if (src)
safePush(todayKey(), "edges", { from: src, to: d.url, t: Date.now() });
});

chrome.runtime.onMessage.addListener((msg, _s, res) => {
if (msg?.type === "GET_DAY") {
chrome.storage.local.get([todayKey()], (d) =>
res(normalise(d[todayKey()])),
);
return true;
}
});

const BURST_WINDOW_MIN = 10; // minute observation window
const BURST_TABS = 9; // tab count that triggers a burst
const REMINDER_DELAY_MIN = 15; // nudge 15 minutes after burst

const BURST_WINDOW_MS = BURST_WINDOW_MIN * 60 * 1_000;
const NUDGE_ID = "recap-nudge";
const NUDGE_ICON = chrome.runtime.getURL("icon.png");

let openTimes = [];
let burstTabs = [];
let alarmActive = false;

chrome.tabs.onCreated.addListener((tab) => {
const now = Date.now();
openTimes.push(now);
burstTabs.push({
tabId: tab.id,
url: tab.pendingUrl || tab.url || "about:blank",
title: "(new tab)",
});

// prune entries outside the window
while (openTimes.length && now - openTimes[0] > BURST_WINDOW_MS) {
openTimes.shift();
burstTabs.shift();
}

console.log("[BurstDetector] window =", openTimes.length, "tabs");

if (!alarmActive && openTimes.length >= BURST_TABS) {
const cache = burstTabs.map(({ url, title }) => ({ url, title }));
chrome.storage.local.set({ burstCache: cache }, () => {
if (chrome.runtime.lastError) {
console.error(
"[BG] Failed to set burstCache:",
chrome.runtime.lastError,
);
} else {
console.log("[BG] burstCache set:", cache);
}
});
chrome.alarms.create(NUDGE_ID, { delayInMinutes: REMINDER_DELAY_MIN });
alarmActive = true;
console.log("[BurstDetector] Burst detected → alarm scheduled");
}
});

chrome.webNavigation.onCommitted.addListener((d) => {
if (d.frameId) return; // ignore iframes

const visit = {
url: d.url,
t: Date.now(),
tabId: d.tabId,
type: d.transitionType,
};
safePush(todayKey(), "visits", visit);

const prev = lastURL.get(d.tabId);
if (prev)
safePush(todayKey(), "edges", { from: prev, to: d.url, t: Date.now() });
lastURL.set(d.tabId, d.url);

const idx = burstTabs.findIndex((t) => t.tabId === d.tabId);
if (idx !== -1) {
chrome.tabs.get(d.tabId, (tab) => {
burstTabs[idx].url = d.url;
burstTabs[idx].title = tab?.title ? tab.title : d.url;
const cache = burstTabs.map(({ url, title }) => ({ url, title }));
chrome.storage.local.set({ burstCache: cache }, () => {
if (chrome.runtime.lastError) {
console.error(
"[BG] Failed to update burstCache:",
chrome.runtime.lastError,
);
} else {
console.log("[BG] burstCache updated:", cache);
}
});
});
}
});

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.type === "REVIEW_LOADED") {
openTimes = [];
burstTabs = [];
chrome.storage.local.remove("burstCache", () => {
if (chrome.runtime.lastError) {
console.error(
"[BG] Failed to remove burstCache:",
chrome.runtime.lastError,
);
} else {
console.log("[BG] burstCache removed after review loaded");
}
});
}
});

chrome.notifications.onClicked.addListener((id) => {
if (id !== NUDGE_ID) return;
openReview();
chrome.notifications.clear(id);
});

chrome.notifications.onClicked.addListener((id) => {
if (id !== NUDGE_ID) return;
openReview();
chrome.notifications.clear(id);
});

function openReview() {
chrome.tabs.create({ url: chrome.runtime.getURL("review.html") });
}

// keep the worker alive forcefuilly
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name !== NUDGE_ID) return;

chrome.notifications
.create(NUDGE_ID, {
type: "basic",
iconUrl: NUDGE_ICON,
title: "Tab spree detected!",
message: "Click to triage your new tabs.",
requireInteraction: true, // keeps notification onscreen (Chrome >= 67)
})
.then(() => {
setTimeout(
() =>
chrome.notifications.getAll((nots) => {
if (nots[NUDGE_ID]) {
openReview();
chrome.notifications.clear(NUDGE_ID);
}
}),
30_000,
);
});

alarmActive = false;
});
135 changes: 135 additions & 0 deletions submissions/tracechain/graph-core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
function drawGraph(svg, visits = [], edges = []) {
svg.innerHTML = "";
if (!edges.length) return;

const VB = svg.viewBox.baseVal;
const W = VB?.width ? VB.width : svg.clientWidth || 800;
const H = VB?.height ? VB.height : svg.clientHeight || 450;

const host = (u) => new URL(u).hostname.replace(/^www\./, "");

const counts = {};
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
// biome-ignore lint/complexity/noForEach: <explanation>
visits.forEach((v) => (counts[host(v.url)] = (counts[host(v.url)] || 0) + 1));

// Ensure every edge endpoint is represented even if count==0
// biome-ignore lint/complexity/noForEach: <explanation>
edges.forEach((e) => {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
// biome-ignore lint/complexity/noForEach: <explanation>
[host(e.from), host(e.to)].forEach((h) => (counts[h] ??= 0));
});

const nodes = Object.fromEntries(
Object.entries(counts).map(([h, n]) => [
h,
{ id: h, count: n, x: 0, y: 0, vx: 0, vy: 0 },
]),
);
const nodeArr = Object.values(nodes);

const rand01 = (str) => {
let h = 2166136261 >>> 0;
for (let i = 0; i < str.length; i++)
h = Math.imul(h ^ str.charCodeAt(i), 16777619);
return (h >>> 0) / 2 ** 32; // 0‥1
};
const R0 = 0.35 * Math.min(W, H);
// biome-ignore lint/complexity/noForEach: <explanation>
nodeArr.forEach((n) => {
const a = 2 * Math.PI * rand01(n.id);
n.x = W / 2 + Math.cos(a) * R0;
n.y = H / 2 + Math.sin(a) * R0;
});

/* ---------- toy force sim ---------- */
const ideal = 140;
const steps = 350;
const repelC = ideal ** 2 / 4;
for (let s = 0; s < steps; s++) {
// repulsion
for (let i = 0; i < nodeArr.length; i++)
for (let j = i + 1; j < nodeArr.length; j++) {
const a = nodeArr[i];
const b = nodeArr[j];
let dx = a.x - b.x;
let dy = a.y - b.y;
const dist = Math.hypot(dx, dy) || 1e-6;
const rep = repelC / dist;
dx /= dist;
dy /= dist;
a.vx += dx * rep;
a.vy += dy * rep;
b.vx -= dx * rep;
b.vy -= dy * rep;
}
// attraction
// biome-ignore lint/complexity/noForEach: <explanation>
edges.forEach(({ from, to }) => {
const a = nodes[host(from)];
const b = nodes[host(to)];
if (!a || !b) return;
let dx = a.x - b.x;
let dy = a.y - b.y;
const dist = Math.hypot(dx, dy) || 1e-6;
const k = (dist - ideal) * 0.08;
dx /= dist;
dy /= dist;
a.vx -= dx * k;
a.vy -= dy * k;
b.vx += dx * k;
b.vy += dy * k;
});
// integrate + damp
// biome-ignore lint/complexity/noForEach: <explanation>
nodeArr.forEach((n) => {
n.vx *= 0.6;
n.vy *= 0.6;
n.x = Math.min(W - 30, Math.max(30, n.x + n.vx * 0.02));
n.y = Math.min(H - 30, Math.max(30, n.y + n.vy * 0.02));
});
}

/* ---------- draw ---------- */
const $ = (tag, attr) => {
const e = document.createElementNS("http://www.w3.org/2000/svg", tag);
for (const k in attr) e.setAttribute(k, attr[k]);
return e;
};

// biome-ignore lint/complexity/noForEach: <explanation>
edges.forEach(({ from, to }) => {
const a = nodes[host(from)];
const b = nodes[host(to)];
if (a && b)
svg.appendChild(
$("line", {
x1: a.x,
y1: a.y,
x2: b.x,
y2: b.y,
stroke: "#bbb",
"stroke-width": 1,
}),
);
});

// biome-ignore lint/complexity/noForEach: <explanation>
nodeArr.forEach((n) => {
const r = 14 + Math.log2(n.count + 1) * 6;
svg.appendChild($("circle", { cx: n.x, cy: n.y, r, fill: "#4da3ff" }));

const txt = $("text", {
x: n.x,
y: n.y + 4,
fill: "#fff",
"text-anchor": "middle",
});
const label = n.id.length > 18 ? `${n.id.slice(0, 16)}…` : n.id;
const fs = Math.min(12, ((r - 3) * 2) / label.length + 4); // heuristic
txt.setAttribute("font-size", fs.toFixed(1));
txt.textContent = label;
svg.appendChild(txt);
});
}
17 changes: 17 additions & 0 deletions submissions/tracechain/graph.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
body {
margin: 0;
font: 14px / 1.3 sans-serif;
background: #111;
color: #ddd;
text-align: center;
}
h1 {
margin: 8px 0 4px;
font-size: 18px;
}
svg {
border: 1px solid #333;
background: #000;
margin: auto;
display: block;
}
Binary file added submissions/tracechain/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading