98 lines
3.1 KiB
JavaScript
98 lines
3.1 KiB
JavaScript
/**
|
|
* Forage P1 — Page Signal Capture
|
|
*
|
|
* Run via Claude's javascript_tool on any web page you want to add to Forage:
|
|
*
|
|
* javascript_tool({ text: <contents of this file>, tabId: <current tab> })
|
|
*
|
|
* What it does:
|
|
* 1. Reads the page title, URL, word count, and meta description.
|
|
* 2. POSTs to /capture — registers the page as a Forage item (idempotent)
|
|
* and fires a "view" signal for USER_ID.
|
|
* 3. After DWELL_MS milliseconds, fires a "dwell" signal if still on page.
|
|
* (The dwell timer is cancelled if the snippet runs again before it fires.)
|
|
*
|
|
* Configuration:
|
|
* Change USER_ID to match the Forage user you are browsing as (1, 2, or 3
|
|
* for the seed users; any positive integer for a new user).
|
|
* Set TOKEN to the value passed with --token when starting the server,
|
|
* or leave empty ('') if the server was started without --token.
|
|
*/
|
|
(function forageCapture() {
|
|
const SERVER = 'http://localhost:4242';
|
|
const USER_ID = 1;
|
|
const TOKEN = '';
|
|
const DWELL_MS = 30_000;
|
|
|
|
const url = location.href;
|
|
const canonicalUrl = document.querySelector('link[rel="canonical"]')?.href || '';
|
|
const title = document.title || url;
|
|
const words = document.body.innerText.trim().split(/\s+/).length;
|
|
const readingTimeMin = Math.max(1, Math.round(words / 200));
|
|
const description =
|
|
document.querySelector('meta[name="description"]')?.content ||
|
|
document.querySelector('meta[property="og:description"]')?.content ||
|
|
'';
|
|
const source = (() => {
|
|
try {
|
|
return new URL(url).hostname.replace(/^www\./, '');
|
|
} catch {
|
|
return '';
|
|
}
|
|
})();
|
|
|
|
// Cancel any previous dwell timer from this snippet on the same page.
|
|
if (window.__forageCaptureDwellTimer) {
|
|
clearTimeout(window.__forageCaptureDwellTimer);
|
|
window.__forageCaptureDwellTimer = null;
|
|
}
|
|
|
|
let itemId = null;
|
|
|
|
const authHeaders = TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {};
|
|
|
|
fetch(`${SERVER}/capture`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
body: JSON.stringify({
|
|
url,
|
|
canonical_url: canonicalUrl,
|
|
title,
|
|
source,
|
|
reading_time_min: readingTimeMin,
|
|
description,
|
|
user_id: USER_ID,
|
|
}),
|
|
})
|
|
.then((r) => r.json())
|
|
.then((d) => {
|
|
itemId = d.item_id;
|
|
console.log(
|
|
`[Forage] Captured: "${title}" — id=${itemId}, ~${readingTimeMin}min read`
|
|
);
|
|
})
|
|
.catch((e) => console.warn('[Forage] /capture failed:', e.message));
|
|
|
|
// Fire dwell signal after DWELL_MS if still on the page.
|
|
window.__forageCaptureDwellTimer = setTimeout(() => {
|
|
if (itemId == null) return;
|
|
fetch(`${SERVER}/signal`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
body: JSON.stringify({
|
|
user_id: USER_ID,
|
|
item_id: itemId,
|
|
signal_type: 'dwell',
|
|
duration_ms: DWELL_MS,
|
|
}),
|
|
})
|
|
.then(() =>
|
|
console.log(`[Forage] Dwell: "${title}" (${DWELL_MS / 1000}s)`)
|
|
)
|
|
.catch(() => {});
|
|
window.__forageCaptureDwellTimer = null;
|
|
}, DWELL_MS);
|
|
|
|
return `[Forage] Capturing "${title}" for user ${USER_ID}`;
|
|
})();
|