آپ کا براؤزر ہر وہ صفحہ یاد رکھتا ہے جسے آپ نے کبھی کھولا ہے، لیکن اسے کچھ معلوم نہیں کہ آپ نے انہیں کیوں کھولا۔
اگر آپ تین دن تک درجن بھر ٹیبز پر موجود نوٹ بک کا موازنہ کرتے ہیں، مشغول ہو جاتے ہیں، اور ایک ہفتے بعد واپس آتے ہیں، تو آپ کی تاریخ صرف ٹائم اسٹیمپ اور عنوانات کی ایک سادہ فہرست دکھا سکتی ہے، اس بات کا کوئی اشارہ نہیں کہ وہ وزٹ ایک چیز تھی — ایک فیصلہ جو آپ نے شروع کیا لیکن کبھی ختم نہیں ہوا۔
اس ٹیوٹوریل میں آپ بناتے ہیں: کھلی لوپایک اوپن سورس، لوکل فرسٹ کروم ایکسٹینشن ہے جو آپ کی براؤزنگ ہسٹری کو اسکین کرکے، انہیں "انٹٹ تھریڈز” (فیصلے، تحقیقات، اور کھلے سوالات جن پر آپ واپس آتے رہتے ہیں) میں گروپ بندی کرکے اور پھر ہر آئٹم کو اسکور کرکے اس مسئلے کو حل کرتی ہے کہ یہ اب بھی کتنا زندہ ہے۔ اختیاری طور پر، ایک چیٹ اسسٹنٹ کو سپورٹ کرنے کے لیے Claude کا استعمال کریں جو ان تھریڈز کو سادہ زبان میں لیبل کر سکتا ہے، مخصوص اگلے اقدامات تجویز کر سکتا ہے، اور پوچھتا ہے کہ "مجھے اس ہفتے کیا بند کرنا چاہیے؟”
آخر میں، آپ مندرجہ ذیل بنائیں گے:
-
مینی فیسٹ V3 کروم ایکسٹینشن سروس ورکر اور مکمل ٹیب ڈیش بورڈ کے ساتھ
-
ایک مقامی پائپ لائن جو IndexedDB میں نیویگیشن ہسٹری کو مکمل طور پر کیپچر کرتی ہے، صاف کرتی ہے، پارٹیشنز اور کلسٹرز کرتی ہے۔
-
اصلی (پیچیدہ) تلاش کے ڈیٹا پر کلسٹرنگ الگورتھم ٹیون اور ڈیبگ کیا گیا۔
-
کلاڈ کا استعمال کرتے ہوئے AI لیبلنگ پرت، context.dev سے برانڈ ڈیٹا کا استعمال کرتے ہوئے ایک بنیادی قدم کے ساتھ
-
ایک چیٹ اسسٹنٹ جو پورے تھریڈ کا اندازہ لگاتا ہے اور آپ کو بتاتا ہے کہ آگے کیا کرنا ہے۔
-
آن بورڈنگ، ڈیزائن سسٹم، اور ورکنگ پائپ لائن اسٹیٹس سسٹم کے ساتھ چیکنا ڈیش بورڈ
ہر چیز ڈیوائس کے اندر چلتی ہے، اور صرف نیٹ ورک کالز اختیاری ہیں اور آپ کی اپنی API کیز کا استعمال کرتے ہوئے کی جاتی ہیں۔
انڈیکس
کیا تعمیر کرنا ہے
پہلی بار، اوپن لوپس آپ کو ایک مرکزی ویلکم اسکرین کے ساتھ خوش آمدید کہتا ہے جو پائپ لائن کے تین مراحل میں آپ کی رہنمائی کرتی ہے۔
تاریخ کو اسکین کرنے، سیشن بنانے، اور ارادے کا نقشہ بنانے کے بعد، نیویگیشن کو ریاستی گروپ تھریڈز (فعال، روکا، اور نیند) میں دوبارہ ترتیب دیا جاتا ہے۔ ہر آئٹم میں اعتماد کا سکور، سادہ زبان کا خلاصہ، مخصوص اگلے اقدامات، اور شامل ہیں۔ دوبارہ شروع کریں یہ بٹن وہی صفحہ دوبارہ کھولتا ہے جسے آپ نے چھوڑا تھا۔ دائیں کالم میں آپ کے اپنے تھریڈ پر مبنی چیٹ اسسٹنٹس ہوتے ہیں۔

وہ معاون جوابات صارف کے حقیقی دھاگوں میں ایکسٹراپولیٹ کرتے ہیں، ان کی درجہ بندی کرتے ہیں کہ چھوڑنا کتنا آسان ہے بمقابلہ اصل فیصلوں کی کتنی مقدار کی ضرورت ہے۔ یہ اس بات کی بھی وضاحت کرتا ہے کہ یہ اس تعمیر کا سب سے نیا حصہ کیوں ہے اور اس کا انحصار context.dev کے بنیادی اقدامات پر ہے جنہیں ہم بعد میں اس ٹیوٹوریل میں شامل کریں گے۔
شرطیں
پیروی کرنے کے لیے آپ کو ضرورت ہو گی:
-
نوڈ 18+ کرومیم پر مبنی براؤزرز (کروم، بہادر، ایج، وغیرہ)۔
-
آرام ٹائپ اسکرپٹ اور رد عمل. آپ کو ماہر بننے کی ضرورت نہیں ہے، لیکن آپ کو Hooks اور async/await پڑھنے میں مہارت حاصل کرنی چاہیے۔
-
بنیادی علم انڈیکسڈ ڈی بی یہ مددگار ہے، لیکن ضروری نہیں ہے۔ کیونکہ جب آپ آگے بڑھتے ہیں تو آپ سیکھتے ہیں کہ آپ کو کیا کرنے کی ضرورت ہے۔
اس تعمیر کے دونوں حصے اختیاری ہیں اور ہر ایک کو مفت درجے کے ساتھ اپنی API کلید کی ضرورت ہوتی ہے۔
-
نہیں ہیومینٹی API کلید (platform.claude.com پر) AI لیبلنگ اور چیٹ اسسٹنٹس کے لیے
-
کوئی راستہ نہیں context.dev API کلید (context.dev سے) برانڈ فاؤنڈیشن بنانے کے اقدامات
چونکہ دونوں کیز ان کے اوپر ایک اضافی پرت ہیں، آپ ان کے بغیر پوری بنیادی پائپ لائن، کیپچر، کلسٹرنگ اور اسکورنگ بنا اور استعمال کر سکتے ہیں۔
اوپن لوپس کی ساخت
کوڈ لکھنے سے پہلے مجموعی شکل کو دیکھنا مفید ہے۔ اوپن لوپس میں ہر قدم ایک IndexedDB اسٹور سے پڑھتا ہے اور اگلے کو لکھتا ہے۔
chrome.history (backfill) ──┐
chrome.tabs.onUpdated (live)─┴─→ raw_events
│ noise filter
▼
sessions
│ ambient detection + clustering + scoring
▼
intent_threads
│
▼
React dashboard
│ optional, opt-in
├──→ brand enrichment (context.dev)
└──→ AI labeling + next step (Claude)
│
▼ optional, opt-in
AI assistant chat (Claude)
ہر قدم ذیل میں ایک الگ ماڈیول ہے۔ src/pipeline/ہر ایک کو آزادانہ طور پر جانچا جا سکتا ہے۔ Chrome DevTools کھولیں اور درج ذیل کو دیکھیں: raw_events, sessionsیا intent_threads آپ ایپلیکیشن ٹیب سے براہ راست کسی دوسرے قدم کو چھوئے بغیر ایک قدم کو دوبارہ بنا سکتے ہیں۔
شیئر کی قسم
تمام مراحل استعمال کرتے ہیں اور ایک ہی بار بیان کردہ TypeScript انٹرفیس کی ایک ہی چھوٹی تعداد تیار کرتے ہیں۔ src/types.ts:
// Shared TypeScript interfaces for the openloops pipeline.
// Each stage of the pipeline consumes and produces these types.
export interface RawEvent {
id: string;
url: string;
domain: string;
title: string;
visitedAt: number; // epoch ms
source: "backfill" | "live";
}
export interface Session {
id: string;
events: RawEvent[];
startedAt: number;
endedAt: number;
domains: string[];
keywords: string[];
}
export interface IntentThread {
id: string;
title: string;
summary?: string;
nextStep?: string; // one concrete action to move the thread forward
sessions: Session[];
type: "buying" | "research" | "planning" | "learning" | "unclassified";
confidence: number; // 0-1
status: "active" | "stalled" | "dormant";
firstSeen: number;
lastSeen: number;
distinctDays: number;
signals: string[];
}
export interface Brand {
domain: string;
name: string;
description: string;
industry: string;
logoUrl: string;
brandColor: string;
}
زیادہ تر فیلڈز IntentThread, confidence, status, signalsاور distinctDays ہم بعد میں اس گائیڈ میں دھاگوں کو کلسٹرنگ اور اسکور کرتے وقت مکمل طور پر مقامی ہیورسٹکس کو بھریں گے۔ summary اور nextStep رہنا undefined جب تک کہ آپ اسے بعد میں شامل اختیاری AI لیبلنگ مرحلے میں آباد نہ کریں۔
یہ وہ نمونہ ہے جو پورے پروجیکٹ کو کام کرتا ہے۔ بنیادی ڈیٹا ماڈل اپنے طور پر کام کرتا ہے، اور AI اسے افزودہ کرتا ہے۔
ظاہر
اوپن لوپس ایک مینی فیسٹ V3 ایکسٹینشن ہے جس میں تین اجازتیں اور تین میزبان اجازتیں ہیں۔
{
"manifest_version": 3,
"name": "openloops",
"version": "0.0.1",
"description": "Reconstruct your browsing history into an AI-labeled map of intent threads: active decisions, stalled research, open questions. Fully local.",
"permissions": ["history", "tabs", "storage"],
"host_permissions": [
"https://api.anthropic.com/*",
"https://api.context.dev/*",
"https://logos.context.dev/*"
],
"background": {
"service_worker": "src/background.ts",
"type": "module"
},
"options_page": "src/dashboard/index.html",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_title": "openloops",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png"
}
}
}
اجازتیں، میزبانی کی اجازت، اور options_page ہر شے کا ایک خاص وزن ہوتا ہے۔
-
permissions: ["history", "tabs", "storage"]صرف اختیار ہے۔ بنیادی پائپ لائن آپ کو اس کی ضرورت ہے۔historyبیک اپ اشتہارات کے لیے اپنی براؤزنگ ہسٹری پڑھیں،tabsسروس ورکر کو صفحہ کے نئے بوجھ کا مشاہدہ کرنے اور ‘ریزیومے’ کے ذریعے ٹیبز کو دوبارہ کھولنے کی اجازت دیتا ہے۔storageیہ وہ جگہ ہے جہاں آپ کی API کیز اور ترجیحات واقع ہیں۔ -
host_permissionsاگر آپ اختیاری AI خصوصیات استعمال کرتے ہیں تو یہ الگ اور صرف اہم ہے۔ ڈیش بورڈ کیا بناتا ہے۔fetch()CORS کی غلطیوں کے بغیر Anthropic اور context.dev کو کال کرتا ہے۔ -
options_pageڈیش بورڈ کو پوائنٹس دیے گئے۔ اس کے بجائے، اسے اس طرح ترتیب دیں:default_popupاس کا مطلب ہے کہ ٹول بار کے آئیکون پر کلک کرنے سے ڈیش بورڈ ایک چھوٹے پاپ اپ کے بجائے مکمل براؤزر ٹیب کے طور پر کھل جائے گا۔ اسٹیٹس گروپ کارڈز اور چیٹ پینلز کے ساتھ ملٹی کالم لے آؤٹ کو دیکھتے وقت یہ اہم ہے۔
ایکسٹینشن کو سکیفولڈ کرنے کا طریقہ
وائٹ اور CRXJS پلگ ان کے ساتھ شروع کریں جو ہاٹ ماڈیول ری لوڈنگ کے ساتھ مینی فیسٹ V3 ایکسٹینشن کو مرتب کرتے ہیں۔
npm create vite@latest openloops -- --template react-ts
cd openloops
npm install @crxjs/vite-plugin idb react-markdown
آپ کا vite.config.ts CRXJS آپ کا manifest.jsonاور وہاں سے، وائٹ تالیف کو سنبھالتا ہے۔ src/background.ts واقعی .js وہ فائلیں جنہیں کروم لوڈ کر سکتا ہے (کچی فائلیں) .ts مینی فیسٹ میں سروس ورکر روٹ رجسٹریشن کی خرابی کے ساتھ ناکام ہو جاتا ہے۔ ہم اسے اگلے حصے میں ڈیبگ کریں گے۔
ڈیش بورڈ کا انٹری پوائنٹ ایک معیاری React 18 روٹ ہے۔
openloops
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./app.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
);
اسے بنائیں اور پھر اسے بغیر زپ شدہ ایکسٹینشن کے طور پر لوڈ کریں۔
npm run build
کروم میں، اس پر جائیں: chrome://extensionsفعال کریں ڈویلپر موڈکلک کریں پیک کھول دیا سامانمنتخب کریں dist/ فولڈر اگر ابھی تک کچھ بھی نہیں بنایا گیا ہے، تو ٹول بار کے آئیکون پر کلک کرنے سے ایک خالی ڈیش بورڈ ٹیب کھل جائے گا اور سروس ورکر (ایکسٹینشن کارڈ پر "سروس ورکر" کے لنک میں نظر آتا ہے) لاگ ان ہونا چاہیے۔ [openloops] Extension installed. انسٹال کرنا۔
ایک بار جب آپ کے پاس فاؤنڈیشن ہے، یہ بھرنا شروع کرنے کا وقت ہے. raw_events آپ کی اصل براؤزنگ ہسٹری کے ساتھ۔
براؤزنگ ہسٹری کو کیسے کیپچر کریں۔
اوپن لوپس میں ہر ریکارڈ اس طرح شروع ہوتا ہے: RawEventوہ اقسام جو ہم نے پہلے دیکھی تھیں: URL، ڈومین، ٹائٹل، ٹائم اسٹیمپ، اور source یا تو ایک "backfill" یا "live".
دو پائپ لائنیں اسے بھرتی ہیں۔
-
کوئی راستہ نہیں ایک بار بیک فل پچھلے 14 دن پڑھیں
chrome.historyمطالبہ پر -
زندہ گرفتاریاس مقام سے، صفحہ کے نئے بوجھ موصول ہوتے ہیں۔
دونوں راستے چند چھوٹے مددگاروں کا اشتراک کرتے ہیں اور ایک ہی IndexedDB پرت پر لکھتے ہیں، لہذا یہ پہلے ان کی تعمیر کے قابل ہے۔
چند شیئرنگ مددگار
بنانا src/lib/util.ts:
export function isHttpUrl(url: string): boolean {
return url.startsWith("http://") || url.startsWith("https://");
}
export function extractDomain(url: string): string {
try {
const { hostname } = new URL(url);
return hostname.replace(/^www./, "");
} catch {
return url;
}
}
export function isLocalHost(domain: string): boolean {
if (domain === "localhost" || domain === "127.0.0.1") return true;
if (domain.endsWith(".local")) return true;
const octets = domain.split(".");
if (octets.length === 4 && octets.every((o) => /^d{1,3}$/.test(o))) {
const [a, b] = octets.map(Number);
if (a === 10) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
}
return false;
}
export function hashId(url: string, visitedAt: number): string {
const str = `({url}|){visitedAt}`;
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
hash |= 0;
}
return (hash >>> 0).toString(36);
}
ان چار خصوصیات میں سے ہر ایک ایسے مسائل کو حل کرتی ہے جو شاید بعد میں تعمیر میں معلوم نہ ہوں۔
-
isHttpUrlلائیو کیپچر اور بیک فل دونوں کے ذریعے استعمال ہونے والا مشترکہ اسکیم گارڈ، اور ایک گیٹ جو اسے ایک ساتھ رکھتا ہے۔chrome://,chrome-extension://,about:اورfile://آپ کے ڈیٹا سے URL کو مکمل طور پر حذف کر دیتا ہے۔ دونوں کیپچر راستے اسے دوسرے سے پہلے کہتے ہیں۔ -
extractDomainنیکیوں کو دور کرتا ہے۔www.آسان میزبان نام لوٹاتا ہے۔bbc.co.ukاورnews.bbc.co.ukاصل رجسٹرڈ ڈومین نکالنے کے لیے عوامی لاحقوں کی فہرست کی ضرورت ہوتی ہے، اس لیے وہ اس منطق کے مطابق ایک جیسے ڈومینز پر نہیں ٹوٹتے۔ اگر URL خراب ہے، تو یہ ان پٹ پھینکنے کے بجائے اسے بغیر تبدیلی کے لوٹاتا ہے۔ -
isLocalHostیہ ایک وجہ سے موجود ہے۔ جب آپ بعد میں اس گائیڈ میں برانڈ میں اضافہ کریں گے، تو آپ اپنے ڈومین کا نام ایک بیرونی API کو بھیجیں گے۔localhost:5173یا192.168.1.50یہ اس API کے لیے معنی خیز نہیں ہے اور یہ صرف ایک ضائع شدہ تلاش ہوگی، اس لیے میں اسے ایک بار یہاں ماخذ میں فلٹر کرنے کی تجویز کرتا ہوں۔ یہ یقینی بناتا ہے کہ:localhost,127.0.0.1,.localمیزبان نام اور معیاری نجی IPv4 رینج (10.x.x.x,172.16.x.x-172.31.x.x,192.168.x.x)۔ -
hashIdیہ URL اور ٹائم اسٹیمپ کو ایک مختصر، تعییناتی سٹرنگ میں جوڑنے کے لیے ایک سادہ ہیشنگ الگورتھم (djb2) کا استعمال کرتا ہے۔(url, visitedAt)جوڑا ہمیشہ ایک ہی ID تیار کرتا ہے۔ یہ لکھنے والوں کو کمزور بنا دیتا ہے۔ یعنی، اگر آپ بیک فل دوبارہ چلاتے ہیں، ایک ہی ID ایک ہی چونکہ آپ IndexedDB وزٹ کرتے ہیں۔put"میری سرگزشت تلاش کریں" پر ایک سے زیادہ بار کلک کرنا محفوظ ہے کیونکہ یہ نقل کرنے کے بجائے صاف طور پر اوور رائٹ کرتا ہے۔
ڈیٹا بیس پرت (اب تک)
اوپن لوپس ہر چیز کو IndexedDB میں اسٹور کرتا ہے بذریعہ: idb ایک ریپر جو خام IndexedDB کالوں کے لیے ٹائپ شدہ وعدے پر مبنی API فراہم کرتا ہے۔ بنانا src/db/index.ts:
import { openDB, type DBSchema, type IDBPDatabase } from "idb";
import type { RawEvent } from "../types";
interface OpenloopsDB extends DBSchema {
raw_events: {
key: string;
value: RawEvent;
indexes: { by_visitedAt: number };
};
}
const DB_NAME = "openloops";
const DB_VERSION = 1;
let _db: Promise> | null = null;
export function getDB(): Promise> {
if (!_db) {
_db = openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains("raw_events")) {
const s = db.createObjectStore("raw_events", { keyPath: "id" });
s.createIndex("by_visitedAt", "visitedAt");
}
},
});
}
return _db;
}
export async function clearEvents(): Promise {
const db = await getDB();
return db.clear("raw_events");
}
export async function putEvents(events: RawEvent[]): Promise {
if (events.length === 0) return;
const db = await getDB();
const tx = db.transaction("raw_events", "readwrite");
await Promise.all([...events.map((e) => tx.store.put(e)), tx.done]);
}
export async function getAllEvents(): Promise {
const db = await getDB();
return db.getAllFromIndex("raw_events", "by_visitedAt");
}
export async function getEventCount(): Promise {
const db = await getDB();
return db.count("raw_events");
}
چار چھوٹی خصوصیات ڈیٹا بیس پرت کے پہلے ورژن کو مکمل کرتی ہیں۔ clearEvents بیک فل اس سٹوریج کو صاف کرتا ہے جسے وہ پہلے کال کرتا ہے تاکہ تمام اسکینز کلین اسنیپ شاٹ سے شروع ہوں۔ putEvents IDB کا استعمال کرتے ہوئے ایک تعیناتی بنائیں۔ putیہ اوور رائٹ ہے، ڈپلیکیشن نہیں۔ getAllEvents ترتیب دی گئی تمام اشیاء کو لوٹاتا ہے: visitedAt انڈیکس کے ذریعے اور getEventCount ڈیش بورڈ کے لیے ایک سادہ شمار لوٹاتا ہے۔
_db یہ ایک ماڈیول سطح کا سنگلٹن وعدہ ہے، لہذا توسیع کے تمام حصے بشمول سروس ورکرز اور ڈیش بورڈز، ایک ہی کنکشن کا اشتراک کرتے ہیں۔ DB_VERSION وقت شروع 1 یہاں بعد کے حصوں میں، جب آپ سیشنز، انٹینٹ تھریڈز، اور برانڈ ڈیٹا شامل کرتے ہیں، تو آپ اس طرح ایک نیا محفوظ اسٹور شامل کریں گے: if (!db.objectStoreNames.contains(...)) اور اس نمبر کو ٹکرانا۔ اس گارڈ کا مطلب ہے کہ موجودہ صارفین اپنے پہلے سے موجود اسٹورز کو چھوئے بغیر محفوظ طریقے سے اپ گریڈ کر سکتے ہیں۔
حقیقی وقت میں نئے دوروں پر قبضہ کریں۔
سروس ورکرز توسیع کا ہمیشہ حصہ ہوتے ہیں۔ بنانا src/background.ts:
import { hashId, extractDomain, isHttpUrl } from "./lib/util";
import { putEvents } from "./db/index";
import type { RawEvent } from "./types";
chrome.runtime.onInstalled.addListener(() => {
console.log("[openloops] Extension installed.");
});
chrome.action.onClicked.addListener(() => {
chrome.runtime.openOptionsPage();
});
const DEDUP_MS = 3_000;
const recentCaptures = new Map();
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status !== "complete" || !tab.url) return;
const url = tab.url;
if (!isHttpUrl(url)) return;
const last = recentCaptures.get(tabId);
const now = Date.now();
if (last && last.url === url && now - last.at < DEDUP_MS) {
console.log(`[openloops] dedup skip — tab ({tabId} ){url}`);
return;
}
recentCaptures.set(tabId, { url, at: now });
const event: RawEvent = {
id: hashId(url, now),
url,
domain: extractDomain(url),
title: tab.title ?? url,
visitedAt: now,
source: "live",
};
putEvents([event]).then(() => {
console.log(`[openloops] captured ({event.domain} — ){event.title}`);
}).catch((err) => {
console.error("[openloops] putEvents failed:", err);
});
});
chrome.action.onClicked ٹول بار کا آئیکن ڈیش بورڈ کو پاپ اپ کے بجائے ٹیب کے طور پر کھولنے کا سبب بنتا ہے۔ options_page اپنے مینی فیسٹ میں اندراج درج کریں۔
لائیو کیپچر اندرونی طور پر ہوتا ہے۔ tabs.onUpdated ایک سامعین جو صفحہ لوڈ ہونے، ری ڈائریکٹ ہونے اور ٹائٹل اپ ڈیٹ ہونے پر کروم بار بار فائر کرتا ہے (لیکن آپ کو صرف اس لمحے میں دلچسپی ہونی چاہیے) changeInfo.status === "complete". وہاں سے، isHttpUrl یہ ہر اس چیز کو حذف کر دیتا ہے جو اصل ویب صفحہ نہیں ہے، ڈپلیکیشن گارڈ بے کار "ڈون" ایونٹس کو ختم کر دیتا ہے جنہیں SPA فائر کرنا پسند کرتا ہے، اور باقی۔ RawEvent کے ساتھ source: "live".
ڈپلیکیشن گارڈ ڈیزائن کی بہترین کوشش سے ہے۔ recentCaptures یہ باقاعدہ ان میموری ہے۔ Mapکروم سروس ورکر کو ایونٹس کے درمیان روکتا ہے۔ Map اس کے ساتھ۔ ایک ہی ویک اپ سیشن میں فالتو برسٹ اب بھی کم ہو جاتے ہیں، لیکن جب سروس ورکر کو دوبارہ شروع کیا جاتا ہے تو نہیں، اور یہ ایک قابل قبول تجارت ہے۔ hashId جب آپ IndexedDB پر پہنچتے ہیں، تو آپ پہلے سے ہی ایک بے ضرر کلون بنا رہے ہوتے ہیں۔
آخری تحریر بھی کچھ عجیب لگتی ہے۔ putEvents([event]).then(...).catch(...) اس کے بجائے await. سننے والے کو تحریر کی تکمیل کو روکنے کی ضرورت نہیں ہے، اور سروس ورکر اتنی دیر تک زندہ رہتا ہے کہ وہ ایک IndexedDB تحریر کو مکمل کر سکے چاہے اسے معطل کر دیا گیا ہو، اس لیے صرف تحریر کو مکمل کرنا اور جاری رکھنا کافی ہے۔
کہ source کھیتوں کا وزن پہلے ظاہر ہونے سے زیادہ ہوتا ہے۔ کیونکہ اس طرح کوڈ بعد میں "صارف نے اصل میں ہسٹری کو بازیافت کیا" اور "ایکسٹینشن صرف 5 منٹ کے لیے کھلا تھا" کے درمیان فرق کرتا ہے۔ اس گائیڈ میں بعد میں اپنے ڈیش بورڈ کو ڈیزائن کرتے وقت آن بورڈنگ کے لیے یہ اہم ہے۔
اب ایکسٹینشن بنائیں اور دوبارہ لوڈ کریں (npm run build، پھر توسیعی کارڈ پر دوبارہ لوڈ آئیکن پر کلک کریں۔ chrome://extensions)، چند صفحات پر جائیں، پھر سروس ورکر کے DevTools کو کھولنے کے لیے توسیعی کارڈ میں "سروس ورکر" پر کلک کریں۔ آپ دیکھ سکیں گے [openloops] captured ... ایک لاگ لائن ظاہر ہوگی جو اس بات کی تصدیق کرتی ہے کہ ریئل ٹائم کیپچر کام کر رہا ہے۔
14 دن کے ریکارڈ کو پُر کریں۔
لائیو کیپچر آپ کو صرف وہی دیکھنے دیتا ہے جو ہو رہا ہے۔ ~ بعد ایکسٹینشن کو انسٹال کرنے کے لیے بھی اوپن لوپس کو کارآمد بنانے کے لیے حالیہ تاریخ کو دوبارہ آباد کرنے کی ضرورت ہوگی۔ بنانا src/pipeline/backfill.ts:
import { extractDomain, hashId, isHttpUrl } from "../lib/util";
import { putEvents, clearEvents } from "../db/index";
import type { RawEvent } from "../types";
const CONCURRENCY = 50;
async function visitsForItem(
item: chrome.history.HistoryItem,
startTime: number
): Promise {
if (!item.url) return [];
if (!isHttpUrl(item.url)) return [];
const visits = await chrome.history.getVisits({ url: item.url });
const events: RawEvent[] = [];
for (const visit of visits) {
if (!visit.visitTime || visit.visitTime < startTime) continue;
events.push({
id: hashId(item.url, visit.visitTime),
url: item.url,
domain: extractDomain(item.url),
title: item.title ?? item.url,
visitedAt: visit.visitTime,
source: "backfill",
});
}
return events;
}
export async function backfillHistory(days = 14): Promise {
await clearEvents();
const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
const historyItems = await chrome.history.search({
text: "",
startTime,
maxResults: 100_000,
});
let totalWritten = 0;
for (let i = 0; i < historyItems.length; i += CONCURRENCY) {
const batch = historyItems.slice(i, i + CONCURRENCY);
const batchResults = await Promise.all(
batch.map((item) => visitsForItem(item, startTime))
);
const events = batchResults.flat();
await putEvents(events);
totalWritten += events.length;
}
return totalWritten;
}
backfillHistory یہ ایک فون کال سے شروع ہوتا ہے۔ clearEvents یہ اسٹوریج کو صاف کرتا ہے تاکہ ہر رن منتخب ونڈو کا صاف سنیپ شاٹ بناتا ہے۔ تمام جسمانی دورے اب بھی موجود ہیں۔ chrome.historyدوبارہ شروع کرنے سے کچھ بھی ضائع نہیں ہوتا ہے۔ پھر تلاش کریں: maxResults: 100_000100 کی ڈیفالٹ قدر کسی بھی شخص کے لیے بہت کم ہے جو کچھ دنوں سے زیادہ حقیقی دنیا میں براؤزنگ کر رہا ہے۔
ہر ایک مماثلت HistoryItem پاس visitsForItemکروم کچھ بھی واپس نہیں کرتا ہے اور جو لوٹتا ہے اسے چھوڑ دیتا ہے۔ url یہ کچھ حذف شدہ تاریخ کے اندراجات میں ایک عجیب بات ہے اور یہ استعمال کرتے ہوئے غیر ویب یو آر ایل کو چھوڑ دیتا ہے: isHttpUrlاس شے کی مکمل ملاحظہ کی گئی فہرست حاصل کرنے سے پہلے۔
کال کرنا getVisits اس پر بھروسہ کرنے کے بجائے search کیونکہ تنہا رہنا ضروری ہے۔ chrome.history.search ایک کال کے طور پر پرکشش ہے، یہ URL کے تمام دوروں کو کم کر دیتا ہے۔ سب سے حالیہ ایک اگر آپ کسی چیز کو ڈیبگ کرتے ہوئے دو دنوں میں تین بار اسی اسٹیک اوور فلو جواب کو دیکھ چکے ہیں، search آپ کو اگلے حصے میں تینوں کی ضرورت ہوگی، جو ایک قطار فراہم کرتا ہے اور واقعات کو سیشنز میں تقسیم کرتا ہے۔ یہ "3 دن پہلے ایک وزٹ" اور "مسلسل ڈیبگنگ سیشن" کے درمیان فرق ہے۔
getVisits یہ ٹائم اسٹیمپ کی مکمل فہرست دیتا ہے، لیکن یہ واپس آتا ہے: ہر چونکہ تاریخ کی حد سے قطع نظر URL کی تاریخ ہے، visitsForItem فلٹرنگ کا معیار startTime خود اور کیونکہ chrome.history.search ضرورت سے زیادہ براؤزر کی تاریخ کی وجہ سے بیک فلنگ دسیوں ہزار آئٹمز واپس کر سکتی ہے۔ getVisits بیچوں میں CONCURRENCYسب کچھ ایک ساتھ چلانے کے بجائے، اسے 50 پر سیٹ کریں۔ کروم ایک ساتھ عمل درآمد پر سخت حدود کو دستاویز نہیں کرتا ہے۔ getVisits لیکن اگر آپ ایک وقت میں 50 اڑتے ہیں، تو آپ اب بھی بہہ جانے کے بغیر جواب دے سکتے ہیں۔
چوکی
آپ عام طور پر براؤزنگ اور دیکھ کر لائیو کیپچر کو چیک کر سکتے ہیں۔ raw_events fill : کھولنا chrome://extensionsاوپن لوپس کارڈ میں، "سروس ورکر" پر کلک کریں اور پھر درخواست ٹیب → انڈیکسڈ ڈی بی → openloops → raw_eventsیہاں ہر ایک قطار ہے۔ RawEvent کے ساتھ source: "live".
backfillHistory اس کا ابھی تک خود سے کوئی UI نہیں ہے، لیکن جب ہم پارٹ 13 میں ڈیش بورڈ ریل بناتے ہیں، تو ہم اسے "Scan My History" بٹن سے جوڑ دیں گے۔ ابھی کے لیے صرف مرتب کرنا ہی کافی ہے۔ raw_events یہ لائیو کیپچر سے بھرا ہوا ہے۔ اگلے حصے میں، ہم اس خام دھارے کو ایک سٹرکچرڈ سیشن، یا سیشن میں تبدیل کرنا شروع کر دیتے ہیں۔
شور کو سیشن میں کیسے تبدیل کریں۔
آپ کی اصل براؤزنگ ہسٹری ایسی سرگرمی سے بھری ہوئی ہے جس کا اس سے کوئی تعلق نہیں ہے جو آپ اصل میں کرنے کی کوشش کر رہے تھے۔ تحقیق کے ایک دوپہر کو Gmail، Slack، یا YouTube کے درجنوں وزٹس کے ذریعے وقفہ دیا جا سکتا ہے، جن کے عنوان سے صفحات "نیا ٹیب" یا "ڈیش بورڈ" ہیں کیونکہ براؤزر میں ریکارڈ ہونے پر صفحہ لوڈنگ ختم نہیں ہوا تھا۔
اس سے پہلے کہ اس میں سے کسی کو بھی بامعنی چیز میں گروپ کیا جائے، دو چیزیں ضروری ہیں۔ یعنی، شور کو فلٹر کیا جانا چاہیے، اور باقی کو سیشنز میں تقسیم کیا جانا چاہیے، یعنی وقت کے وقفوں سے الگ ہونے والی سرگرمی کی مسلسل حدود۔
اس سیکشن میں، ہم ایک چھوٹے مطلوبہ الفاظ کے ایکسٹریکٹر کے ساتھ ان دونوں مراحل کو بناتے ہیں جسے ہر سیشن اپنے مواد کو بیان کرنے کے لیے استعمال کرتا ہے۔ کیونکہ یہی وضاحت بعد میں کلسٹرنگ کو مضبوط کرتی ہے۔
شور فلٹرنگ
بنانا src/pipeline/noise.ts:
import type { RawEvent } from "../types";
import { isHttpUrl, isLocalHost } from "../lib/util";
export const BLOCKED_DOMAINS: readonly string[] = [
"mail.google.com",
"outlook.live.com",
"outlook.office.com",
"calendar.google.com",
"slack.com",
"app.slack.com",
"discord.com",
"web.whatsapp.com",
"teams.microsoft.com",
"messenger.com",
];
export const ADULT_DOMAINS: readonly string[] = [
"xvideos.com",
"pornhub.com",
"xnxx.com",
"xhamster.com",
"redtube.com",
"youporn.com",
"spankbang.com",
];
export const JUNK_DOMAINS: readonly string[] = [
"trk.myperfect2give.com",
"t.buenotraffic.com",
"bwredir.com",
"osom.saintscommunity.net",
];
const ALL_BLOCKED = [...BLOCKED_DOMAINS, ...ADULT_DOMAINS, ...JUNK_DOMAINS];
function domainIsBlocked(domain: string): boolean {
return ALL_BLOCKED.some(
(blocked) => domain === blocked || domain.endsWith("." + blocked)
);
}
export const NOISE_TITLE_PREFIXES: readonly string[] = [
"new tab",
"new chat",
"untitled",
"inbox",
"home",
"dashboard",
"sign in",
"log in",
"loading",
];
function titleIsGeneric(title: string, domain: string): boolean {
if (title.trim() === "") return true;
if (title.toLowerCase() === domain.toLowerCase()) return true;
const lower = title.toLowerCase();
return NOISE_TITLE_PREFIXES.some((prefix) => lower.startsWith(prefix));
}
export function isNoise(event: RawEvent): boolean {
if (!isHttpUrl(event.url)) return true;
if (isLocalHost(event.domain)) return true;
return domainIsBlocked(event.domain) || titleIsGeneric(event.title, event.domain);
}
isNoise یہ ایک واحد فنکشن ہے جسے باقی پائپ لائن کال کرتی ہے، ایک دوسرے کے اوپر چار چیک اسٹیک ہوتے ہیں جو مختلف قسم کے شور کو پکڑتے ہیں۔
پہلے دو چیک پچھلے مددگاروں کو دوبارہ استعمال کرتے ہیں۔ isHttpUrl اور isLocalHost کسی بھی چیز کو حذف کریں جو حقیقی ویب صفحہ نہیں ہے یا آپ کے مقامی ترقیاتی سرور کی طرف اشارہ کرتا ہے۔ یہ وہی فلٹر ہے جو پہلے ہی آپ کی گرفتاری کی حفاظت کر رہا ہے۔ جو ہم یہاں دوبارہ دیکھتے ہیں وہ جان بوجھ کر بیلٹ اور معطل کرنے والی کارروائی ہے۔ raw_events اگر یہ کیپچر چیک پاس نہیں کرتا ہے، تب بھی آپ سیشن میں داخل نہیں ہو پائیں گے۔
BLOCKED_DOMAINS مواصلات اور پیداواری ٹولز جیسے Gmail، Slack، Discord، WhatsApp Web، اور مزید کا احاطہ کرتا ہے۔ یہ ایک ایسا آلہ ہے جس کا مسلسل دورہ کیا جاتا ہے لیکن اس کا اپنا کوئی تحقیقی ارادہ نہیں ہے۔ domainIsBlocked چونکہ عین ڈومین اور تمام ذیلی ڈومینز دونوں مماثل ہیں، slack.com فہرست میں بھی app.slack.com. ADULT_DOMAINS اور JUNK_DOMAINS یہ ایک متعلقہ وجہ سے موجود ہے، بالغوں کے مواد اور معلوم ٹریکر کو مکمل طور پر خارج کرنے یا تھریڈز سے ڈومینز کو ری ڈائریکٹ کرنے کے لیے۔
BLOCKED_DOMAINS یہ ایک کیوریٹڈ سٹیٹک لسٹ ہے، جو بعد میں اس گائیڈ میں دوسرے فریکوئنسی پر مبنی ڈیٹیکٹر کے ساتھ ضمیمہ کی گئی ہے۔ ambient.ts. یہ تمام ڈومینز کو حذف کر دے گا جو تقریباً ہر سیشن میں ظاہر ہوتے ہیں، قطع نظر اس کے کہ وہ ڈومین اصل میں کیا ہیں۔
آخری چیک تھا، titleIsGenericغیر مددگار عنوانات والے صفحات کو پکڑتا ہے۔ یعنی، ایک خالی عنوان، ایک عنوان جو آپ کے ڈومین نام سے ملتا جلتا ہے، یا ایک عنوان جو عام سابقہ جیسے "نیا ٹیب"، "ڈیش بورڈ"، "لوڈنگ..." یا "سائن ان" سے شروع ہوتا ہے۔ NOISE_TITLE_PREFIXES چونکہ یہ چھوٹے حروف کے عنوان کے آغاز سے میل کھاتا ہے، "ڈیش بورڈ | ورسل" کو پہلے سے طے شدہ "ڈیش بورڈ" کے بالکل آگے چھوڑ دیا جاتا ہے، جبکہ اسی ڈومین سے مواد سے بھرپور عنوانات کو اسی طرح رکھا جاتا ہے۔
بنانا src/pipeline/keywords.ts. یہ NLP نہیں ہے، یہ سٹاپ الفاظ کو ہٹانے کے بعد تعدد کا حساب ہے۔ یہ متعلقہ تلاش کے سیشنز میں "ٹائپ اسکرپٹ جنرکس" یا "رییکٹ ہکس" جیسی چیزوں کو سامنے لانے کے لیے کافی ہے۔
import { BLOCKED_DOMAINS } from "./noise";
export const STOPWORDS: ReadonlySet = new Set([
"the", "and", "for", "with", "you", "your", "how", "what", "this", "that",
"from", "are", "was", "not", "but", "all", "can", "has", "have", "will",
"its", "out", "one", "get", "our", "had", "just", "about", "also", "more",
"into", "than", "then", "when", "their", "there", "which", "would", "been",
"his", "her", "who", "they", "she", "him", "now", "any", "way", "use",
"using", "used", "make", "made",
"google", "youtube", "search", "chat", "new", "home", "www", "com", "org",
"net", "page", "site", "tab", "view", "app", "log", "sign", "login",
"official", "free", "online", "best", "top", "open",
]);
export const PLATFORM_STOPWORDS: ReadonlySet = new Set([
"instagram", "facebook", "youtube", "claude", "google", "linkedin",
"twitter", "reddit", "netflix", "amazon", "gmail", "whatsapp", "tiktok",
"messenger",
"stories", "story", "reel", "reels", "shorts", "short", "feed", "watch",
"video", "videos", "music", "post", "posts", "message", "messages",
"dm", "dms", "notification", "notifications", "profile", "home", "login",
"signin", "follow", "followers",
]);
function derivedDomainLabels(): Set {
const labels = new Set();
for (const domain of BLOCKED_DOMAINS) {
const label = domain.split(".").at(-2);
if (label) labels.add(label);
}
return labels;
}
const ALL_STOP_TOKENS: ReadonlySet = new Set([
...STOPWORDS,
...PLATFORM_STOPWORDS,
...derivedDomainLabels(),
]);
export function extractKeywords(titles: string[], max = 8): string[] {
const freq = new Map();
for (const title of titles) {
const tokens = title.toLowerCase().split(/[^a-z0-9]+/);
for (const token of tokens) {
if (token.length < 3) continue;
if (/^d+$/.test(token)) continue;
if (ALL_STOP_TOKENS.has(token)) continue;
freq.set(token, (freq.get(token) ?? 0) + 1);
}
}
return [...freq.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, max)
.map(([token]) => token);
}
extractKeywords یہ واقعات کے ایک گروپ سے صفحہ کا عنوان لیتا ہے، تمام غیر موضوع آئٹمز کو ہٹاتا ہے، اور اکثر آنے والے الفاظ میں سے چند کو واپس کرتا ہے۔ یہ اسٹرپنگ نام "اسٹاپ ورڈ" سے زیادہ کام کرتی ہے۔
STOPWORDS اس میں عام انگریزی فنکشن الفاظ جیسے "the" اور "with" کے ساتھ ساتھ عام سائیٹ کروم جیسے "search"، "login" اور "page" کا احاطہ کیا گیا ہے۔ خود سے، آپ "Instagram" یا "reels" جیسے عنوان میں "Reels · Instagram" جیسے ٹوکن پاس کر سکتے ہیں اور وہ ٹوکن اس سیشن کے کلیدی الفاظ کے طور پر ظاہر ہوں گے۔
وہ خلا ہے۔ PLATFORM_STOPWORDS بند "Reels · Instagram" یا "Watch - YouTube" جیسے عنوانات اس ٹول کی نشاندہی کرتے ہیں جسے آپ استعمال کر رہے تھے، نہ کہ آپ نے اس کے ساتھ کیا کیا۔ تو PLATFORM_STOPWORDS سوشل میڈیا UI کروم جیسے "کہانیاں"، "فیڈ"، "DM"، "اطلاعات" کے ساتھ پلیٹ فارم اور برانڈ کے ناموں کو ہٹا دیں۔ اس فہرست کے بغیر، "Instagram" یا "View" جیسے کلیدی الفاظ سماجی پلیٹ فارمز کے سیشنز سے نکالے جاتے ہیں۔ چونکہ تمام سوشل میڈیا سیشنز ایک بے معنی کلیدی لفظ کا اشتراک کرتے ہیں، کلسٹرنگ کے دوران یہ تھریڈ ٹائٹل بن جاتا ہے جو خاموشی سے غیر متعلقہ سیشنز کو اکٹھا کرتا ہے۔
derivedDomainLabels اسٹاپ ورڈز کے تیسرے ماخذ کو خود بخود مطابقت پذیر رکھتا ہے۔ BLOCKED_DOMAINSاپنے ٹاپ لیول ڈومین سے فوراً پہلے لیبل استعمال کریں۔ تو mail.google.com بن جاتا ہے۔ google اور web.whatsapp.com بن جاتا ہے۔ whatsapp. اگر آپ بعد میں اس بلاک لسٹ میں نئے ڈومینز شامل کرتے ہیں، تو آپ بغیر کسی اضافی ریکارڈ کے ان ناموں کو اپنے مطلوبہ الفاظ کو آلودہ کرنے سے روک سکتے ہیں۔
اگر ماڈیول لوڈ کرتے وقت تینوں سیٹوں کو ایک بار ملا دیا جائے۔ ALL_STOP_TOKENS, extractKeywords یہ بذات خود سادہ ہے۔ تمام عنوانات کو چھوٹے حروف میں بنائیں، غیر حرفی یا عددی اندراجات میں تقسیم کریں، 3 حروف سے چھوٹے یا مکمل طور پر اعداد پر مشتمل ٹوکنز کو ہٹا دیں، اور تمام اندراجات کو الگ کریں ALL_STOP_TOKENS. اس کے بعد یہ باقی اشیاء کو شمار کرتا ہے اور سب سے زیادہ بار بار آنے والی اشیاء کو واپس کرتا ہے۔
سیشنز کے لیے ڈیٹا بیس کی توسیع
سیشنز کو رہنے کے لیے جگہ کی ضرورت ہوتی ہے۔ پہلے اس گائیڈ میں src/db/index.ts آپ نے ابھی ایک اسکیما کی وضاحت کی ہے۔ raw_events ورژن 1 سے۔ sessions ورژن محفوظ کریں اور اسے 2 میں تبدیل کریں۔
پہلے اسکیما کو بڑھا دیں۔ upgrade کال بیک:
import type { RawEvent, Session } from "../types";
interface OpenloopsDB extends DBSchema {
raw_events: {
key: string;
value: RawEvent;
indexes: { by_visitedAt: number };
};
sessions: {
key: string;
value: Session;
indexes: { by_startedAt: number };
};
}
const DB_VERSION = 2;
export function getDB(): Promise> {
if (!_db) {
_db = openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains("raw_events")) {
const s = db.createObjectStore("raw_events", { keyPath: "id" });
s.createIndex("by_visitedAt", "visitedAt");
}
if (!db.objectStoreNames.contains("sessions")) {
const s = db.createObjectStore("sessions", { keyPath: "id" });
s.createIndex("by_startedAt", "startedAt");
}
},
});
}
return _db;
}
پھر اپنے سیشن کے لیے ضروری مددگار فنکشنز شامل کریں۔ raw_events ایک مددگار جو آپ پہلے ہی لکھ چکے ہیں۔ وہ ایک ہی شکل کی پیروی کرتے ہیں: putSessions عمدگی سے بیچز بنائیں۔ clearSessions دوبارہ تعمیر کرنے سے پہلے اسٹور کو صاف کریں، getAllSessions ترتیب دی گئی تمام اشیاء کو لوٹاتا ہے: startedAt انڈیکس کے ذریعے getSessionCount رقم لوٹاتا ہے۔
export async function putSessions(sessions: Session[]): Promise {
if (sessions.length === 0) return;
const db = await getDB();
const tx = db.transaction("sessions", "readwrite");
await Promise.all([...sessions.map((s) => tx.store.put(s)), tx.done]);
}
export async function clearSessions(): Promise {
const db = await getDB();
return db.clear("sessions");
}
export async function getAllSessions(): Promise {
const db = await getDB();
return db.getAllFromIndex("sessions", "by_startedAt");
}
export async function getSessionCount(): Promise {
const db = await getDB();
return db.count("sessions");
}
کہ if (!db.objectStoreNames.contains(...)) پچھلا تحفظ وہی ہے جو اسے محفوظ بناتا ہے۔ کوئی بھی جس کے پاس پہلے سے ہی ورژن 1 ڈیٹا بیس ہے۔ raw_events حقیقی ڈیٹا سے بھرا ہوا، آپ کو کچھ نیا ملتا ہے۔ sessions پہلے سے موجود چیزوں کو چھوئے بغیر اسے اوپر شامل کریں۔
واقعات کو سیشنز میں تقسیم کریں۔
سیشن نیویگیشن سرگرمی کا ایک مسلسل بلاک ہوتا ہے، جب بھی دو لگاتار واقعات کے درمیان وقفہ ختم ہو جاتا ہے تو ایک نیا سیشن شروع ہوتا ہے۔ SESSION_GAP_MS. بنانا src/pipeline/sessions.ts:
import { getAllEvents, clearSessions, putSessions } from "../db/index";
import { isNoise } from "./noise";
import { extractKeywords } from "./keywords";
import { hashId } from "../lib/util";
import type { RawEvent, Session } from "../types";
const SESSION_GAP_MS = 30 * 60 * 1000;
function rankDomains(events: RawEvent[]): string[] {
const freq = new Map();
for (const e of events) {
freq.set(e.domain, (freq.get(e.domain) ?? 0) + 1);
}
return [...freq.entries()]
.sort((a, b) => b[1] - a[1])
.map(([domain]) => domain);
}
function buildSession(events: RawEvent[]): Session {
const startedAt = events[0].visitedAt;
const endedAt = events[events.length - 1].visitedAt;
return {
id: hashId(events[0].url, startedAt),
events,
startedAt,
endedAt,
domains: rankDomains(events),
keywords: extractKeywords(events.map((e) => e.title)),
};
}
export async function buildSessions(): Promise<{ events: number; sessions: number }> {
const allEvents = await getAllEvents();
const meaningful = allEvents.filter((e) => !isNoise(e));
if (meaningful.length === 0) {
await clearSessions();
return { events: 0, sessions: 0 };
}
const sessions: Session[] = [];
let currentGroup: RawEvent[] = [meaningful[0]];
for (let i = 1; i < meaningful.length; i++) {
const gap = meaningful[i].visitedAt - meaningful[i - 1].visitedAt;
if (gap > SESSION_GAP_MS) {
sessions.push(buildSession(currentGroup));
currentGroup = [meaningful[i]];
} else {
currentGroup.push(meaningful[i]);
}
}
sessions.push(buildSession(currentGroup));
const substantive = sessions.filter(
(s) => !(s.events.length === 1 && s.keywords.length === 0)
);
await clearSessions();
await putSessions(substantive);
return { events: meaningful.length, sessions: substantive.length };
}
buildSessions پانچ چیزیں ترتیب سے کریں۔
-
وقت کے لحاظ سے ترتیب دیئے گئے تمام خام واقعات کو لوڈ کرتا ہے۔
-
کچھ بھی چھوڑ دو
isNoiseجھنڈا -
یہ فہرست کے باقی حصوں سے گزرتا ہے اور جب بھی دو لگاتار واقعات کے درمیان وقفہ ختم ہوجاتا ہے تو ایک نیا سیشن شروع ہوتا ہے۔
SESSION_GAP_MS(اور کچھ بھی لوپ کو بند نہیں کرتا ہے، لہذا جب لوپ ختم ہوتا ہے تو یہ آخری جاری گروپ کو دھکیل دیتا ہے۔) -
ایسے سیشنوں کو چھوڑ دیں جو ایک ہی ایونٹس بنتے ہیں بغیر نکالے جانے والے مطلوبہ الفاظ کے (عام طور پر گمراہ صفحہ کسی اور چیز سے غیر منسلک ہوتا ہے)۔
-
نتائج کو برقرار رکھیں۔
ہر سیشن کے domains اور keywords مقامی rankDomains اور extractKeywords صرف اس گروپ میں ایونٹس چلیں گے۔ rankDomains یہ ہر ڈومین سے واقعات کی تعداد شمار کرتا ہے اور تعدد کے لحاظ سے ترتیب دیتا ہے، اس لیے سیشن میں سب سے زیادہ دیکھے جانے والے ڈومین پہلے آتے ہیں۔
کام کی گئی مثالیں "فہرست سے گزرنا" کو ٹھوس بناتی ہیں۔ آئیے پانچ واقعات (A سے E) کو دیکھتے ہیں جنہوں نے شور فلٹرنگ کو منظور کیا۔
A t= 0 min "TypeScript generics - Stack Overflow" stackoverflow.com
B t= 5 min "TypeScript Handbook" typescriptlang.org
C t=10 min "microsoft/TypeScript - GitHub" github.com
↑ gap to D = 45 min > SESSION_GAP_MS (30 min) → SPLIT HERE
D t=55 min "React hooks explained - YouTube" youtube.com
E t=60 min "useEffect cleanup - Stack Overflow" stackoverflow.com
جیسے جیسے لوپ A سے B کی طرف C کی طرف جاتا ہے، ہر ایک وقفہ 30 منٹ کی حد سے کم ہوتا ہے، تینوں کو ایک ہی گروپ میں رکھتے ہوئے۔ C سے D تک چھلانگ لگانے میں 45 منٹ لگتے ہیں۔ SESSION_GAP_MSتو لوپ بند ہے [A, B, C] سیشن 1 کے طور پر D کے ساتھ ایک نیا گروپ شروع کریں۔ D سے E تک کا وقت صرف 5 منٹ ہے، لہذا جب E D میں شامل ہوتا ہے اور لوپ ختم ہوتا ہے، گروپ سیشن 2 بن جاتا ہے۔
سیشن 1 درج ذیل مطلوبہ الفاظ کے ساتھ ٹیگ کیا گیا ہے: typescript اور genericsسیشن 2 کو ٹیگ کیا گیا ہے۔ react اور hooksحالانکہ دونوں سیشن ایک ہی دن ہوئے تھے۔
SESSION_GAP_MS اسے 30 منٹ پر سیٹ کیا گیا ہے کیونکہ یہ وہی ڈیفالٹ ہے جسے Google Analytics اور ملتے جلتے ٹولز استعمال کرتے ہیں اور زیادہ تر براؤزنگ پیٹرن کے لیے موزوں ہے۔
تجارت دونوں طریقوں سے ہوتی ہے۔ چھوٹے وقفوں کے نتیجے میں زیادہ سیشن اور چھوٹے سیشن ہوتے ہیں، جو کلسٹرنگ کو مزید تفصیلی سگنل فراہم کرنے کی اجازت دیتا ہے، لیکن ایک مسلسل کام کے متعدد ٹکڑوں میں تقسیم ہونے کا خطرہ ہوتا ہے۔ لمبے وقفوں کے نتیجے میں کم سیشن ہوتے ہیں اور ایسی سرگرمیوں کے ضم ہونے کا خطرہ ہوتا ہے جن کا اصل میں کوئی تعلق نہیں ہے۔
30 منٹ ایک معقول نقطہ آغاز ہے، ایک قسم کا مستقل جس پر آپ واپس آ سکتے ہیں اور یہ دیکھنے کے بعد ایڈجسٹ کر سکتے ہیں کہ آپ کے تھریڈز کیسے نکلتے ہیں۔
چوکی
buildSessions ابھی تک کوئی UI نہیں ہے۔ اس گائیڈ میں بعد میں اپنا ڈیش بورڈ ڈیزائن کرتے وقت، آپ "میرے ریکارڈز تلاش کریں" کے ساتھ "سیشن بنائیں" کے بٹن سے لنک کریں گے۔
ابھی کے لیے، مقصد یہ ہے کہ اس سیکشن میں ہر چیز کو صاف ستھرا مرتب کیا جائے۔ src/pipeline/noise.ts, src/pipeline/keywords.tsاپ ڈیٹ src/db/index.tsاور src/pipeline/sessions.ts ہر چیز کو غلطیوں کے بغیر بنانا چاہئے۔ getDB() اگلی بار ایکسٹینشن دوبارہ لوڈ ہونے پر، اسے ورژن 2 کی اطلاع دینی چاہیے (ڈیو ٹولز میں دکھائی دے رہا ہے)۔ درخواست → انڈیکسڈ ڈی بی → openloopsدونوں اب ڈیٹا بیس میں درج ہیں۔ raw_events اور sessions بطور آبجیکٹ اسٹور)۔
سیشنز کے تیار ہونے کے بعد، اگلا سیکشن اس سٹرکچرڈ لیکن منقطع ڈیٹا کو لے جائے گا اور سیشنز کو انٹینٹ تھریڈز میں گروپ کرے گا، جو اس پورے پروجیکٹ کا نام ہے۔
سیشنوں کو ارادے کے دھاگوں میں کیسے کلسٹر کریں۔
ایک سیشن ان واقعات کو ایک ساتھ جوڑتا ہے جو وقت کے ساتھ ساتھ قریب واقع ہوتے ہیں۔ لیکن جو آپ واقعی کرنے کی کوشش کر رہے ہیں وہ شاذ و نادر ہی ایک سیشن میں فٹ بیٹھتا ہے۔ لیپ ٹاپ کا موازنہ 4 دنوں میں 3 سیشنز میں کیا جا سکتا ہے۔ جن سوالات کو آپ دیکھنا چاہتے ہیں وہ دو ہفتوں تک ہر چند دنوں میں 10 منٹ کے لیے پاپ اپ ہو سکتے ہیں۔
اس سیکشن میں، ہم متعلقہ سیشنز کو انٹینٹ تھریڈز میں گروپ کرتے ہیں، پھر ہر تھریڈ کو اسکور کرتے ہیں کہ آیا اوپن لوپ کسی حقیقی چیز کی نمائندگی کرتا ہے اور یہ اب بھی کتنا زندہ ہے۔
دو فائلیں یہ کرتی ہیں۔ src/pipeline/ambient.ts یہ ایسے ڈومینز کا پتہ لگاتا ہے جو مخصوص ارادے کے بجائے آپ کے روزمرہ کے معمولات کا حصہ ہیں، اس لیے یہ غیر متعلقہ سیشنز کے درمیان غلط مماثلت نہیں پیدا کرتا ہے۔ src/pipeline/threads.ts اصل کلسٹرنگ اور اسکورنگ انجام دیں۔
قریبی ڈومین کا پتہ لگانا
کچھ ڈومینز تقریباً ہر سیشن میں ظاہر ہوتے ہیں، قطع نظر اس کے کہ آپ کیا کر رہے ہیں۔ youtube.com کو بیک گراؤنڈ شور کے طور پر استعمال کریں، ہر روز کمٹ کرنے والے ڈیولپرز کے لیے github.com، اور claude.ai اپنے جنرل اسسٹنٹ کے طور پر استعمال کریں۔ اگر ان ڈومینز کے سیشنز کا کلسٹرنگ اسی طرح موازنہ کرتا ہے جس طرح یہ کسی دوسرے ڈومین کے سیشنز کا موازنہ کرتا ہے، تو دو مکمل طور پر غیر متعلقہ سیشنز ایک جیسے نظر آئیں گے کیونکہ وہ دونوں youtube.com کو مارتے ہیں، اور سب کچھ آخر کار ایک بڑے دھاگے میں ضم ہو جائے گا۔
ambient.ts ہم تعدد کی جانچ کرکے اس مسئلے کو حل کرتے ہیں۔ موضوع سے قطع نظر، اگر کوئی ڈومین اپنی سرگرمی کی مدت کے کافی بڑے حصے میں ظاہر ہوتا ہے، تو یہ ایک پیریفرل ڈومین ہے۔
بنانا src/pipeline/ambient.ts:
import type { Session } from "../types";
export const UBIQUITY_THRESHOLD = 0.6;
export const MIN_ACTIVE_DAYS = 3;
function toDay(epochMs: number): string {
return new Date(epochMs).toDateString();
}
export function detectAmbientDomains(sessions: Session[]): Set {
const allEvents = sessions.flatMap((s) => s.events);
const activeDays = new Set(allEvents.map((e) => toDay(e.visitedAt)));
const totalActiveDays = activeDays.size;
if (totalActiveDays < MIN_ACTIVE_DAYS) {
return new Set();
}
const domainDayMap = new Map>();
for (const event of allEvents) {
const day = toDay(event.visitedAt);
if (!domainDayMap.has(event.domain)) {
domainDayMap.set(event.domain, new Set());
}
domainDayMap.get(event.domain)!.add(day);
}
const ambient = new Set();
for (const [domain, days] of domainDayMap) {
const ubiquity = days.size / totalActiveDays;
if (ubiquity >= UBIQUITY_THRESHOLD) {
ambient.add(domain);
console.log(
`[openloops] ambient: ({domain} (){days.size}/({totalActiveDays} days, ubiquity=){ubiquity.toFixed(2)})`
);
}
}
return ambient;
}
toDay یہ کیلنڈر کی تاریخ کے تاروں میں ٹائم اسٹیمپ کو کم کر دیتا ہے، اس لیے ایک ہی دن کے دو واقعات عین وقت سے قطع نظر ایک ہی کلید پیدا کریں گے۔
detectAmbientDomains پہلے، ہم حساب لگاتے ہیں کہ براؤزنگ کی سرگرمی کتنے دنوں میں تھی۔ دوسرے لفظوں میں totalActiveDays - پھر ہم ہر ڈومین سے تاریخوں کے سیٹ تک ایک نقشہ بناتے ہیں جس پر وہ ڈومین ظاہر ہوتا ہے۔ ڈومینز کی ہر جگہ ہے۔ days.size / totalActiveDaysفعال دنوں کا فیصد جس کے لیے ڈومین نظر آتا ہے۔ زیادہ یا زیادہ UBIQUITY_THRESHOLD 0.6 واپس کیے گئے سیٹ میں شامل کیا جاتا ہے۔
MIN_ACTIVE_DAYS اس کی وجہ یہ ہے کہ صرف ایک یا دو دن کے ڈیٹا کے ساتھ، آپ جو بھی ڈومین دیکھتے ہیں وہ تکنیکی طور پر آپ کے فعال دنوں کے 100% پر ظاہر ہوتا ہے، اور ڈیٹیکٹر ہر چیز کو زیر التواء کے بطور نشان زد کر دیتا ہے۔ اگر فعال دنوں کی تعداد 3 سے کم ہے، تو ہم ایک خالی سیٹ واپس کر دیتے ہیں اور دریافت کو مکمل طور پر چھوڑ دیتے ہیں۔
اس نقطہ نظر کے ساتھ عملی تجارتی تعلقات ہیں۔ اگرچہ یہ اصل پیریفرل ٹولز کی صحیح شناخت کرتا ہے، لیکن یہ ان ڈومینز کو بھی دبا سکتا ہے جن پر ہم ایک ہفتے سے روزانہ بہت زیادہ تحقیق کر رہے ہیں، جو 60% کی حد کو بھی عبور کر لیتے ہیں۔
UBIQUITY_THRESHOLD یہ اس سمجھوتہ کا ہینڈل ہے۔ اونچائی میں اضافہ حقیقی محیطی شور کو دوبارہ پیش کرنے کی قیمت پر غلط مثبت کو کم کرتا ہے۔
ارادے کے دھاگوں کے لیے ڈیٹا بیس کی توسیع
دھاگوں کو ان کی اپنی اسٹوریج کی ضرورت ہے۔ کریش DB_VERSION 3 میں شامل کریں اور intent_threadsانڈیکسڈ lastSeenڈیش بورڈ سب سے پہلے حالیہ فعال تھریڈز کو ظاہر کر سکتا ہے۔
import type { RawEvent, Session, IntentThread } from "../types";
interface OpenloopsDB extends DBSchema {
raw_events: {
key: string;
value: RawEvent;
indexes: { by_visitedAt: number };
};
sessions: {
key: string;
value: Session;
indexes: { by_startedAt: number };
};
intent_threads: {
key: string;
value: IntentThread;
indexes: { by_lastSeen: number };
};
}
const DB_VERSION = 3;
export function getDB(): Promise> {
if (!_db) {
_db = openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains("raw_events")) {
const s = db.createObjectStore("raw_events", { keyPath: "id" });
s.createIndex("by_visitedAt", "visitedAt");
}
if (!db.objectStoreNames.contains("sessions")) {
const s = db.createObjectStore("sessions", { keyPath: "id" });
s.createIndex("by_startedAt", "startedAt");
}
if (!db.objectStoreNames.contains("intent_threads")) {
const s = db.createObjectStore("intent_threads", { keyPath: "id" });
s.createIndex("by_lastSeen", "lastSeen");
}
},
});
}
return _db;
}
پھر مماثل مددگار شامل کریں۔
export async function putThreads(threads: IntentThread[]): Promise {
if (threads.length === 0) return;
const db = await getDB();
const tx = db.transaction("intent_threads", "readwrite");
await Promise.all([...threads.map(
}
export async function clearThreads(): Promise {
const db = await getDB();
return db.clear("intent_threads");
}
export async function getAllThreads(): Promise {
const db = await getDB();
const index = db
.transaction("intent_threads", "readonly")
.store.index("by_lastSeen");
let cursor = await index.openCursor(null, "prev");
const results: IntentThread[] = [];
while (cursor) {
results.push(cursor.value);
cursor = await cursor.continue();
}
return results;
}
export async function getThreadCount(): Promise {
const db = await getDB();
return db.count("intent_threads");
}
putThreads, clearThreadsاور getThreadCount یہ اسی طرز کی پیروی کرتا ہے۔ sessions وہ مددگار جس نے پہلے میری مدد کی تھی۔ getAllThreads یہاں عجیب بات ہے: بجائے getAllFromIndexاگر یہ صرف چڑھتے ہوئے ترتیب دیتا ہے، تو کرسر کھل جاتا ہے۔ by_lastSeen کو "prev" سمت کی وضاحت کریں اور دستی طور پر منتقل کریں۔ اس سے آپ کو آرڈر کیے گئے تھریڈز ملیں گے، جو حال ہی میں فعال سے شروع ہو رہے ہیں، یعنی وہ آرڈر جو ڈیش بورڈ اسٹیٹس گروپ کارڈز کے لیے چاہتا ہے۔
دھاگوں میں کلسٹر سیشنز
ایک بار جب آس پاس کے ڈومینز کی شناخت ہوجائے src/pipeline/threads.ts اب اصل کام آتا ہے۔ اس کا مطلب ہے سیشنز کو تھریڈز میں گروپ کرنا اور پھر ہر سیشن کو اسکور کرنا اور درجہ بندی کرنا۔
نقطہ نظر لالچی اجتماعی کلسٹرنگ ہے۔ تاریخ کے مطابق سیشنز کے ذریعے جائیں اور ہر سیشن کے لیے سب سے زیادہ ملتے جلتے موجودہ تھریڈ میں ضم ہو جائیں یا اگر کچھ ملتا جلتا نہ ہو تو نیا تھریڈ شروع کریں۔
ہم درآمد، پیمانہ مستقل، اور مماثلت کے حساب سے شروع کرتے ہیں۔
import { getAllSessions, clearThreads, putThreads } from "../db/index";
import { detectAmbientDomains } from "./ambient";
import { hashId } from "../lib/util";
import type { Session, IntentThread } from "../types";
export const SIMILARITY_THRESHOLD = 0.15;
export const DOMAIN_WEIGHT = 0.5;
export const KEYWORD_WEIGHT = 0.5;
interface ThreadBuilder {
id: string;
sessions: Session[];
domainSet: Set;
keywordSet: Set;
}
function jaccard(a: Set, b: Set): number {
if (a.size === 0 && b.size === 0) return 0;
let intersection = 0;
for (const item of a) {
if (b.has(item)) intersection++;
}
const union = a.size + b.size - intersection;
return intersection / union;
}
function similarity(
session: Session,
thread: ThreadBuilder,
ambient: Set
): number {
const sessionDomains = new Set(session.domains.filter((d) => !ambient.has(d)));
const threadDomains = new Set([...thread.domainSet].filter((d) => !ambient.has(d)));
const sessionKeywords = new Set(session.keywords);
const domainScore = jaccard(sessionDomains, threadDomains);
const keywordScore = jaccard(sessionKeywords, thread.keywordSet);
return DOMAIN_WEIGHT * domainScore + KEYWORD_WEIGHT * keywordScore;
}
ThreadBuilder ایک تغیر پذیر جمع کرنے والا جو صرف کلسٹرنگ کے دوران استعمال ہوتا ہے۔ یعنی یہ اس دھاگے کا امتزاج ہے جس میں سیشن ہو رہا ہے اور اب تک دیکھے گئے تمام ڈومینز اور کلیدی الفاظ۔ jaccard معیاری سیٹ مماثلت کا پیمانہ چوراہے کا سائز ہے جسے یونین کے سائز سے تقسیم کیا جاتا ہے، جو 0 کو 0 سے تقسیم کرنے کے بجائے دو خالی سیٹوں کے لیے 0 لوٹاتا ہے۔
similarity ایک امیدوار کے سیشن کا ایک جاری تھریڈ سے موازنہ کرتا ہے۔ اشتراک کیا گیا کیونکہ دونوں فریق ڈومینز کا موازنہ کرنے سے پہلے پڑوسی ڈومینز کو فلٹر کرتے ہیں۔ youtube.com یہ آپ کے سکور میں کبھی حصہ نہیں ڈالتا ہے۔ اس کے بعد ہم ڈومین Jaccard سکور اور کلیدی لفظ Jaccard سکور کا الگ الگ حساب لگاتے ہیں اور ان کو اس کے ساتھ جوڑتے ہیں: DOMAIN_WEIGHT اور KEYWORD_WEIGHTدونوں 0.5 ہیں، جس کا کہنا ہے کہ ڈومین ڈپلیکیشن اور کلیدی الفاظ کی نقل حتمی نمبر میں برابر ہیں۔
اگلا، کلسٹرنگ لوپ خود ہے:
function clusterSessions(
sessions: Session[],
ambient: Set
): ThreadBuilder[] {
const threads: ThreadBuilder[] = [];
for (const session of sessions) {
let bestThread: ThreadBuilder | null = null;
let bestScore = 0;
for (const thread of threads) {
const score = similarity(session, thread, ambient);
if (score > bestScore) {
bestScore = score;
bestThread = thread;
}
}
if (bestThread && bestScore >= SIMILARITY_THRESHOLD) {
bestThread.sessions.push(session);
for (const d of session.domains) bestThread.domainSet.add(d);
for (const k of session.keywords) bestThread.keywordSet.add(k);
} else {
threads.push({
id: hashId(session.id, session.startedAt),
sessions: [session],
domainSet: new Set(session.domains),
keywordSet: new Set(session.keywords),
});
}
}
return threads;
}
clusterSessions پر بھروسہ کریں sessions یہ پہلے ہی تاریخ کے لحاظ سے منظم ہے۔ getAllSessions انڈیکس کے ذریعے گارنٹی شدہ۔ ہر سیشن میں، ہم اب تک بنائے گئے تمام تھریڈز کو اسکور کرتے ہیں اور بہترین میچ رکھتے ہیں۔
ایک بار جب وہ اعلی اسکور صاف ہوجاتا ہے۔ SIMILARITY_THRESHOLDسیشنز کو ضم کر دیا جاتا ہے اور ان کے ڈومینز اور کلیدی الفاظ تھریڈز کے مجموعی سیٹ میں شامل ہوتے ہیں۔ اس کا مطلب ہے کہ بعد کے سیشنز کا موازنہ تھریڈ کے سیشن سے کیا جاتا ہے۔ پوری مجموعی تاریخ فراہم کرتا ہے، نہ صرف بیج سیشنز۔ اگر کچھ بھی حد کو صاف نہیں کرتا ہے، تو سیشن ایک نئے دھاگے کا بیج بن جاتا ہے۔
ایک حقیقی دنیا کی مثال سے پتہ چلتا ہے کہ یہ کیسے ہوتا ہے۔ فرض کریں detectAmbientDomains واپسی { youtube.com }تین سیشن مندرجہ ذیل ترتیب میں آتے ہیں:
S1: domains=[stackoverflow.com, typescriptlang.org]
keywords=[typescript, generics, interface, mapped]
S2: domains=[stackoverflow.com, typescriptlang.org, github.com]
keywords=[typescript, generics, utility, types]
S3: domains=[python.org, docs.python.org]
keywords=[python, async, await, coroutine]
S1 پہلے آتا ہے۔ بیج کا دھاگہ A اگر ابھی تک کوئی دھاگہ نہیں ہے۔ domainSet = {stackoverflow.com, typescriptlang.org}, keywordSet = {typescript, generics, interface, mapped}.
S2 تھریڈ A کے خلاف اسکور کیا جاتا ہے۔ نہ ہی سیٹ میں آس پاس کا ماحول شامل ہوتا ہے۔ youtube.com، تو کچھ بھی فلٹر نہیں کیا جاتا ہے۔ ڈومین جیکارڈ ہے۔ |{stackoverflow.com, typescriptlang.org}| / |{stackoverflow.com, typescriptlang.org, github.com}|یا 2/3 ≒ 0.667۔ کلیدی لفظ جیکارڈ |{typescript, generics}| / |{typescript, generics, interface, mapped, utility, types}|یا 2/6 ≒ 0.333۔ مشترکہ مماثلت ہے۔ 0.5 × 0.667 + 0.5 × 0.333 = 0.5آرام سے پیٹ SIMILARITY_THRESHOLD (0.15)، تو S2 کو تھریڈ A میں ضم کر دیا جاتا ہے، اور اس کے سیٹ کو بڑھا کر شامل کیا جاتا ہے: github.com, utilityاور types.
S3 تھریڈ A کے لیے اسکور کیا گیا ہے۔ تھریڈ A اور تھریڈ A کے درمیان بالکل کوئی اوورلیپ نہیں ہے۔ {python.org, docs.python.org} چونکہ وہ تھریڈ A میں ڈومینز یا مطلوبہ الفاظ کے سیٹ کے درمیان ہیں، اس لیے ان کے جیکارڈ اسکور تمام 0 ہیں اور ان کی مشترکہ مماثلت 0 ہے۔ یہ حد سے نیچے ہے، اس لیے S3 ایک نیا تھریڈ B تیار کرتا ہے۔
نتیجہ: تھریڈ اے دو سیشنز میں ٹائپ اسکرپٹ ریسرچ رکھتا ہے، اور تھریڈ بی اپنا ازگر سیشن رکھتا ہے۔
SIMILARITY_THRESHOLD اس فائل میں واحد سب سے اہم مستقل ہے، اور 0.15 اس سے کم ہے جس کا آپ 50/50 وزن والے جیکارڈ سکور کے لیے اندازہ لگاتے ہیں۔ 0.3 جیسی ابتدائی قدر زیادہ اصولی لگتی ہے۔ اس کا مطلب ہے کہ دو سیشنز کو اپنے مشترکہ ڈومینز اور کلیدی الفاظ کا تقریباً 1/3 شیئر کرنا چاہیے اس سے پہلے کہ وہ ایک ہی دھاگے کا حصہ سمجھے جائیں۔
لیکن جب میں اسے اپنی اصل گندی براؤزنگ ہسٹری کے خلاف چلاتا ہوں، تو یہ بہت سارے دھاگوں کو جنم دیتا ہے۔ وہ سیشن جو واضح طور پر ایک ہی مطالعہ کا حصہ تھے لیکن 0.3 کو صاف کرنے کے لیے کافی مطلوبہ الفاظ کا اشتراک نہیں کیا گیا تھا وہ الگ الگ تھریڈز میں تقسیم ہو گئے۔
حد کو 0.15 تک کم کرنا سیشن کو کمزور ہونے دیتا ہے لیکن پھر بھی حقیقی سگنل میں ضم ہو جاتا ہے۔ صرف ایک ڈومین اور ایک کلیدی لفظ کا اشتراک کرنے والے دو سیشنز پہلے ہی 0.15 سے تجاوز کر سکتے ہیں، جس کے نتیجے میں کم مسلسل تھریڈز ہوتے ہیں جو درحقیقت آپ کی براؤزنگ ہسٹری کی ظاہری شکل سے ملتے ہیں۔
یہ ایک قسم کا مستقل ہے جسے آپ پہلے اصولوں سے اخذ کرنے کے بجائے تجرباتی طور پر ایڈجسٹ کرتے ہیں (ایک تھریڈ بنائیں، نتائج چیک کریں اور اسے ایڈجسٹ کریں)۔
buildThreadsاس کے بعد، ہم تمام دھاگوں کے عنوان، قسم، حیثیت، شہرت، اور سرفہرست مطلوبہ الفاظ کے ساتھ ایک ٹیبل پرنٹ کرتے ہیں تاکہ آپ انہیں اپنی آنکھوں سے دیکھ سکیں۔ اگر دو تھریڈز واضح طور پر ایک دوسرے سے تعلق رکھتے ہیں تو کم قدر استعمال کریں۔ SIMILARITY_THRESHOLD. اگر کسی تھریڈ میں ایک سے زیادہ عنوانات ایک دوسرے سے جڑے ہوئے ہیں جو واضح طور پر ایک دوسرے سے متعلق نہیں ہیں تو اس تھریڈ کو بڑھائیں۔
تھریڈ اسکورنگ اور درجہ بندی
کلسٹرنگ سیشن گروپس بناتی ہے، لیکن سیشن گروپس ابھی تک نہیں ہیں۔ IntentThread. باقی threads.ts ہر گروپ کو ایک میں تبدیل کرتا ہے جس میں انسانی پڑھنے کے قابل سگنلز کا ایک سیٹ ہوتا ہے جو اس کی قسم، اعتماد کا اسکور، حیثیت اور وجہ بیان کرتا ہے۔
چند چھوٹے مددگار پہلے آتے ہیں۔
export const BUYING_WORDS: readonly string[] = [
"vs", "versus", "alternative", "alternatives",
"comparison", "pricing", "price", "review", "reviews", "best",
];
export const LEARNING_WORDS: readonly string[] = [
"how to", "tutorial", "tutorials", "docs", "documentation",
"guide", "learn", "example", "examples", "crash course", "introduction",
];
const STATUS_ACTIVE_MS = 48 * 60 * 60 * 1000;
const STATUS_STALLED_MS = 7 * 24 * 60 * 60 * 1000;
function toTitleCase(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function findMatches(titles: string[], wordList: readonly string[]): string[] {
const lower = titles.map(
const found = new Set();
for (const word of wordList) {
const isPhrase = word.includes(" ");
for (const title of lower) {
if (isPhrase) {
if (title.includes(word)) found.add(word);
} else {
const tokens = title.split(/[^a-z0-9]+/);
if (tokens.includes(word)) found.add(word);
}
}
}
return [...found];
}
function toCalendarDay(epochMs: number): string {
return new Date(epochMs).toDateString();
}
BUYING_WORDS اور LEARNING_WORDS یہ ایک چھوٹی ذخیرہ الفاظ ہے جو ارادے کا اظہار کرتی ہے۔ findMatches یہ ان الفاظ میں سے کسی ایک کے خلاف صفحہ کے عنوانات کی فہرست کو چیک کرتا ہے اور ایک ہی الفاظ اور فقرے کو مختلف طریقے سے دیکھتا ہے۔ کثیر لفظی آئٹمز جیسے "طریقہ" کو ذیلی اسٹرنگ کے طور پر حل کیا جاتا ہے کیونکہ وہ کافی مخصوص ہیں کہ غلط مثبت ہونے کا امکان نہیں ہے۔ تاہم، واحد الفاظ جیسے "جائزہ" کو عنوان سے غیر حروفِ عددی حروف سے الگ کیا جاتا ہے اور مکمل ٹوکن کے طور پر حل کیا جاتا ہے۔
اس امتیاز کے بغیر، "جائزہ" بھی "مجموعی جائزہ" کے اندر مماثل ہو جائے گا، جس کے نتیجے میں "مجموعی جائزہ" صفحہ سے متعلق دھاگوں کی غلط درجہ بندی ہو جائے گی۔ toTitleCase اور toCalendarDay اگلا ایک چھوٹا فارمیٹنگ مددگار ہے جو گریڈنگ فنکشن میں استعمال ہوتا ہے۔
سکور کی تقریب، scoreThreadپروجیکٹ کی سب سے طویل خصوصیت ہے کیونکہ یہ وہ جگہ ہے جہاں اب تک جمع کیے گئے تمام سگنلز کو فیلڈز میں تبدیل کیا جاتا ہے۔ IntentThread:
function scoreThread(builder: ThreadBuilder): IntentThread {
const { sessions, keywordSet } = builder;
const firstSeen = sessions[0].startedAt;
const lastSeen = sessions[sessions.length - 1].endedAt;
const allEvents = sessions.flatMap((s) => s.events);
const totalEvents = allEvents.length;
const daySet = new Set(allEvents.map((e) => toCalendarDay(e.visitedAt)));
const distinctDays = daySet.size;
const allTitles = allEvents.map((e) => e.title);
const buyingMatches = findMatches(allTitles, BUYING_WORDS);
const learningMatches = findMatches(allTitles, LEARNING_WORDS);
let type: IntentThread["type"];
if (buyingMatches.length > 0) {
type = "buying";
} else if (learningMatches.length > 0) {
type = "learning";
} else if (distinctDays > 5 && sessions.length >= 3) {
type = "planning";
} else if (totalEvents >= 3) {
type = "research";
} else {
type = "unclassified";
}
const age = Date.now() - lastSeen;
const status: IntentThread["status"] =
age < STATUS_ACTIVE_MS ? "active" :
age < STATUS_STALLED_MS ? "stalled" :
"dormant";
const confidence = parseFloat((
Math.min(distinctDays / 5, 1) * 0.35 +
Math.min(sessions.length / 5, 1) * 0.25 +
Math.min(totalEvents / 20, 1) * 0.20 +
(type !== "unclassified" ? 1 : 0) * 0.20
).toFixed(2));
const signals: string[] = [];
if (distinctDays > 1)
signals.push(`revisited across ${distinctDays} days`);
if (type === "buying" && buyingMatches.length > 0)
signals.push(`comparison language: ${buyingMatches.join(", ")}`);
if (type === "learning" && learningMatches.length > 0)
signals.push(`learning language: ${learningMatches.join(", ")}`);
signals.push(`({sessions.length} session){sessions.length !== 1 ? "s" : ""}`);
if (totalEvents > 5)
signals.push(`${totalEvents} total events`);
if (type === "planning")
signals.push("sustained activity across many days");
const ageDays = Math.floor(age / (24 * 60 * 60 * 1000));
if (ageDays === 0) signals.push("last active today");
else if (ageDays === 1) signals.push("last active yesterday");
else signals.push(`last active ${ageDays} days ago`);
const title =
[...keywordSet].slice(0, 3).map(toTitleCase).join(" ") || "Untitled Thread";
return {
id: builder.id,
title,
sessions,
type,
confidence,
status,
firstSeen,
lastSeen,
distinctDays,
signals,
};
}
یہاں دیکھنے کے لیے بہت کچھ ہے، اس لیے ہر میدان میں چہل قدمی کرنا قابل قدر ہے۔ IntentThread حسابی ترتیب میں۔
firstSeen اور lastSeen سیدھا باؤنڈری سیشن سے باہر آؤ۔ کیونکہ sessions کلسٹرنگ کے ذریعے تاریخی ترتیب میں پہنچتا ہے۔ distinctDays اسی کیلنڈر دن کی کمی کو دوبارہ استعمال کریں۔ ambient.ts. اس بار، ہم حساب لگاتے ہیں کہ یہ کون سا دن ہے۔ اس تھریڈ میں سرگرمی کے دنوں کی کل تعداد سے قطع نظر ایونٹ جاری رہتا ہے۔
کی طرف سے درجہ بندی type یہ جھلک رہا ہے، لہذا آرڈر اہم ہے۔ تقابلی زبان (BUYING_WORDS) پہلے چیک کیا جاتا ہے۔ اس کی وجہ یہ ہے کہ دو فریم ورک کا موازنہ کرنے والے تھریڈز "خرید رہے ہیں" چاہے ان میں ٹیوٹوریل پیجز بھی شامل ہوں۔ یہ موازنہ کرنے کے مضبوط ارادے کا اشارہ ہے۔
زبان سیکھنا آگے آتا ہے۔ جب سے، planning 5 دن سے زیادہ چلنے والے دھاگوں کے لیے محفوظ ہے۔ اور ایک گہرائی سے تجزیہ کرنے کے بجائے مستقل، تکراری سرگرمی کے کم از کم تین سیشن کریں۔
research اس کا مقصد کسی بھی ایسی چیز کو شامل کرنا ہے جس میں تین یا اس سے زیادہ واقعات شامل ہوں جو کسی خاص چیز سے میل نہیں کھاتے ہیں۔ unclassified جو رہ گیا ہے وہ تھریڈز ہیں جن میں عام طور پر اعتماد کے لیے بہت کم سرگرمی ہوتی ہے۔
status یہ خالصتاً ایک فنکشن ہے کہ یہ کتنا عرصہ پہلے تھا۔ lastSeen پچھلا: 48 گھنٹے سے بھی کم active7 دن سے کم stalledجو بھی پرانا ہے dormant.
confidence یہ چار سگنلز کا وزنی مجموعہ ہے، ہر ایک کو وزن میں ڈالنے سے پہلے زیادہ سے زیادہ 1 پر معمول بنایا جاتا ہے، لہذا کل 1 سے زیادہ نہیں ہو سکتا۔ distinctDays / 5یہ 1 تک محدود ہے، 35% تک حصہ ڈالتا ہے، اور 5 دن سے زیادہ کے انفرادی دنوں کو اس محور پر مکمل طور پر قابل اعتماد سمجھا جاتا ہے۔ sessions.length / 5زیادہ سے زیادہ 1 کا حصہ 25% تک ہے۔ totalEvents / 20زیادہ سے زیادہ 1 کے ساتھ 20% تک تعاون کریں۔ اور چاہے type کے علاوہ unclassified آخری 20% سب یا کچھ بھی نہیں بونس کے طور پر دیا جاتا ہے۔
جن تھریڈز پر 5 دنوں، 5 سیشنز، یا 20 یا اس سے زیادہ ایونٹس پر نظرثانی کی جاتی ہے، اور انہیں صاف طور پر درجہ بندی بھی کی جاتی ہے، انہیں 1.0 کا اسکور ملتا ہے۔ ایک دھاگہ جو دو ایونٹس کے ساتھ واحد سیشن ہے اور صفر کے قریب کوئی درجہ بندی اسکور نہیں ہے۔
signals اعتماد کے اسکور اور حیثیت کا ایک سادہ انگریزی آڈٹ ٹریل۔ یہ بتاتا ہے کہ تھریڈ جیسا نظر آتا ہے، دھاگے پر نظرثانی کیے جانے والے دنوں کی تعداد، موازنہ یا سیکھنے والی زبانیں، سیشنز اور ایونٹس کی تعداد، اور یہ کتنی دیر تک فعال رہا۔ ڈیش بورڈ اس کی براہ راست نمائندگی کرتا ہے۔
آخر کار title پلیس ہولڈر: تھریڈ جمع کرنے میں سرفہرست 3 کلیدی الفاظ۔ keywordSetعنوان کے ساتھ شروع ہوتا ہے اور اسپیس سے جڑا ہوتا ہے، یا "Untitled Thread" اگر کچھ نہیں ہے۔
یہ جان بوجھ کر کمزور ہے۔ AI لیبلنگ اس گائیڈ میں بعد میں اس heuristic عنوان کی جگہ لے لیتا ہے۔ summary اور nextStepتھریڈ کے اصل مواد پر مبنی کچھ ہے (لیکن تھریڈ اس قدم کے بغیر مکمل طور پر قابل استعمال ہیں)۔
ایک ساتھ رکھو
buildThreads یہ اس سیکشن میں ہر چیز کو جوڑتا ہے۔
export async function buildThreads(): Promise<{ sessions: number; threads: number }> {
const sessions = await getAllSessions();
if (sessions.length === 0) {
await clearThreads();
return { sessions: 0, threads: 0 };
}
const ambient = detectAmbientDomains(sessions);
const builders = clusterSessions(sessions, ambient);
const substantive = builders.filter(
(b) => !(b.sessions.length === 1 && b.sessions[0].events.length < 3)
);
const threads = substantive.map(scoreThread);
await clearThreads();
await putThreads(threads);
console.table(
threads.map(
title: t.title,
type: t.type,
status: t.status,
confidence: t.confidence,
distinctDays: t.distinctDays,
sessions: t.sessions.length,
events: t.sessions.reduce((n, s) => n + s.events.length, 0),
keywords: [...new Set(t.sessions.flatMap((s) => s.keywords))].slice(0, 5).join(", "),
}))
);
return { sessions: sessions.length, threads: threads.length };
}
آرڈر یہاں اہم ہے۔ detectAmbientDomains کلسٹرنگ ہونے سے پہلے ہر سیشن میں ایک بار ایمبیئنٹ ڈٹیکشن چلائی جاتی ہے کیونکہ اسے یہ جاننے کے لیے ایکسپلوریشن کی مکمل تصویر درکار ہوتی ہے کہ "روزانہ" کیا سمجھا جاتا ہے۔
clusterSessions پھر پیداوار ThreadBuilders، اسکور کرنے سے پہلے فلٹر کیا گیا: ThreadBuilder بالکل 1 سیشن اور 3 سے کم واقعات تقریباً ہمیشہ ہی گمراہ صفحہ کے بوجھ ہوتے ہیں جو کسی بھی چیز میں ضم نہیں ہوتے ہیں، اس لیے انہیں تقریباً صفر اعتبار کے ساتھ تھریڈز بننے کے بجائے ضائع کر دیا جاتا ہے۔
زندہ رہنے والی ہر چیز کے اسکور یہ ہیں: scoreThreadیہ برقرار رہتا ہے اور اس کے ذریعے پرنٹ کیا جاتا ہے: console.tableیہ ٹیوننگ اسسٹنس فنکشن ہے جس کا پہلے ذکر کیا گیا ہے۔ اسے چلانے کے بعد، اگر آپ سروس ورکر کا کنسول کھولتے ہیں، تو تمام تھریڈز کو ترتیب دینے والے ٹیبل میں رکھا جائے گا۔ یہ تیز ترین طریقہ ہے۔ SIMILARITY_THRESHOLD بہت زیادہ یا بہت کم۔
چوکی
پچھلے دو حصوں کی طرح، buildThreads ابھی تک کوئی UI نہیں ہے۔ بعد میں اس گائیڈ میں آپ کے ڈیش بورڈ کو ڈیزائن کرتے وقت اسے دو دیگر بٹنوں کے ساتھ "Create Intent Map" بٹن سے منسلک کیا جائے گا۔
ابھی کے لیے، درج ذیل کو چیک کریں: src/pipeline/ambient.tsاپ ڈیٹ src/db/index.tsاور src/pipeline/threads.ts ہر چیز غلطیوں کے بغیر بنتی ہے۔ getDB() اگلی بار ایکسٹینشن دوبارہ لوڈ ہونے پر، یہ ورژن 3 کی اطلاع دے گی۔ intent_threads انہیں اب ایک ساتھ درج کیا جانا چاہئے۔ raw_events اور sessions DevTools میں۔
اس مقام پر، پوری بنیادی پائپ لائن بغیر کسی API کلید کے مقامی طور پر، آخر سے آخر تک چلتی ہے۔ تلاش کی سرگزشت خام واقعات بن جاتی ہے، خام واقعات سیشن بن جاتے ہیں، اور سیشنز اسکور کیے جاتے ہیں اور انٹینٹ تھریڈز کی درجہ بندی کرتے ہیں۔
یہاں سے، سب کچھ اختیاری اور اضافی ہے۔ یہ ایک ڈیش بورڈ ہے جو خود حوالہ شور کے ذرائع کو صاف کرتا ہے جن کے ساتھ پائپ لائن نے پہلے ہی نمٹا نہیں ہے (جس کو آپ شاید دیکھنا اور انٹیگریٹ کرنا چاہتے ہیں)، اور پھر AI لیبلنگ، برانڈ گراؤنڈنگ، اور ان سب کو ایک ساتھ باندھنا۔
خود حوالہ شور کو کیسے صاف کریں۔
جب آپ خود اپنی براؤزنگ کے لیے پائپ لائن کو چند بار چلاتے ہیں تو عجیب قسم کے دھاگے نمودار ہونے لگتے ہیں۔ ایک مکمل طور پر کھلی لوپ پر مشتمل ہوتا ہے۔
چونکہ ڈیش بورڈ ایک ویب صفحہ ہے، جب بھی آپ دھاگوں کو چیک کرنے کے لیے ڈیش بورڈ کو کھولتے ہیں، تو اس صفحہ کا بوجھ ایک ایونٹ کے طور پر پکڑا جاتا ہے۔ اگر آپ بھی ایکسٹینشن تیار کر رہے ہیں۔ localhost ڈیٹا میں ترقیاتی سرورز اور کسی بھی نجی نیٹ ورک کے پتے بھی شامل ہیں۔
ٹولز بالآخر اپنے استعمال کا مشاہدہ کریں گے، اور خود حوالہ جات ارادے کے نقشے کو دو الگ الگ طریقوں سے آلودہ کرتے ہیں جو الگ کرنے کے قابل ہیں۔
دو مسائل
پہلا مسئلہ ایکسٹینشن کا اپنا صفحہ ہے۔ کروم ایکسٹینشن کا ڈیش بورڈ اس سے لوڈ ہوتا ہے: chrome-extension:// یو آر ایل اور کروم کے اپنے اندرونی صفحات ہیں۔ chrome://. اگر آپ اوپن لوپس ڈیش بورڈ کو بغیر فلٹر کیے ایک دوپہر میں 10 بار کھولتے ہیں، تو ایک وقت میں 10 ایونٹس تیار ہوں گے۔ chrome-extension:// یہ بنیادی طور پر ایک اصل ہے جو خوشی سے اس دھاگے میں جمع ہو جاتی ہے جسے وہ دیکھ رہا ہے۔
یہ سرکلر اور بیکار ہے، اور تلاش کا بقیہ وقت پرسکون ہوتا ہے، لیکن چونکہ ہم اکثر ڈیش بورڈ کھولتے ہیں، اس لیے یہ سیلف تھریڈ تازگی اور سیشن کی تعداد میں ناقابل یقین حد تک زیادہ سکور کر سکتا ہے۔
دوسرا مسئلہ علاقائی ترقی کے بنیادی ڈھانچے کا ہے۔ اگر آپ ایکسٹینشن یا مقامی پروجیکٹ بنا رہے ہیں، تو اس کی تاریخ درج ذیل ہو گی: localhost:5173, 127.0.0.1:8080اور اس طرح کے LAN ایڈریس بھی ہو سکتے ہیں: 192.168.1.40. جہاں تک کروم کا تعلق ہے، یہ ایک حقیقی صفحہ کا دورہ ہے، لیکن اس معنی میں نیویگیشن کا کوئی ارادہ نہیں ہے جس کی اوپن لوپس کی پرواہ ہے۔ سب سے بری بات یہ ہے کہ یہ برانڈ کی افزودگی کے دوران بعد میں context.dev پر بھیجا جاتا ہے، جس سے کچھ بھی حل نہیں ہوتا اور صرف آپ کے API کریڈٹ ضائع ہوتے ہیں۔
دونوں مسائل کی ایک بنیادی وجہ ہے۔ پائپ لائن ان URLs کو پکڑتی ہے جو اصل میں نیویگیشن کا حصہ نہیں ہیں۔ حل یہ ہے کہ ایک بار اس کی وضاحت کی جائے جسے ایک حقیقی بیرونی ویب صفحہ سمجھا جاتا ہے اور جہاں بھی یو آر ایل یا ڈومین سسٹم میں داخل ہوتا ہے وہاں اس تعریف کو لاگو کریں۔
ایک تعریف، ہر جگہ لاگو ہوتی ہے۔
اس کام میں مدد کے لیے دو مددگار، isHttpUrl اور isLocalHostاسے پہلی تعمیر پر دوبارہ لکھا گیا تھا۔ src/lib/util.ts. ہم نے جان بوجھ کر اسے اسی لمحے کے لیے جلد متعارف کرایا۔
isHttpUrl صرف کے لیے درست لوٹاتا ہے۔ http:// اور https:// خارج کردہ URLs chrome-extension://, chrome://, about:اور file:// سب ایک ساتھ۔ isLocalHost اس کے لیے درست لوٹاتا ہے: localhostلوپ بیک اور نجی IP رینجز .local میزبان کا نام۔
جو چیز اسے موثر بناتی ہے وہ مستقل مزاجی ہے۔ وہی دو خصوصیات تمام انٹری پوائنٹس کی حفاظت کرتی ہیں، اس لیے "حقیقی صفحہ" کی تعریف پائپ لائن کے ایک حصے اور دوسرے حصے کے درمیان منتقل نہیں ہو سکتی۔ اس طرح کے داخلے کے تین مقامات ہیں:
لائیو کیپچر، src/background.tsفون کال isHttpUrl ریکارڈنگ سے پہلے:
if (!isHttpUrl(url)) return;
بیک فل، src/pipeline/backfill.tsوزٹ امپورٹ کرنے سے پہلے تاریخ کے تمام اندراجات پر ایک ہی گارڈ لگائیں۔
if (!item.url) return [];
if (!isHttpUrl(item.url)) return [];
اور شور فلٹر ہے src/pipeline/noise.tsسب سے اوپر دونوں مددگاروں کو چیک کریں۔ isNoiseڈومین یا ٹائٹل رول چلنے سے پہلے:
export function isNoise(event: RawEvent): boolean {
if (!isHttpUrl(event.url)) return true;
if (isLocalHost(event.domain)) return true;
return domainIsBlocked(event.domain) || titleIsGeneric(event.title, event.domain);
}
کیپچر اور بیک فل کو چیک کریں کیونکہ یہ پہلے ہی غیر ویب یو آر ایل کو روکتا ہے۔ isHttpUrl تیسری بار میں isNoise یہ بے کار لگ سکتا ہے، لیکن عام آپریشن کے تحت یہ ہے. تیسرا چیک گارنٹی ہے۔ جب کوئی نان ویب ویب ایونٹ آتا ہے۔ raw_events یہ اب بھی غیر متوقع راستوں کے ذریعے سیشن میں زندہ نہیں رہ سکتا ہے (مثلاً مستقبل کی گرفتاری کے طریقہ کار، درآمد شدہ ڈیٹا، یا کیڑے)۔
ہر قدم اپنے ان پٹ کا دفاع کرتا ہے بجائے اس پر بھروسہ کرنے کے کہ پچھلے مرحلے نے اپنا کام کیا ہے۔ یہ ایک گمشدہ کیس کو خود بخود ارادے کے نقشے پر پھیلانے سے روکنے کے لیے ہے۔
ارتکاز سرحدی دفاع
ایک ہی isLocalHost جب آپ کا ڈومین context.dev پر منتقل ہو جائے گا تو آپ کو اگلے بلڈ آؤٹ برانڈ بڑھانے کے مرحلے کے دوران ایک اور تصدیق نظر آئے گی۔ یہاں تک کہ اگر یہ ہے isNoise مقامی پتے سیشنائزیشن سے پہلے ہی ہٹا دیے جاتے ہیں، اور نیٹ ورک کال کرنے سے پہلے سختی انہیں دوبارہ فلٹر کر دیتی ہے۔
const unique = [...new Set(domains)].filter((d) => !isLocalHost(d));
Heuristics وہی دفاعی گہرائی والے آئیڈیاز ہیں جو دائرہ میں لاگو ہوتے ہیں جہاں غلطی کی قیمت زیادہ ہوتی ہے۔ ایک مقامی پتہ جو کسی نہ کسی طرح تھریڈ کی ڈومین لسٹ میں آتا ہے UI میں بیکار شور نہیں ہونا چاہیے۔ آپ کو اپنے کمپیوٹر کو API کی درخواست کے حصے کے طور پر کبھی نہیں چھوڑنا چاہیے۔ فلٹرز کو براہ راست نیٹ ورک کے کنارے پر رکھنے کا مطلب یہ ہے کہ گارنٹی برقرار رکھی جاتی ہے اس سے قطع نظر کہ اوپر کی طرف کیا ہوتا ہے۔
چوکی
اپ ڈیٹ شدہ بلڈ کو لوڈ کرنے کے بعد، اوپن لوپس اپنے ارادے کے نقشے میں مزید ظاہر نہیں ہوں گے۔ چیک کرنے کے لیے، ڈیش بورڈ کو کئی بار کھولیں، اصل صفحات کو براؤز کریں، اور پائپ لائن کو دوبارہ بنائیں۔ chrome-extension:// اس کا اپنا دھاگہ غائب ہونا چاہیے، localhost متبادل طور پر، نجی IP ڈومین کو تمام تھریڈز کی ڈومین فہرست میں ظاہر ہونا چاہیے۔
اگر آپ ٹیسٹ کرواتے ہیں۔ raw_events DevTools میں، بیک فل ایونٹس کو صاف اور دوبارہ لکھتا ہے، لیکن لائیو کیپچر شامل کرتا ہے، لہذا آپ اس ترمیم سے پہلے بھی لائیو کیپچر ایونٹس دیکھ سکتے ہیں۔ اگر آپ ایک نیا "میری سرگزشت تلاش کریں" چلاتے ہیں تو اسے حذف کر دیا جائے گا اور دوبارہ آباد کر دیا جائے گا۔ raw_events صاف ستھرا نئے قوانین کے مطابق۔
اب جب کہ پائپ لائن حقیقی بیرونی نیویگیشن کے لیے ایک واضح ارادے کا نقشہ بناتی ہے، اس تھریڈ کو مزید پڑھنے کے قابل بنانا اچھا خیال ہے۔
اب تک، ہر تھریڈ کا عنوان صرف تین ٹاپ کلیدی الفاظ کی ایک تار ہے، جس میں کوئی خلاصہ یا تجویز کردہ اگلے اقدامات نہیں ہیں۔ اگلے حصے میں، ہم کلاڈ کا استعمال کرتے ہوئے پہلی اختیاری کلیدی گیٹ پرت، AI لیبلنگ شامل کرتے ہیں۔
کلاڈ کے ساتھ تھریڈز کو لیبل کرنے کا طریقہ
"Typescript Generics Handbook" کے عنوان سے تھریڈ پڑھنے کے قابل ہے، لیکن یہ کلیدی الفاظ کی تفصیل ہے، وہ نہیں جو آپ کرنے کی کوشش کر رہے ہیں۔ "Learning Advanced Type Systems in TypeScript" وہ قسم کے لیبل ہیں جنہیں لوگ دراصل لکھتے ہیں، اور دونوں کے درمیان فرق وہ خلا ہے جسے یہ حصہ پُر کرتا ہے۔
Claude ہر تھریڈ کے کلیدی الفاظ، ڈومینز، اور نمونے والے صفحہ کے عنوانات کو پڑھتا ہے اور اصل عنوان، ایک جملے کا خلاصہ، ایک خرابی، اور مخصوص اگلے مراحل واپس کرتا ہے۔
یہ اوپن لوپس کا پہلا حصہ ہے جو ایک بیرونی API کو کال کرتا ہے اور اسے کلید کی ضرورت ہوتی ہے۔ ڈیزائن کے بارے میں ہر چیز کو ایک رکاوٹ کی شکل دی جاتی ہے۔ اس کا مطلب یہ ہے کہ درخواستوں کا حقیقی ڈیٹا ہونا ضروری ہے، اور 30 سے 40 تھریڈز ہو سکتے ہیں، ہر تھریڈ میں 12 صفحات کے عنوانات ہیں۔
اس کا ایک سادہ ورژن یہ ہوگا کہ تمام تھریڈز کو ایک درخواست میں بھیجیں اور تمام لیبلز کی دوبارہ درخواست کریں۔ یہ بالکل وہی ہے جو پہلے نفاذ نے کیا۔ لیکن فکس اس طرح ناکام ہو جاتا ہے جو دیکھنے کے قابل ہے کیونکہ یہ پورے سیکشن کا سب سے زیادہ معلوماتی حصہ ہے۔
کلید کو مقامی طور پر محفوظ کریں۔
API کو کال کرنے سے پہلے، ہمیں زندہ رہنے کے لیے کسی جگہ کی ضرورت ہے۔ openloops اسے رکھتا ہے chrome.storage.localیہ کہیں بھی مطابقت پذیر نہیں ہوتا ہے اور آپ کے آلے کو کبھی نہیں چھوڑتا ہے۔ بنانا src/lib/settings.ts:
export async function getApiKey(): Promise {
const result = await chrome.storage.local.get("anthropicApiKey");
return (result.anthropicApiKey as string) ?? null;
}
export async function setApiKey(key: string): Promise {
await chrome.storage.local.set({ anthropicApiKey: key });
}
وہی فائل بعد میں context.dev کلید اور اسسٹنٹ کے ماڈل اور کوشش کی ترجیحات کے لیے متوازی گیٹرز اور سیٹرز کو بڑھاتی ہے، یہ سب اسی شکل کی پیروی کرتے ہیں۔ تو بس اس جوڑے کو سمجھ لینا ہی کافی ہے سب کچھ سمجھنے کے لیے۔
پہلا ورژن اور یہ کیوں ناکام ہوا۔
پہلے لیبلنگ کے نفاذ نے تمام تھریڈز کو ایک ہی درخواست میں کلاڈ کو بھیج دیا۔ تمام 40 تھریڈز کو ایک JSON پے لوڈ میں سیریلائز کرتا ہے، اور اس کے بدلے میں 40 لیبلز کی JSON سرنی کی درخواستیں، تجزیہ کرتا ہے اور واپس لکھتا ہے۔ ابتدائی جانچ کے دوران اس نے 5-6 دھاگوں کے ساتھ بالکل کام کیا، لیکن 30+ دھاگوں کے ساتھ حقیقی ریکارڈ پاس ہونے کے بعد، کچھ بھی تیار نہیں ہوا۔ کوئی غلطی یا استثناء نہیں ہوا، صرف دھاگہ پچھلے مطلوبہ الفاظ کے عنوان کو برقرار رکھتا ہے گویا کوئی لیبلنگ نہیں ہوئی ہے۔
وجہ آؤٹ پٹ ٹوکن ٹرنکیشن تھی۔ درخواست بیان کرتی ہے: max_tokensجواب میں ماڈل کتنا پیدا کر سکتا ہے اس کی ایک بالائی حد ہے، 40 دھاگوں کے عنوانات، خلاصے اور اگلے اقدامات بہت زیادہ پیداوار ہیں۔ جب رسپانس وسط جنریشن کی حد تک پہنچ گیا، JSON سرنی کھلنے کے آدھے راستے پر ٹوٹ گئی۔ [ and thirty complete objects followed by half of the thirty-first and no closing ]. JSON.parse اس تھرو پر، کیچ بلاک نے اسے لاگ کیا اور کچھ بھی واپس نہیں کیا، اور لیبلنگ کو خوبصورتی سے ناکام کرنے اور موجودہ عنوان کو برقرار رکھنے کے لیے ڈیزائن کیا گیا تھا، لہذا ناکامی UI کے لیے پوشیدہ تھی۔
ڈیزائن میں دو تبدیلیاں کی گئیں اور دونوں حتمی کوڈ میں ہیں۔ کام کو چھوٹے بیچوں میں تقسیم کرنا اس بات کو یقینی بناتا ہے کہ کوئی بھی ردعمل اتنا بڑا نہیں ہے کہ اسے چھوٹا کیا جا سکے، اور پارسنگ کو اتنا لچکدار بناتا ہے کہ ایک برا بیچ پوری عمل درآمد کو نہیں روک سکتا۔
بیچ پروسیسنگ کی درخواست کریں۔
بنانا src/pipeline/label.tsہم درخواست کے لحاظ سے بیچ کی خصوصیت کے ساتھ شروع کرتے ہیں۔
import { getAllThreads, putThreads, getAllBrands } from "../db/index";
import type { IntentThread } from "../types";
interface ThreadDescriptor {
id: string;
keywords: string[];
domains: string[];
sampleTitles: string[];
domainContext: string[];
}
interface LabelResult {
id: string;
title: string;
summary: string;
type: string;
nextStep: string;
}
const VALID_TYPES: ReadonlySet = new Set([
"buying",
"research",
"learning",
"planning",
"unclassified",
]);
const BATCH_SIZE = 10;
const MAX_TOKENS_PER_BATCH = 4000;
async function callClaudeBatch(
apiKey: string,
systemPrompt: string,
batch: ThreadDescriptor[],
): Promise {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
},
body: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: MAX_TOKENS_PER_BATCH,
system: systemPrompt,
messages: [
{
role: "user",
content: JSON.stringify(batch),
},
],
}),
});
if (!response.ok) {
let body = "";
try { body = (await response.text()).slice(0, 400); } catch { }
console.error(
`[openloops] label: API request failedn` +
` → HTTP ({response.status} ){response.statusText}n` +
` body: ${body || "(empty)"}`,
);
if (response.status === 401) {
throw new Error("Invalid API key. Check your Anthropic API key and try again.");
}
throw new Error(`API request failed: ({response.status} ){response.statusText}`);
}
const data = await response.json();
const raw: string = data.content[0].text;
const cleaned = raw
.trim()
.replace(/^```(?:json)?s*/, "")
.replace(/```s*$/, "")
.trim();
try {
return JSON.parse(cleaned);
} catch (err) {
console.error(`[openloops] label: parse error: ${err instanceof Error ? err.message : String(err)}`);
console.error(`[openloops] label: raw tail (last 400 chars):n${raw.slice(-400)}`);
return null;
}
}
BATCH_SIZE 10 میں سے MAX_TOKENS_PER_BATCH 4000 کٹوتی کے مسئلے کا براہ راست جواب ہے۔ 10 دھاگوں کی مالیت کے لیبل 4000 آؤٹ پٹ ٹوکنز کے اندر آرام سے فٹ ہوں گے جس میں گنجائش باقی ہے، اس لیے بیچ اپنی حد تک نہیں پہنچے گا اور اسے چھوٹا نہیں کیا جائے گا۔ 40 تھریڈز والا ریکارڈ ایک بڑی درخواست کے بجائے چار آزاد درخواستیں ہوں گی۔
درخواست خود خام استعمال کرتا ہے. fetch یہ Anthropic کے TypeScript SDK نہ ہونے کی وجہ یہ ہے کہ SDK کسی براؤزر یا ایکسٹینشن سیاق و سباق میں چلانے کے لیے نہیں بنایا گیا ہے۔
براؤزر سے شروع کی جانے والی انتھروپک API کالز کی بھی ضرورت ہوتی ہے: anthropic-dangerous-direct-browser-access ہیڈر وہی ہے جو اس استعمال کے پیٹرن کو اٹھاتا ہے۔ ماڈل کلاڈ ہائیکو ہے، جو لائن اپ میں سب سے تیز اور سستا ہے۔ یہ اس طرح کی بڑی، سٹرکچرڈ آؤٹ پٹ جابز کے لیے اچھی طرح کام کرتا ہے جن کے لیے ایک سے زیادہ کالز کی ضرورت ہوتی ہے اور اس پر تیزی سے کارروائی کی جانی چاہیے۔
غلطی سے نمٹنے کو جان بوجھ کر دو مختلف طرز عمل میں تقسیم کیا جاتا ہے: HTTP سطح کی ناکامی ہوتی ہے (خراب کلید کے لیے 401، شرح محدود کرنے کے لیے 429)۔ کیونکہ اس کے بعد کے تمام بیچ اسی طرح ناکام ہو جائیں گے اور آپ مزید جاری نہیں رہ سکتے۔ کوئی راستہ نہیں تجزیہ اس کے برعکس، ناکامی واپس آتی ہے۔ null پھینکنے کے بجائے، کالر اس ایک بیچ کو چھوڑ سکتا ہے اور باقی بیچوں کے ساتھ جاری رکھ سکتا ہے۔
باڑ ہٹانے سے پہلے JSON.parse عام عملی مسائل کو ہینڈل کرتا ہے۔ ماڈل بعض اوقات اپنے JSON آؤٹ پٹ کو مارک ڈاؤن کوڈ کی باڑ سے لپیٹ دیتے ہیں (```json)، چاہے آپ خام JSON کی درخواست کر رہے ہوں۔ دو .replace کال معروف اور پچھلی باڑ کو ہٹا کر آس پاس کی سفید جگہ کی اجازت دیتی ہے، اگر کوئی ہے، تو جواب دیا جاتا ہے قطع نظر اس کے کہ یہ لپیٹ کر آیا ہے یا نہیں۔
اگر تجزیہ اب بھی ناکام ہو جاتا ہے تو، کیچ خام جواب کے آخری 400 حروف کو لاگ کرتا ہے۔ یہ وہ جگہ ہے جہاں آپ کٹ آف سرنی کے تراشے ہوئے دستخط دیکھ سکتے ہیں، ایک ایسا تشخیصی جو اصل بگ کو منٹوں میں ظاہر کر دیتا ہے۔
اشارے بنائیں اور نتائج کو ضم کریں۔
عوام labelThreads فنکشن ایک ڈسکرپٹر بناتا ہے، بیچ کو عمل میں لاتا ہے، اور لوٹے ہوئے مواد کو ضم کرتا ہے۔
export async function labelThreads(apiKey: string): Promise<{ labeled: number }> {
const threads = await getAllThreads();
if (threads.length === 0) return { labeled: 0 };
const allBrands = await getAllBrands();
const brandMap = new Map(allBrands.map((b) => [b.domain, b]));
const descriptors: ThreadDescriptor[] = threads.map(
const keywords = [...new Set(t.sessions.flatMap((s) => s.keywords))].slice(0, 8);
const domains = [...new Set(t.sessions.flatMap((s) => s.domains))].slice(0, 5);
const titles = [...new Set(t.sessions.flatMap((s) => s.events.map((e) => e.title)))].slice(0, 20);
const domainContext = domains
.map((d) => {
const brand = brandMap.get(d);
if (!brand || !brand.name) return null;
let line = `({d}: ){brand.name}`;
if (brand.description) line += ` — ${brand.description}`;
if (brand.industry) line += ` (${brand.industry})`;
return line;
})
.filter((s): s is string => s !== null);
return { id: t.id, keywords, domains, sampleTitles: titles, domainContext };
});
const systemPrompt = `You label browsing intent threads. Return ONLY a JSON array — no markdown fences, no explanation.
Each element: { "id": "", "title": "<3-6 word title>", "summary": "<1 sentence>", "type": "", "nextStep": "" }
The nextStep must be grounded in what the person was actually looking at. Be specific — name the actual decision, comparison, or action (e.g. "Decide between MacBook Pro and Dell XPS — your open question was battery life") rather than generic advice ("continue researching"). Use the sampleTitles and domainContext to ground it.
Each thread descriptor may include a "domainContext" array of company descriptions for the sites visited. When present, use these to produce sharper, more specific titles, summaries, and next steps grounded in what each company actually does.
Respond with exactly one array covering every thread in the request.`;
const allResults: LabelResult[] = [];
let failedBatches = 0;
for (let i = 0; i < descriptors.length; i += BATCH_SIZE) {
const batch = descriptors.slice(i, i + BATCH_SIZE);
const results = await callClaudeBatch(apiKey, systemPrompt, batch);
if (results === null) {
failedBatches++;
continue;
}
allResults.push(...results);
}
const byId = new Map(allResults.map((r) => [r.id, r]));
let labeled = 0;
const updated = threads.map(
const label = byId.get(t.id);
if (!label) return t;
const type = VALID_TYPES.has(label.type as IntentThread["type"])
? (label.type as IntentThread["type"])
: t.type;
labeled++;
return {
...t,
title: label.title || t.title,
summary: label.summary || undefined,
nextStep: label.nextStep || undefined,
type,
};
});
await putThreads(updated);
return { labeled };
}
ہر تھریڈ کو اس طرح کمپریس کیا جاتا ہے: ThreadDescriptor کلاڈ صرف وہی بتاتا ہے جس کی اسے لیبل لگانے کی ضرورت ہے۔ 8 مطلوبہ الفاظ، 5 ڈومینز، 20 نمونے والے صفحہ کے عنوانات، اور سینکڑوں واقعات والے تھریڈز پے لوڈ کو پھولنے سے بچانے کے لیے محدود ہیں۔
کہ domainContext اگلے حصے میں شامل برانڈ فاؤنڈیشن کی تعمیر کے مرحلے میں فیلڈز مرکزی حیثیت رکھتے ہیں۔ یہ فی الحال خالی ہے کیونکہ ابھی تک کوئی برانڈ درآمد نہیں کیا گیا ہے۔ یہی وجہ ہے کہ لیبلنگ اپنے طور پر اچھی طرح کام کرتی ہے اور جب زمین کو شامل کیا جاتا ہے تو واضح ہو جاتا ہے۔
انضمام کے مرحلے کے دوران، ناکام بیچوں کو صرف اپنے دھاگوں میں لاگت آتی ہے۔ نتائج تمام کامیاب تعیناتیوں کی ایک سادہ فہرست کے طور پر واپس آتے ہیں، جو تھریڈ ID کے ذریعہ ترتیب دی گئی ہے۔ byId.
اس کے بعد یہ تمام دھاگوں کو عبور کرتا ہے۔ اس تھریڈ کا لیبل واپس آنے کے بعد، AI ٹائٹل، سمری، اگلے مراحل اور قسم کو واپس کیے گئے تھریڈ کے ساتھ ملا دیا جاتا ہے۔ type تصدیق شدہ VALID_TYPES اگر ماڈل غیر متوقع نتائج لوٹاتا ہے، تو یہ ہورسٹک قسم کی طرف لوٹ جاتا ہے۔ اگر کوئی لیبل واپس نہیں کیا جاتا ہے، تو اس تھریڈ کے لیے بیچ پارس کرنا ناکام ہو جاتا ہے، اور دھاگہ پہلے سے موجود مطلوبہ الفاظ کے عنوانات اور ہیورسٹکس کو برقرار رکھتے ہوئے اسی طرح لوٹتا ہے۔
ایک ناکام بیچ کے نتیجے میں مکمل رن کے بجائے 10 دھاگوں کے برابر پیسنے کی لاگت آئے گی، اور کوئی تھریڈ خراب ڈیٹا سے خراب نہیں ہوگا۔
توجہ فرمائیں title, summaryاور nextStep خالی تاروں کے خلاف تمام تحفظ || t.title اور || undefined. یہاں تک کہ اگر ماڈل ایک خالی عنوان لوٹاتا ہے، دھاگے میں ہمیشہ ایک عنوان دستیاب ہوگا۔ summary اور nextStep رہنا undefined ایک خالی تار ہونے کے بجائے۔ اس سے "کیا اس تھریڈ میں کوئی خلاصہ ہے؟" محفوظ رہے گا۔ ڈیش بورڈ ایمانداری سے چیک کریں۔
چوکی
لیبلنگ کے لیے کلیدوں اور بٹنوں کی ضرورت ہوتی ہے، جو دونوں کو بعد میں اس گائیڈ میں ڈیش بورڈ کے ساتھ فراہم کیا جاتا ہے، لہذا مکمل اینڈ ٹو اینڈ ٹیسٹنگ اس وقت تک انتظار کرے گی۔
جو آپ ابھی چیک کر سکتے ہیں۔ src/lib/settings.ts اور src/pipeline/label.ts اس بات کو یقینی بنانے کے لیے کہ درخواست اچھی طرح سے تشکیل دی گئی ہے، مرتب کریں اور کال کریں۔ labelThreads اگر آپ فوری فیڈ بیک چاہتے ہیں تو ایک عارضی ٹیسٹ ٹول میں اصلی کلیدیں استعمال کریں۔ جب کسی بنے ہوئے دھاگے کے خلاف چلایا جائے، console آپ اپنی تعیناتی کی پیشرفت دیکھیں گے، اور IndexedDB میں تھریڈ کا عنوان کلیدی الفاظ کے ٹکڑوں سے پڑھنے کے قابل فقرے میں تبدیل ہو جائے گا۔ summary اور nextStep پہلا فیلڈ جو ظاہر ہوتا ہے۔
لیبلز کو پہلے ہی بہت بہتر کیا گیا ہے، لیکن وہ اب بھی کلیدی الفاظ اور بنیادی ڈومین ناموں کا استعمال کرتے ہوئے کام کرتے ہیں۔ اس کا مطلب ہے کہ اس کے ارد گرد ایک دھاگہ بنا ہوا ہے۔ mastra.ai اور langchain.com مجھے نہیں معلوم کہ یہ AI ایجنٹ کا فریم ورک ہے۔ صرف دو ڈومین سٹرنگز دکھائے جاتے ہیں۔
اگلا سیکشن ڈومینز کو لیبل لگانے سے پہلے کمپنی کی اصل تفصیل کے ساتھ ان کی تصدیق کرکے اس خلا کو دور کرتا ہے۔ یہ ایک بنیادی قدم ہے جو AI کو استدلال کے لیے ٹھوس مواد فراہم کرتا ہے۔
context.dev کا استعمال کرتے ہوئے لیبل کو گراؤنڈ کرنے کا طریقہ
یہ اوپن لوپس کا سب سے منفرد خیال ہے، اس لیے کوڈ سے پہلے واضح طور پر اس کا ذکر کرنا ضروری ہے۔ ماڈل کو مطلوبہ الفاظ اور بنیادی ڈومین ناموں کے ساتھ تھریڈز کو لیبل کرنے کے لیے کہنے کے بجائے، اوپن لوپس پہلے ہر ڈومین کی اصل کمپنی کی تفصیل کے ساتھ تصدیق کرتا ہے (کمپنی کیا ہے، یہ کس صنعت میں ہے، یہ اصل میں کیا کرتی ہے) اور اس تفصیل کو لیبلنگ پرامپٹ میں فیڈ کرتی ہے۔ ماڈل تھریڈز کو یہ جانتے ہوئے لیبل کرتا ہے کہ: mastra.ai اور langchain.com دو مبہم تاروں کو دیکھنے کے بجائے جن کا آپ کو اندازہ لگانا ہے، وہ دونوں AI ایجنٹ فریم ورک ہیں۔
کلیدی لفظ "mastra langchain sholajegede" کے ساتھ ایک دھاگہ "Mastra Langchain Sholajegede" جیسا عنوان پیدا کرے گا جو بغیر کسی بنیاد کے کلیدی لفظ کی عکاسی کرتا ہے۔ اس علم کے ساتھ کہ ڈومین ایک مسابقتی ایجنٹ کا فریم ورک ہے، وہی دھاگہ "LangChain کے خلاف بینچ مارکنگ ماسٹرا" کے عنوان سے اصل ارادے کا نام دیتا ہے۔
اچھے لیبل کے لیے خام مال ہمیشہ تلاش میں موجود رہا ہے۔ جو غائب تھا وہ اس کی تشریح کے لیے سیاق و سباق تھا، اور یہ وہی ہے جو برانڈ انٹیلی جنس API فراہم کرتا ہے۔
API کیا واپس کرتا ہے۔
openloops context.dev کا استعمال کرتا ہے، جو ڈومینز کو سٹرکچرڈ برانڈ ریکارڈز میں ترجمہ کرتا ہے جیسے کمپنی کا نام، ایک لائن کی تفصیل، صنعت کی درجہ بندی، برانڈ کے رنگ، اور لوگو URL۔ بنیادی مراحل کے لیے نام، تفصیل اور صنعت کی ضرورت ہوتی ہے، جبکہ لوگو اور رنگوں کو بعد میں ڈیش بورڈ میں ڈومین چپ پیش کرنے کے لیے استعمال کیا جائے گا۔
یہ مرحلہ مکمل طور پر اختیاری ہے۔ پچھلے حصے میں لیبلنگ اس قدم کے بغیر کام کرتی ہے، اور context.dev کلید موجود ہونے پر گراؤنڈ آؤٹ پٹ کو واضح کرتا ہے۔
Anthropic کلید کی طرح، context.dev کلید بھی ہے۔ chrome.storage.localاسی گیٹر/سیٹر پیٹرن کے ذریعے src/lib/settings.ts:
export async function getContextKey(): Promise {
const result = await chrome.storage.local.get("contextDevApiKey");
return (result.contextDevApiKey as string) ?? null;
}
export async function setContextKey(key: string): Promise {
await chrome.storage.local.set({ contextDevApiKey: key });
}
چونکہ ایک ہی ڈومین کی دو بار تصدیق کرنا فضول ہے اور API کریڈٹس کی لاگت آتی ہے، اس لیے آپ کو اپنے برانڈ ریکارڈز کو کیش کرنے کے لیے ایک جگہ کی بھی ضرورت ہے۔ کریش DB_VERSION 4 اور میں شامل کریں۔ domain_brands ڈومین کلید ذخیرے:
import type { RawEvent, Session, IntentThread, Brand } from "../types";
interface OpenloopsDB extends DBSchema {
raw_events: { key: string; value: RawEvent; indexes: { by_visitedAt: number } };
sessions: { key: string; value: Session; indexes: { by_startedAt: number } };
intent_threads: { key: string; value: IntentThread; indexes: { by_lastSeen: number } };
domain_brands: {
key: string;
value: Brand;
};
}
const DB_VERSION = 4;
اندرونی upgrade کال بیک کو انجام دینے سے نئے ریپوزٹری میں وہی گارڈز شامل ہو جائیں گے جیسے دیگر ریپوزٹریز۔ domain_brands چابی آن ہے domain بلکہ id کیونکہ ڈومین ایک منفرد، منفرد کلید ہے۔
if (!db.objectStoreNames.contains("domain_brands")) {
db.createObjectStore("domain_brands", { keyPath: "domain" });
}
مماثل مددگار کیشنگ سے متعلق مددگار شامل کرتے ہیں۔ getCachedDomains. یہ پہلے سے تصدیق شدہ ڈومینز کا ایک سیٹ واپس کرتا ہے تاکہ ہم انہیں سختی کے مرحلے میں چھوڑ سکیں۔
export async function getBrand(domain: string): Promise {
const db = await getDB();
return db.get("domain_brands", domain);
}
export async function putBrands(brands: Brand[]): Promise {
if (brands.length === 0) return;
const db = await getDB();
const tx = db.transaction("domain_brands", "readwrite");
await Promise.all([...brands.map((b) => tx.store.put(b)), tx.done]);
}
export async function getAllBrands(): Promise {
const db = await getDB();
return db.getAll("domain_brands");
}
export async function getCachedDomains(): Promise> {
const db = await getDB();
const keys = await db.getAllKeys("domain_brands");
return new Set(keys);
}
ایک برانڈ درآمد کرنا
بنانا src/pipeline/enrich.ts. کلید ایک ڈومین کو حل کرنے کی صلاحیت ہے، اور اس کی زیادہ تر لمبائی اس بات کو یقینی بنانے کے لیے موجود ہے کہ سست یا ناکام تلاش کی وجہ سے پورا مرحلہ لٹک جائے یا کریش نہ ہو۔
import { getCachedDomains, putBrands } from "../db/index";
import { isLocalHost } from "../lib/util";
import type { Brand } from "../types";
const API_BASE = "https://api.context.dev/v1";
const LOGO_LINK_BASE = "https://logos.context.dev";
const REQUEST_TIMEOUT_MS = 15_000;
const BATCH_SIZE = 3;
const BATCH_DELAY_MS = 2_000;
interface FetchResult {
brand: Brand | null;
errorCode?: string;
}
async function fetchBrand(domain: string, contextKey: string): Promise {
const url = `({API_BASE}/brand/retrieve?domain=){encodeURIComponent(domain)}`;
const headers = { Authorization: `Bearer ${contextKey}` };
async function attempt(): Promise {
const ctrl = new AbortController();
const tid = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(url, { headers, signal: ctrl.signal });
} finally {
clearTimeout(tid);
}
}
try {
let res = await attempt();
if (res.status === 408) {
res = await attempt();
}
if (!res.ok) {
let body = "";
try { body = (await res.text()).slice(0, 400); } catch { }
console.error(`[openloops] enrich: HTTP ({res.status} for "){domain}" — ${body}`);
return { brand: null, errorCode: String(res.status) };
}
let data: { status?: string; brand?: Record };
try {
data = await res.json();
} catch (e) {
return { brand: null, errorCode: "parse" };
}
if (data.status !== "ok" || !data.brand) {
return { brand: null, errorCode: "shape" };
}
const b = data.brand as {
title?: string;
description?: string;
colors?: { hex?: string }[];
logos?: { url?: string }[];
industries?: { eic?: { industry?: string; subindustry?: string }[] };
};
const logoUrl =
b.logos?.[0]?.url ||
`({LOGO_LINK_BASE}?domain=){encodeURIComponent(domain)}`;
return {
brand: {
domain,
name: b.title ?? domain,
description: b.description ?? "",
industry: b.industries?.eic?.[0]?.industry ?? "",
logoUrl,
brandColor: b.colors?.[0]?.hex ?? "",
},
};
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
return { brand: null, errorCode: "timeout" };
}
return { brand: null, errorCode: "network" };
}
}
درخواست بیئرر ٹوکن کے ساتھ تصدیق شدہ ہے اور ہے۔ brand/retrieve اختتامی نقطہ کہ attempt اندرونی فنکشن ہر کال کرتا ہے۔ AbortController ہم 15 سیکنڈ کا ٹائم آؤٹ استعمال کرتے ہیں، لہٰذا اسقاط شدہ کنکشن سختی کے مرحلے کو غیر معینہ مدت تک معطل کرنے کے بجائے خود ہی اسقاط ہو جائے گا۔
کہ finally ٹائمر کو اس بنیاد پر صاف کرتا ہے کہ آیا درخواست کامیاب ہوتی ہے، ناکام ہوجاتی ہے، یا اسقاط ہوجاتی ہے۔ کوئی راستہ نہیں 408 context.dev کی طرف سے جواب کا مطلب ہے اس طرف ایک کولڈ کیش مس۔ دستاویزات میں کہا گیا ہے کہ ایک بار دوبارہ کوشش کریں، اس لیے میں ہار ماننے سے پہلے دوبارہ کوشش کرتا ہوں۔
ردعمل ہر سطح پر دفاعی ہے۔ وہ ریاست جو ٹھیک نہیں ہے۔ FetchResult اگر آپ HTTP کوڈ استعمال کرتے ہیں، تو وہ باڈی ہوگی جسے آپ پارس نہیں کر رہے ہیں۔ "parse" اگر کوئی غلطی ہوتی ہے اور جواب توقع سے مختلف ہوتا ہے، تو یہ واپس آتا ہے: "shape" غلطی
جب ایک برانڈ ریکارڈ پاس کیا جاتا ہے، تو ہر فیلڈ ایک مناسب ڈیفالٹ پر واپس آجائے گا اگر غائب ہو جائے، کمپنی کا نام خود ڈومین میں واپس آجائے گا، تفصیل اور صنعت ایک خالی سٹرنگ پر واپس آجائے گی، اور اگر ریکارڈ میں کوئی لوگو URL نہیں ہے، تو لوگو context.dev میں کلیدی لوگو CDN پر واپس آجائے گا۔
تمام ناکام راستے واپس آ جاتے ہیں۔ { brand: null, errorCode } یہ اس لیے ہے کہ مندرجہ بالا بیچ ڈرائیور کسی ایک ڈومین کی ناکامی کو کریش کے بجائے اسکیپ کے طور پر سنبھال سکتا ہے۔
بیچوں میں ڈومینز کو مضبوط بنائیں
عوام enrichDomains فنکشن ڈومینز کی فہرست کو چیک کرتا ہے، ڈومینز کو چھوڑتا ہے جو پہلے سے کیش شدہ ہیں اور API کی شرح کی حدود کا احترام کرتے ہیں۔
export async function enrichDomains(
contextKey: string,
domains: string[],
): Promise<{ enriched: number; failed: number; error?: string }> {
const unique = [...new Set(domains)].filter((d) => !isLocalHost(d));
let cached: Set;
try {
cached = await getCachedDomains();
} catch (err) {
return { enriched: 0, failed: 0, error: "DB error" };
}
const toFetch = unique.filter((d) => !cached.has(d));
if (toFetch.length === 0) return { enriched: 0, failed: 0 };
let enriched = 0;
let failed = 0;
let firstErrorCode: string | undefined;
for (let i = 0; i < toFetch.length; i += BATCH_SIZE) {
const batch = toFetch.slice(i, i + BATCH_SIZE);
const results = await Promise.all(batch.map((d) => fetchBrand(d, contextKey)));
const brands = results.map((r) => r.brand).filter((b): b is Brand => b !== null);
for (const r of results) {
if (!r.brand) {
failed += 1;
if (!firstErrorCode) firstErrorCode = r.errorCode;
}
}
if (brands.length > 0) {
try {
await putBrands(brands);
enriched += brands.length;
} catch (err) {
failed += brands.length;
}
}
if (i + BATCH_SIZE < toFetch.length) {
await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY_MS));
}
}
let error: string | undefined;
if (firstErrorCode) {
const map: Record = {
"401": "401 — invalid key",
"403": "403 — check key permissions",
"429": "429 — rate limited, try again later",
"timeout": "request timeout (15 s)",
"network": "unreachable — check network/CORS",
};
error = map[firstErrorCode] ?? firstErrorCode;
}
return { enriched, failed, error };
}
یہ فنکشن مندرجہ ذیل مقامی ایڈریس کو اتار کر کھولا جاتا ہے: isLocalHostافزودہ باؤنڈری گارڈ پر خود حوالہ شور والے حصے میں تبادلہ خیال کیا گیا۔ اس کا مطلب ہے کہ اگر ڈویلپمنٹ سرور تھریڈ کی ڈومین لسٹ میں داخل ہو جائے تو بھی اسے context.dev پر نہیں بھیجا جا سکتا۔ پھر پہلے سے کیش شدہ ڈومینز کو بذریعہ ہٹائیں: getCachedDomainsلہذا، اگر آپ دوبارہ افزودگی چلاتے ہیں، تو آپ کو صرف وہ ڈومین ملیں گے جو آپ نے نہیں دیکھے ہوں گے۔ یہ آپ کے کریڈٹ کے استعمال کو کل تلاشوں کے بجائے نئی تلاشوں کے متناسب رکھتا ہے۔
بقیہ ڈومینز بیچوں کے درمیان 2 سیکنڈ کے وقفے کے ساتھ ایک وقت میں 3 درآمد کیے جاتے ہیں۔ یہ درخواست کی شرحوں کو API کی حد سے نیچے رکھتا ہے بغیر صارفین کو طویل سیریل قطاروں میں انتظار کرنا پڑے۔
ناکامیاں نہیں ہوں گی اور لاگ ان ہوں گی۔ ڈومینز جو اضافہ کو حل نہیں کرتے ہیں۔ failed یہ ایک ایرر کوڈ کو لاگ کرتا ہے، لیکن لوپ جاری رہتا ہے۔ سامنے آنے والے پہلے ایرر کوڈ کو آخر میں انسانی پڑھنے کے قابل پیغام میں میپ کیا جاتا ہے، جس سے UI مفید معلومات جیسے غلط کلید یا شرح کی حد کی اطلاعات ظاہر کر سکتا ہے۔
پورا فنکشن اس میں اضافہ کرنے کے بجائے گنتی لوٹاتا ہے۔ یہ ضروری ہے کیونکہ ڈیش بورڈ لیبل لگانے سے ٹھیک پہلے افزودگی چلاتا ہے، اور اس کے بعد لیبل لگانے میں برانڈنگ کے مسائل کی وجہ سے رکاوٹ نہیں ہونی چاہیے۔
کس طرح گراؤنڈنگ لیبلنگ پر دوبارہ لاگو ہوتی ہے۔
زمین کو دوبارہ جوڑ دیا گیا ہے۔ labelThreads پچھلے حصے میں ہم نے پہلے ہی تعمیر کیا domainContext برانڈ کیشے میں تمام ڈومینز کو بازیافت کرتا ہے، ہر تھریڈ کے لیے ترتیب دیا گیا ہے:
const domainContext = domains
.map((d) => {
const brand = brandMap.get(d);
if (!brand || !brand.name) return null;
let line = `({d}: ){brand.name}`;
if (brand.description) line += ` — ${brand.description}`;
if (brand.industry) line += ` (${brand.industry})`;
return line;
})
.filter((s): s is string => s !== null);
افزودگی کے چلنے سے پہلے، برانڈ کیش خالی ہے اور تمام ہٹ کچھ بھی نہیں لوٹاتے۔ domainContext یہ ایک خالی صف ہے اور پرامپٹ کو صرف مطلوبہ الفاظ اور ڈومین نام سے بدل دیا گیا ہے۔
سخت ہونے کے بعد، وہی کوڈ اس طرح کی لائنیں تیار کرتا ہے: mastra.ai: Mastra — TypeScript framework for building AI agents (Developer Tools)لیبلنگ پرامپٹس استعمال کرنے کے لیے ہدایات domainContext آخر کار میرے پاس کام کرنے کے لیے کچھ تھا، "واضح اور زیادہ مخصوص عنوانات، خلاصے، اور اگلے مراحل بنانے کے لیے۔"
دونوں مراحل کو ڈیزائن کے ذریعے الگ کیا گیا ہے۔ اگرچہ لیبلنگ کے لیے گراؤنڈنگ کی ضرورت نہیں ہے، لیکن گراؤنڈنگ لیبلنگ کو نمایاں طور پر بہتر بناتی ہے۔ یہی وجہ ہے کہ ڈیش بورڈ انہیں ایک ہی "اینرش اور لیبل" کام کے طور پر ترتیب وار چلاتا ہے۔
چوکی
لیبلنگ کے مرحلے کی طرح، افزودگی ڈیش بورڈ کے ذریعے کی جاتی ہے، لہذا مکمل راستہ ڈیش بورڈ سیکشن کا منتظر ہے۔ ابھی کے لیے، درج ذیل کو چیک کریں: src/pipeline/enrich.ts اور اپ ڈیٹ src/db/index.ts اسے مرتب کریں، اور getDB() رپورٹ ورژن 4۔ domain_brands یہ DevTools میں ہے۔
جب context.dev کلید کا استعمال کرتے ہوئے کسی حقیقی دھاگے کے خلاف چلایا جائے، domain_brands اسٹور کیش شدہ ریکارڈز سے بھر جائے گا، اور تھریڈ لیبل نمایاں طور پر واضح ہو جائیں گے۔ سب سے واضح واحد مظاہرہ ایک طاق یا ٹیکنالوجی ڈومین کے ارد گرد بنایا گیا ایک دھاگہ ہوگا جس کا نام خود ظاہر نہیں کرتا کہ یہ کیا ہے۔
انجن کے تمام حصے اب موجود ہیں: کیپچر، سیشنز، کلسٹرنگ، اسکورنگ، لیبلنگ، اور گراؤنڈنگ۔ جو چیز غائب ہے وہ ایک سطح ہے جو اسے چلاتی ہے اور نتائج دکھاتی ہے۔
اگلے حصے میں، ہم ایک ڈیش بورڈ، ایک آن بورڈنگ فلو اور پائپ لائن سٹیٹ مشین کے ساتھ تین کالم ری ایکٹ انٹرفیس بنا کر اس پائپ لائن کو انسانی استعمال کے لیے حقیقی چیز میں تبدیل کر دیں گے۔
ڈیش بورڈ کو کیسے ڈیزائن کریں۔
ڈیش بورڈ ری ایکٹ اجزاء کا ایک واحد درخت ہے جسے مکمل طور پر ٹیب شدہ صفحہ کے طور پر پیش کیا جاتا ہے، جس سے آپ ابتدائی طور پر سیٹ اپ کے دوران منسلک ہوتے ہیں۔ options_page ظاہر میں۔
یہ تین چیزیں کرتا ہے: یہ پائپ لائن چلاتا ہے (بٹن جو اسکین چلاتے ہیں، سیشن بناتے ہیں، تھریڈز بناتے ہیں، اور ان پر لیبل لگاتے ہیں)، نتیجے میں آنے والے ارادے کا نقشہ دکھاتا ہے (ریاست کے لحاظ سے دھاگوں کو گروپ کیا جاتا ہے)، اور اگلے حصے میں کور کیے گئے مددگاروں کی میزبانی کرتا ہے۔
یہ سیکشن ساخت اور منطق کے واقعی دلچسپ ٹکڑوں میں سے ایک پر توجہ مرکوز کرتا ہے: ریاستی مشین جو یہ طے کرتی ہے کہ پائپ لائن کا کون سا بٹن کسی بھی وقت فعال ہے۔ طرزیں زیادہ تر سادہ سی ایس ایس ہوتی ہیں، اس لیے میں انہیں یہاں سمری لیول پر کور کروں گا۔
3 کالم لے آؤٹ
src/dashboard/App.tsx فلیکس شیل کے اندر تین کالم رکھیں۔ بائیں ریل میں پائپ لائن کنٹرولز، API کلیدی ان پٹ، اور اسٹیٹس فلٹرز ہوتے ہیں۔ مرکزی کالم مرکزی مواد ہے، جیسے آن بورڈنگ سپلیش اسکرین یا دھاگے کا ارادہ نقشہ۔ دائیں کالم میں مجموعی اعداد و شمار اور ثانوی چیٹ شامل ہیں۔
┌──────────────┬───────────────────────────┬──────────────────┐
│ LEFT RAIL │ MAIN COLUMN │ RIGHT COLUMN │
│ │ │ │
│ Pipeline │ Welcome screen │ Overview stats │
│ · Scan │ — or — │ │
│ · Sessions │ Intent map: │ Assistant chat │
│ · Threads │ ACTIVE threads │ · messages │
│ │ STALLED threads │ · composer │
│ Keys │ DORMANT threads │ · model/effort │
│ Filter │ │ │
└──────────────┴───────────────────────────┴──────────────────┘
ہر تھریڈ کو ایک کارڈ کے طور پر پیش کیا جاتا ہے جس میں ٹائٹل، ٹائپ اور سٹیٹس کی گولیاں، AI سمری، ریزیومے بٹن کے ساتھ اگلی سٹیپ قطار، کنفیڈینس بار، ڈومین، کلیدی الفاظ اور سگنلز کے ساتھ ٹوٹنے کے قابل تفصیلات کا سیکشن دکھایا جاتا ہے۔
کارڈز کو ACTIVE، STALLED، اور DORMANT حصوں میں گروپ کیا جاتا ہے اور ہر گروپ کے اندر اعتماد کی سطح کے مطابق ترتیب دیا جاتا ہے۔ سب سے قیمتی دھاگے چلیں گے کیونکہ وہ انتہائی ضروری گروپ کے اوپری حصے پر جائیں گے۔
اسٹائل زندہ ہے۔ src/dashboard/app.css اور یہ روایتی ہے: سیاہ تھیم (قریب سیاہ پس منظر، سنگل نارنجی لہجہ) جس کی وضاحت CSS حسب ضرورت خصوصیات کے ذریعے کی گئی ہے۔ --accent: #ff5c33متن اور بارڈرز کے لیے چھوٹا گرے اسکیل)، لیبلز اور میٹا ڈیٹا کے لیے مونو اسپیسڈ فونٹس، اور مواد کے لیے sans-serif فونٹس۔
ڈیزائن کے انتخاب جو استعمال کے لیے اہم ہیں ریاستی بنیاد پر کلر کوڈنگ (فعال کے لیے لہجہ، معطل کے لیے خاموش امبر، غیر فعال کے لیے سرمئی) اور اعتماد بار کی چوڑائی کو براہ راست دھاگے کے اعتماد کے سکور پر نقش کرنا ہے۔
CSS میں سے کوئی بھی تعمیر کو سمجھنے کے لیے بوجھ برداشت کرنے والا نہیں ہے، اس لیے اسے دوبارہ تیار کرنے کے بجائے، اس حصے کا باقی حصہ اسٹائل کی بنیادی منطق پر توجہ مرکوز کرتا ہے۔
پائپ لائن ریاست مشین
پائپ لائن میں سخت حکم ہے۔ ریکارڈز اسکین ہونے سے پہلے سیشنز نہیں بنائے جا سکتے، اور سیشنز بننے سے پہلے تھریڈز نہیں بنائے جا سکتے۔ ڈیش بورڈ اسے ایک چھوٹی سٹیٹ مشین میں انکوڈ کرتا ہے، اور صحیح طریقے سے سیٹ اپ ہونے پر، انٹرفیس الجھنے کی بجائے رہنمائی محسوس کرے گا۔ تمام بٹن یا تو غیر فعال ہیں (ان پٹ ابھی موجود نہیں ہے)، اگلی کارروائی کے طور پر نمایاں کیا گیا ہے، یا مکمل کیا گیا ہے (دوبارہ قابل، لیکن اب کوئی واضح اگلا مرحلہ نہیں ہے)۔
type PipelineState = "disabled" | "next" | "done";
function pipelineStates(
hasScanned: boolean,
eventCount: number | null,
sessionCount: number | null,
threadCount: number | null,
): { scan: PipelineState; sessions: PipelineState; threads: PipelineState } {
const hasEvents = (eventCount ?? 0) > 0;
const hasSessions = (sessionCount ?? 0) > 0;
const hasThreads = (threadCount ?? 0) > 0;
if (!hasScanned) return { scan: "next", sessions: "disabled", threads: "disabled" };
if (!hasSessions) return { scan: "done", sessions: hasEvents ? "next" : "disabled", threads: "disabled" };
if (!hasThreads) return { scan: "done", sessions: "done", threads: "next" };
return { scan: "done", sessions: "done", threads: "done" };
}
یہ فنکشن ہر قدم پر ڈیٹا کی موجودگی یا غیر موجودگی کو پڑھتا ہے اور تینوں بٹنوں کی حیثیت واپس کرتا ہے۔ اسکین کرنے سے پہلے، صرف اسکیننگ کو فعال اور ڈسپلے کیا جاتا ہے۔ nextباقی دو معذور ہیں۔
اگر کوئی واقعہ موجود ہے لیکن سیشن موجود نہیں ہے تو اسکین اس پر بدل جاتا ہے: done اور سیشن ہے۔ next. اگر سیشن موجود ہے لیکن تھریڈ نہیں ہے تو تھریڈ بن جاتا ہے: next. جب تینوں مراحل آؤٹ پٹ پیدا کرتے ہیں، تو سب کچھ ہو جاتا ہے۔ doneتمام اقدامات دوبارہ کیے جا سکتے ہیں، لیکن کسی کو بھی توجہ کی ضرورت نہیں ہے۔ جھرن پائپ لائن کے ذریعے تسلسل کے ساتھ حرکت کرتی ہے، بالکل ایک روشنی کو آن کرتی ہے۔ next خیال یہ ہے کہ تین بٹنوں کی ایک قطار کو ہدایت شدہ ترتیب میں تبدیل کیا جائے۔
پہلا پیرامیٹر، hasScannedیہ ایک سادہ شمار سے زیادہ لطیف ہے۔ یہ وہ جگہ ہے جہاں پہلے کیپچر سیکشن کے پلمبنگ بٹس ادا کرتے ہیں۔
آپ صرف "کیا کوئی واقعات ہیں" کو چیک نہیں کر سکتے ہیں کیونکہ لائیو کیپچر بھرنا شروع ہو جائے گا۔ raw_events جس لمحے ایکسٹینشن انسٹال ہوتی ہے۔ وہاں ہو جائے گا ہمیشہ یہ ایک واقعہ بن جاتا ہے اور آن بورڈنگ صارف کے اسکین سے پہلے اسکیننگ کے مرحلے کو چھوڑ دیتا ہے۔
ٹھیک ہے source فی فیلڈ RawEventپر سیٹ کریں "backfill" یا "live" جب آپ ایک کیپچر بناتے ہیں۔ hasScanned خاص طور پر، یہ ایک وقف شدہ استفسار سے آتا ہے جو بیک فل ایونٹس کی جانچ کرتا ہے۔
export async function hasBackfillEvents(): Promise {
const db = await getDB();
let cursor = await db.transaction("raw_events", "readonly").store.openCursor();
while (cursor) {
if (cursor.value.source === "backfill") return true;
cursor = await cursor.continue();
}
return false;
}
یہ چلتا ہے raw_events جب تک کوئی ایک واقعہ نہ ملے source === "backfill"وہ لمحہ جلد واپس آتا ہے۔ ہم صرف لائیو ایونٹس کیپچر کرنے سے کبھی مطمئن نہیں ہوتے، اس لیے "میری سرگزشت تلاش کریں" کو پہلے قدم کے طور پر آن کیا جاتا ہے جب تک کہ صارف حقیقت میں بیک فل نہیں چلاتا۔ یہ آن بورڈنگ کا صحیح رویہ ہے۔ ہر واقعہ کے ماخذ کو ٹیگ کرنے کا بظاہر معمولی فیصلہ، جو کئی حصے پہلے کیا گیا تھا، اب اس فرق کو ممکن بناتا ہے۔
اسی سسٹم پر اسٹارٹ اپ اسکرین چلانا
پہلی بار غیر تھریڈ والے صارفین کو خالی ارادے کے نقشے کی بجائے سینٹرڈ سپلیش اسکرین نظر آئے گی۔ تاہم، ان اسکرینوں کے لیے الگ منطق فراہم کرنے کے بجائے، ڈیش بورڈ اسکرینوں کو اسی منطق کے ذریعے چلاتا ہے۔ pipelineStates حساب آپ جس مرحلے میں بھی ہوں۔ next ایک واحد کال ٹو ایکشن کا فیصلہ کریں جو ویلکم اسکرین پر ظاہر ہوتا ہے۔
let welcomeStep: 1 | 2 | 3 = 1;
let welcomeCtaLabel = "Scan my history";
let welcomeCtaClick = handleScan;
if (scanState === "next") {
welcomeStep = 1;
welcomeCtaLabel = scanning ? "Scanning…" : "Scan my history";
welcomeCtaClick = handleScan;
} else if (sessionsState === "next") {
welcomeStep = 2;
welcomeCtaLabel = buildingSessions ? "Building…" : "Build sessions";
welcomeCtaClick = handleBuildSessions;
} else if (threadsState === "next") {
welcomeStep = 3;
welcomeCtaLabel = buildingThreads ? "Building…" : "Build your intent map";
welcomeCtaClick = handleBuildThreads;
}
اسٹارٹ اسکرین پر ایک واحد بٹن ہمیشہ ریلوں پر ایک بٹن کا عکس دیتا ہے۔ next یہ کام کرتا ہے، صارفین کو ایک نمایاں بٹن پر تین کلکس کے ساتھ سکیننگ، سیشن بنانے اور تھریڈز بنانے کی اجازت دیتا ہے۔ جس لمحے کوئی دھاگہ موجود ہوتا ہے، سپلیش اسکرین کو ارادے کے نقشے سے بدل دیا جاتا ہے۔ ریل اور ویلکم اسکرین کبھی بھی اس بات پر متفق نہیں ہوتے ہیں کہ آگے کیا کرنا ہے۔ کیونکہ دونوں ایک ہی ذریعہ سے پڑھتے ہیں۔
ہینڈلر وائرنگ
سنبھالنے والا خود ایک منظر ہے۔ ہر ہینڈلر پائپ لائن کے مرحلے کو انجام دیتا ہے اور پھر ڈیٹا بیس میں اجزاء کے منظر کو تازہ کرتا ہے۔ گراؤنڈنگ اور لیبلنگ کو ایک ساتھ چلانا قابل غور ہے کیونکہ یہ پچھلے دو حصوں میں بیان کردہ ڈیکپلنگ کو لاگو کرتا ہے۔
async function handleEnrichAndLabel() {
setLabelError(null);
setEnrichError(null);
if (contextKey.trim() && contextKeySaved) {
setEnriching(true);
try {
const allDomains = [...new Set(
threads.flatMap(
)];
const result = await enrichDomains(contextKey.trim(), allDomains);
if (result.error) setEnrichError(`context.dev: ${result.error}`);
if (result.enriched > 0) {
const all = await getAllBrands();
setBrands(new Map(all.map((b) => [b.domain, b])));
}
} catch (err) {
setEnrichError(`context.dev: ${err instanceof Error ? err.message : "unknown error"}`);
} finally {
setEnriching(false);
}
}
setLabeling(true);
try {
await labelThreads(apiKey.trim());
setThreads(await getAllThreads());
} catch (err) {
setLabelError(err instanceof Error ? err.message : "Labeling failed.");
} finally {
setLabeling(false);
}
}
سختی صرف اس صورت میں چلتی ہے جب context.dev کلید موجود ہو، اور اسے لپیٹ دیا جاتا ہے تاکہ خرابیاں (جیسے نیٹ ورک کی غلطیاں، غلط کلیدیں، یا شرح کی حدیں) ایک خرابی کا پیغام سیٹ کرتی ہیں لیکن اس پر عمل درآمد نہیں روکتی ہیں۔ اس کے بعد لیبلنگ غیر مشروط طور پر کمک کے بلاک سے باہر چلتی ہے، لہذا یہ اس بات سے قطع نظر آگے بڑھے گا کہ کمک کامیاب ہوئی، ناکام ہوئی، یا کلیدوں کی کمی کی وجہ سے مکمل طور پر چھوڑ دی گئی۔
ساخت کو خاص طور پر زمینی حصے سے الگ کرنے کے لیے ڈیزائن کیا گیا ہے۔ گراؤنڈنگ لیبلنگ کو بہتر بناتا ہے جب یہ کام کرتا ہے، اور جب یہ کام نہیں کرتا ہے، لیبلنگ کلیدی الفاظ اور ڈومین کے سیاق و سباق کو خوبصورتی سے کم کرتی ہے۔
اضافہ کی غلطیاں سرخ کے بجائے عنبر میں دکھائی دیتی ہیں۔ اس کی وجہ یہ ہے کہ یہ ایک انتباہ ہے (لیبل اب بھی ہوتا ہے) نہ کہ بلاک کرنے کی ناکامی۔ یہ چھوٹے UI سراگ ہیں جو غلط ہونے کی اصل شدت سے میل کھاتے ہیں۔
دوبارہ شروع بٹن
ایک تعامل ارادے کے نقشے کو ریئل ٹائم نیویگیشن سے دوبارہ جوڑتا ہے۔ ہر تھریڈ کارڈ میں ایک ریزیومے بٹن ہوتا ہے جو اس صفحہ کو دوبارہ کھولتا ہے جس پر آپ فی الحال تھے، اس لیے تھریڈ پر آپریشنز ہسٹری میں تلاش کرنے کے بجائے ایک کلک کے ساتھ کیے جاتے ہیں۔
const RESUME_SKIP_DOMAINS = new Set([
"google.com", "youtube.com", "bing.com", "duckduckgo.com",
"gmail.com", "mail.google.com",
]);
function resumeThread(thread: IntentThread): void {
const seen = new Set();
const urls: string[] = [];
const sorted = thread.sessions
.flatMap((s) => s.events)
.sort((a, b) => b.visitedAt - a.visitedAt);
for (const ev of sorted) {
if (RESUME_SKIP_DOMAINS.has(ev.domain)) continue;
if (seen.has(ev.url)) continue;
seen.add(ev.url);
urls.push(ev.url);
if (urls.length >= 3) break;
}
urls.forEach((url, i) => {
chrome.tabs.create({ url, active: i === 0 });
});
}
تازہ ترین سے شروع ہونے والے دھاگے میں واقعات کو دوبارہ شروع کریں، سرچ انجنوں اور ویب میل کو چھوڑ دیتا ہے (ان منزلوں کے بجائے جہاں آپ واپس جانا چاہتے ہیں)، یو آر ایل کے ذریعے ڈپلیکیٹس کو ہٹاتا ہے، اور تین حالیہ بامعنی صفحات کو کھولتا ہے۔ پہلا ایکٹیو ٹیب ہے اور باقی بیک گراؤنڈ میں ہیں۔ یہ ایک چھوٹی سی خصوصیت ہے، لیکن اس سے یہ محسوس ہوتا ہے کہ دھاگہ کہاں رہا ہے اس کے ریکارڈ کے بجائے ایک ایسی جگہ جہاں آپ واپس جا سکتے ہیں۔
چوکی
ڈیش بورڈز کے منسلک ہونے کے بعد، پوری پائپ لائن آخر کار انٹرفیس کے ذریعے اینڈ ٹو اینڈ تک دستیاب ہوتی ہے۔ جب آپ ایکسٹینشن کو دوبارہ لوڈ کریں گے اور ڈیش بورڈ کھولیں گے، تو آپ کو ایک ویلکم اسکرین نظر آئے گی جو آپ سے اسکین کرنے کو کہے گی۔
اسکین پر کلک کریں، سیشن بنائیں، اور ایک ارادے کا نقشہ بنائیں، اور آپ کو سٹیٹس کے لحاظ سے دھاگوں کا گروپ نظر آئے گا۔ اینتھروپک کلید (اختیاری طور پر context.dev کلید) شامل کریں اور عنوان اور اگلے اقدامات واضح ہونے کے لیے "لیبل اور اضافہ کریں" پر کلک کریں۔ پورا لوپ جو ہم نے پچھلے تمام حصوں میں بنایا تھا اب ایک ہی اسکرین پر چلتا ہے۔
بس جو رہ گیا ہے وہ دائیں طرف ڈائیلاگ پرت ہے۔ ایک AI اسسٹنٹ جو ایک ساتھ تمام تھریڈز کے بارے میں استدلال کر سکتا ہے اور سوالات کا جواب دے سکتا ہے جیسے "مجھے اس ہفتے کیا بند کرنا چاہیے؟" ہم اسے اگلے حصے میں بناتے ہیں۔
اے آئی اسسٹنٹ کیسے بنایا جائے۔
لیبلنگ کا مرحلہ کلاڈ سے ایک وقت میں ایک تھریڈ کی وضاحت کرنے کو کہتا ہے۔ اسسٹنٹ کچھ اور مشکل سے پوچھتا ہے۔ تمام تھریڈز کو اکٹھا کریں اور کھلے سوالات کے جواب دیں جیسے کہ اس ہفتے کو کیا ختم کرنا ہے، کس چیز میں سب سے زیادہ تاخیر ہوئی ہے، یا کسی خاص تھریڈ کو کیسے مکمل کرنا ہے۔
یہ ایک چیٹ انٹرفیس ہے، لیکن یہ محدود ہے۔ چونکہ یہ مکمل طور پر آپ کے اپنے تھریڈ ڈیٹا پر مبنی ہے، اس لیے جوابات عام پیداواری مشورے فراہم کرنے کے بجائے اصل دھاگوں کا نام لے کر حوالہ دیتے ہیں۔
پورا ڈیزائن ایک خیال پر مبنی ہے۔ دی گئی صورتحال کے مطابق چیٹ اسسٹنٹ کی کارکردگی بہتر ہوتی ہے۔ لہذا یہاں زیادہ تر کام ہر پیغام کے لیے صحیح بنیادی سیاق و سباق کی تعمیر کر رہا ہے، نہ کہ خود چیٹ میکانزم۔
مکالمے کی بنیاد رکھنا
کلاڈ کو پیغام پہنچانے سے پہلے، اسسٹنٹ ایک سسٹم پرامپٹ بناتا ہے جو صارف کے تھریڈ کو بیان کرتا ہے۔ یہ دو طریقوں میں سے ایک میں کرتا ہے، اس بات پر منحصر ہے کہ آیا صارف نے کسی مخصوص تھریڈ پر کلک کیا ہے۔
اگر کوئی دھاگہ منتخب نہیں کیا جاتا ہے، تو تمام تھریڈز کے لیے ایک مختصر ڈائجسٹ بنایا جاتا ہے۔ ایک کو منتخب کرنے سے آپ کو اس تھریڈ کے بارے میں بھرپور تفصیلات اور دوسرے تھریڈز کی ایک مختصر فہرست ملے گی۔
function buildGroundingContext(
threads: IntentThread[],
brands: Map,
selectedThread: IntentThread | null,
): string {
if (!selectedThread) {
const digest = threads
.map(
const domains = [...new Set(t.sessions.flatMap((s) => s.domains))].slice(0, 5).join(", ");
return `- ({t.title} (){t.status}, ({t.type}): ){t.summary ?? "no summary yet"} | next: ({t.nextStep ?? "none"} | domains: ){domains || "none"}`;
})
.join("n");
return `({SYSTEM_INSTRUCTION}nnHere is a digest of all the user's open intent threads:n){digest || "(no threads yet)"}`;
}
const keywords = [...new Set(selectedThread.sessions.flatMap((s) => s.keywords))].slice(0, 10).join(", ");
const domains = [...new Set(selectedThread.sessions.flatMap((s) => s.domains))].slice(0, 5);
const domainLines = domains
.map((d) => {
const brand = brands.get(d);
if (brand?.description) return `- ({d}: ){brand.name} — ${brand.description}`;
return `- ${d}`;
})
.join("n");
const sampleTitles = [...new Set(selectedThread.sessions.flatMap((s) => s.events.map((e) => e.title)))]
.slice(0, 20)
.map(
.join("n");
const otherTitles = threads
.filter(
.map(
.join(", ");
return `${SYSTEM_INSTRUCTION}
The user is focused on this thread:
Title: ${selectedThread.title}
Status: ${selectedThread.status}
Type: ${selectedThread.type}
Summary: ${selectedThread.summary ?? "none"}
Next step: ${selectedThread.nextStep ?? "none"}
Keywords: ${keywords || "none"}
Domains visited:
${domainLines || "(none)"}
Recent page titles:
${sampleTitles || "(none)"}
For context, the user's other open threads are: ${otherTitles || "none"}.`;
}
یہ دونوں طریقے ان دو قسم کے سوالات کے مطابق ہیں جو لوگ پوچھتے ہیں۔ سوالات جیسے "مجھے اس ہفتے کیا ختم کرنا چاہیے؟" چونکہ یہ پورے سیٹ کے بارے میں ہے، ڈائجسٹ موڈ کلاڈ کو ہر تھریڈ کا ایک سطری خلاصہ دیتا ہے۔ ہر چیز کا موازنہ اور ترجیح دینے کے لیے یہ کافی گنجائش ہے۔
دوسری طرف، سوالات جیسے "میں یہ کیسے کروں؟" ایک ہی دھاگے کے بارے میں ہیں، لہذا انتہائی موڈ میں آپ چوڑائی کے لیے گہرائی کی تجارت کرتے ہیں۔ اس تھریڈ کے کلیدی الفاظ، اپنے برانڈ کی تفصیل کے ساتھ ایک ڈومین، اور 20 اصل صفحہ کے عنوانات کو پاس کریں، جبکہ دوسرے تھریڈز کے نام جاری رکھیں تاکہ Claude کو معلوم ہو کہ آپ اور کس چیز پر کام کر رہے ہیں۔
فوکس موڈ وہ جگہ ہے جہاں برانڈ گراؤنڈنگ دوبارہ ابھرتی ہے۔ ڈومین کی فہرست میں وہی برانڈ ریکارڈ شامل ہے جو افزودگی کے دوران کھینچا گیا تھا، لہذا جب صارف دھاگے کے بارے میں پوچھتا ہے، کلاڈ mastra.ai: Mastra — TypeScript framework for building AI agents خالی ڈومین نہیں ہے۔ یہ وہی بنیادی اصول ہے جیسا کہ لیبلنگ، اب بات چیت پر لاگو ہوتا ہے۔
سسٹم کمانڈز جو دونوں موڈز کے لیے پریفکس بتاتی ہیں اس ڈیٹا کے اسسٹنٹ کو اینکر کرتی ہیں۔
const SYSTEM_INSTRUCTION =
`You are the assistant inside "openloops", a browser extension that reconstructs ` +
`the user's browsing history into "intent threads" — decisions, research, or ` +
`plans they started and haven't closed. Help the user understand and act on ` +
`these open loops. Be concrete: reference the actual threads by name and ` +
`suggest real next actions. You are grounded only in the thread data provided ` +
`below — if the user asks about something not present in it, say so plainly ` +
`rather than guessing.`;
آخری ہدایت اہم ہے۔ ماڈل کو اس بات کو تسلیم کرنے کی ہدایت کرنا کہ جب کوئی قابل فہم جواب پیدا کرنے کے بجائے ڈیٹا میں کچھ غائب ہو تو وہ اسسٹنٹ کو بھروسہ مند رکھتا ہے جب صارفین ڈیٹا میں غیر موجود تھریڈز یا تفصیلات کے بارے میں پوچھتے ہیں۔
پیغام بھیجیں۔
بھیجنے کا فنکشن ہر پیغام کے لیے ایک نئے زمینی سیاق و سباق کی تشکیل نو کرتا ہے۔ اسسٹنٹ ہمیشہ تھریڈ کی موجودہ حالت کی عکاسی کرتا ہے (بشمول بات چیت شروع ہونے کے بعد سے کیا تبدیلی آئی ہے) اور کلاؤڈ کو پیغام کی پوری تاریخ پوسٹ کرتا ہے۔
async function send(text: string) {
const trimmed = text.trim();
if (!trimmed || sending) return;
if (!keySaved) {
setError("Add your Anthropic key above to chat.");
return;
}
setError(null);
const nextMessages: Message[] = [...messages, { role: "user", content: trimmed }];
setMessages(nextMessages);
setInput("");
setSending(true);
try {
const systemPrompt = buildGroundingContext(threads, brands, selectedThread);
const maxTokens = EFFORT_OPTIONS.find((e) => e.id === effort)?.maxTokens ?? 1024;
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
},
body: JSON.stringify({
model,
max_tokens: maxTokens,
system: systemPrompt,
messages: nextMessages.map((m) => ({ role: m.role, content: m.content })),
}),
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid API key. Check your Anthropic API key and try again.");
}
throw new Error(`API request failed: ({response.status} ){response.statusText}`);
}
const data: { content: AnthropicContentBlock[] } = await response.json();
const reply = data.content
.filter((b) => b.type === "text" && b.text)
.map((b) => b.text)
.join("");
setMessages((prev) => [...prev, { role: "assistant", content: reply || "(empty response)" }]);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong.");
} finally {
setSending(false);
}
}
میکانزم لیبلنگ کی درخواستوں، وہی اختتامی نقطوں، وہی براؤزر تک رسائی کے ہیڈر، اور وہی 401 شناختی غلطی سے نمٹنے کا آئینہ دار ہے۔ اس کی وجہ یہ ہے کہ وہ دونوں ایک ہی محدود ماحول میں ایک ہی API سے بات کرتے ہیں۔ آپ کا پیغام چلتے ہوئے پیغامات میں شامل ہو جاتا ہے۔ messages صفوں کے لیے، پوری صف بھیجی جاتی ہے تاکہ ماڈل اب تک اس سے بات کر سکے، اور اسمبل شدہ زمینی سیاق و سباق اس طرح ہے: system فوری۔ جواب میں متن کے ایک بلاک کو متبادل سٹرنگ کے ساتھ جوڑ کر نکالا جاتا ہے اگر ماڈل کوئی دستیاب شے واپس نہیں کرتا ہے۔
تعمیر نو buildGroundingContext ہر بات چیت میں ایک بار بھیجنے کے بجائے ہر بار بھیجنے کا یہ جان بوجھ کر انتخاب ہے۔ جب کوئی صارف بات چیت کے دوران پائپ لائن کو دوبارہ چلاتا ہے یا کسی دھاگے پر لیبل لگاتا ہے، تو اگلا پیغام خود بخود اپ ڈیٹ شدہ ڈیٹا کی عکاسی کرتا ہے جب سے چیٹ شروع ہوتی ہے۔
ماڈل اور کوشش کا کنٹرول
اسسٹنٹ دو سلیکٹرز کو ظاہر کرتا ہے: استعمال کرنے کے لیے ماڈل اور اجازت دینے کے لیے گہرائی۔ دونوں آخری۔ chrome.storage.local کلید کے طور پر اسی سیٹ اپ پیٹرن کے ذریعے:
const MODEL_OPTIONS = [
{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5 — fastest" },
{ id: "claude-sonnet-4-6", label: "Sonnet 4.6 — balanced" },
{ id: "claude-opus-4-8", label: "Opus 4.8 — most capable" },
];
const EFFORT_OPTIONS = [
{ id: "low", label: "Low", maxTokens: 512 },
{ id: "medium", label: "Medium", maxTokens: 1024 },
{ id: "high", label: "High", maxTokens: 2048 },
];
ماڈل سلیکٹرز رفتار بمقابلہ فعالیت کے اسپیکٹرم کو پھیلاتے ہیں۔ فوری جوابات کے لیے ہائیکو، الجھے ہوئے دھاگوں کے سیٹ پر زیادہ مشکل اندازہ کے لیے اوپس۔ کوشش سلیکٹر نقشہ بناتا ہے: max_tokensردعمل کی مدت کو کنٹرول کرتا ہے جو ماڈل تیار کر سکتا ہے۔ اس بات کو مدنظر رکھتے ہوئے کہ میسجنگ API میں کوئی وقف شدہ گہرائی کنٹرول نہیں ہے، یہ ردعمل کی گہرائی کے لیے ایک معقول پراکسی ہے۔ وہ صارفین جو ایک سطری جواب چاہتے ہیں کم کو منتخب کرتے ہیں؛ وہ صارفین جو ایک معقول، اعلی ترجیحی منصوبہ چاہتے ہیں ہائی کو منتخب کریں۔
رینڈرنگ جواب اور خالی حالت
ماڈل قدرتی طور پر ایک ترجیحی فہرست اور مرحلہ وار تجاویز کو عنوانات اور گولیوں کے ساتھ فارمیٹ کرتا ہے، لہذا مددگار Claude کے جواب کو Markdown کے طور پر پیش کرتا ہے۔ جب سادہ متن کے طور پر پیش کیا جاتا ہے، تو یہ خام ستارے اور ہیش کی طرح لگتا ہے۔ استعمال کریں react-markdownردعمل کا جزو بنیادی طور پر ہے۔ ثانوی پیغامات کے لیے، صارف کے پیغام کو سادہ متن کے طور پر پیش کیا جاتا ہے۔ ساتھ والی طرزیں ڈیش بورڈ کے ٹائپ اسکیل سے مماثل مارک ڈاؤن عناصر کو نشانہ بناتی ہیں۔
بات چیت شروع ہونے سے پہلے، پینل ایک لائن کی تفصیل کے ساتھ ایک خالی حالت دکھاتا ہے اور کلک کے قابل چپس کے ذریعہ تجویز کردہ کئی اشارے، جیسے کہ "مجھے اس ہفتے کیا بند کرنا چاہیے؟"، "لوپ کا خلاصہ کھولیں"، اور "سب سے زیادہ تاخیر کس چیز میں ہوئی؟" دونوں آپ کو دکھاتے ہیں کہ آپ کا اسسٹنٹ کیا کر سکتا ہے اور آپ کو ایک کلک کے ساتھ شروع کرنے کا طریقہ فراہم کرتے ہیں۔
جب کسی تھریڈ پر فوکس کیا جاتا ہے تو تجویز کردہ پرامپٹ تھوڑا سا بدل جاتا ہے "میں اس کام کو کیسے مکمل کروں؟" یہ مکمل سیٹ سمری کے بجائے فوکسڈ گراؤنڈنگ موڈ سے میل کھاتا ہے۔
ایک پرائیویسی لائن مستقل طور پر مصنف کے نیچے واقع ہے، جس سے یہ ظاہر ہوتا ہے کہ چیٹ تھریڈ کا عنوان اور خلاصہ Anthropic کو بھیجے گا، اور ڈیوائس کے باہر کچھ نہیں بھیجا جائے گا۔ یہ ایماندارانہ انکشاف کا وہی اصول ہے جو پورے بورڈ میں ان جگہوں پر لاگو ہوتا ہے جہاں صارف ٹائپ کرنے سے پہلے چیزوں کو دیکھ سکتے ہیں۔
چوکی
اسسٹنٹ کے ساتھ، اوپن لوپس مکمل طور پر فعال ہو جاتا ہے۔ دوبارہ لوڈ کریں، ایک ارادے کا نقشہ بنائیں، انتھروپک کیز شامل کریں، اور تجویز کردہ اشارے آزمائیں۔ جب اس سے پوچھا گیا کہ اس ہفتے کیا ختم کرنا ہے، تو آپ کے معاون کو ایک مخصوص تھریڈ کا نام دینا چاہیے اور یہ بتانا چاہیے کہ حقیقی فیصلے کے مقابلے میں یہ ایک آسان جیت کیوں ہے۔ اگر آپ کسی ایک تھریڈ پر کلک کرتے ہیں اور پوچھتے ہیں کہ یہ کیسے ختم ہونا چاہیے، تو جواب اس تھریڈ کی تفصیلات تک محدود ہو جائے گا۔
بات چیت اصل موجودہ دھاگے کی عکاسی کرتی ہے، اور گفتگو کے بارے میں کچھ بھی آپ کے کمپیوٹر پر باقی نہیں رہتا سوائے ایک دھاگے کے خلاصے کے جسے خود زمینی تناظر میں دیکھا جا سکتا ہے۔
تعمیر مکمل ہے۔ آخری حصے میں، ہم ایک قدم پیچھے ہٹتے ہیں اور دیکھتے ہیں کہ آپ نے کیا بنایا ہے۔ یعنی، یہ اس خیال کی ایک مرکزی دھارے کی کوشش سے کس طرح موازنہ کرتا ہے، یہ رازداری کے ماڈل میں کیا اضافہ کرتا ہے، اور یہ اسے اگلے درجے تک کہاں لے جا سکتا ہے۔
آپ کیا بناتے ہیں اور کہاں لے جاتے ہیں۔
ہم نے ایک مکمل نظام بنایا ہے۔ تلاش کی سرگزشت کو کیپچر کے ذریعے فیڈ کیا جاتا ہے، منظم اور سیشنز میں تقسیم کیا جاتا ہے، انٹینٹ تھریڈز کے ذریعے کلسٹرڈ اور اسکور کیا جاتا ہے، اختیاری طور پر لیبل لگا ہوا اور AI کے ذریعے چلایا جاتا ہے، اور بات چیت کے معاونین کے ساتھ ڈیش بورڈ کے ذریعے ڈسپلے کیا جاتا ہے۔ ہر قدم اپنی مشین پر چلتا ہے، اور AI پرت پائپ لائن کے اوپر ایک اختیاری اضافہ ہے، جو اس کے بغیر کام کرتی ہے۔
اگر کلسٹرنگ آپ کو Chrome کی پرانی Journeys کی خصوصیت کی یاد دلاتا ہے، تو یہ ایک منصفانہ کنکشن ہے۔ وقت کے بجائے عنوان کے لحاظ سے ریکارڈز کو گروپ کرنا اتنا ہی اچھا نقطہ آغاز ہے۔
openloops ایک قدم آگے جاتا ہے۔ ہر دھاگہ اعتماد کے اسکور اور حالت کو پاس کرتا ہے، ایک AI پرت لیبلز اور مخصوص اگلے مراحل کا اضافہ کرتی ہے، مطالبے پر تھریڈز میں ایک اسسٹنٹ وجوہات، اور سب کچھ اوپن سورس اور لوکل فرسٹ ہے۔ اس کا مطلب ہے کہ آپ بالکل وہی پڑھ سکتے ہیں اور تبدیل کر سکتے ہیں جو ڈیٹا کے ساتھ کیا جاتا ہے۔
رازداری کا ماڈل کیا شامل کرتا ہے۔
رازداری نے ہر قدم پر تعمیر کو شکل دی ہے، اور یہ اس کے مواد کو ایک جگہ جمع کرنے کے قابل ہے۔ پوری بنیادی پائپ لائن، سکور شدہ تھریڈز کے ذریعے کیپچر کی گئی ہے، بغیر کسی نیٹ ورک کال کے مقامی طور پر IndexedDB پر چلتی ہے۔ آپ کی براؤزنگ ہسٹری - خام واقعات، سیشنز، تھریڈز، وغیرہ - آپ کے کمپیوٹر کو سسٹم کے کسی ایسے حصے کے لیے نہیں چھوڑتی جو بغیر چابیاں کے کام کرتا ہے۔
دو AI پرتیں وہ واحد راستے ہیں جن کے ذریعے تمام ڈیٹا ڈیوائس سے نکل جاتا ہے، اور دونوں اپنی API کیز فراہم کرنے والے صارفین کے لیے آپٹ ان کرتے ہیں۔ رن ٹائم پر بھیجا جانے والا مواد جان بوجھ کر کم سے کم ہے۔ برانڈ اینہانسمنٹ صرف بنیادی ڈومین نام بھیجتا ہے، URL یا صفحہ کا مواد نہیں، context.dev پر اور پہلے مقامی پتے نکال دیتا ہے۔ لیبلنگ اور اسسٹنٹ تھریڈ کا عنوان، خلاصہ، کلیدی الفاظ، اور نمونہ صفحہ کا عنوان Anthropic کو بھیجتا ہے، جو آپ کے کوڈ سے براہ راست پڑھنے کے لیے پہلے سے طے شدہ سیاق و سباق ہے اور اس سے زیادہ کچھ نہیں۔ کلید خود chrome.storage.localیہ کبھی بھی مطابقت پذیر نہیں ہوتا ہے۔
میں اسے آگے کہاں لے جاؤں؟
تعمیر میں اچھی مشق کے لیے کچھ جان بوجھ کر آسانیاں شامل ہیں۔
سب سے زیادہ اطمینان بخش چیز براہ راست کوڈ کے اوپری حصے پر بنانا ہے جو آپ پہلے ہی لکھ چکے ہیں۔ ڈومین کی طرف ambient.tsایسے ڈومینز کو ہٹا دیں جو زیادہ فعال دنوں میں ظاہر ہوتے ہیں۔ تاہم، مطلوبہ الفاظ کی طرف کوئی مساوی لفظ نہیں ہے، لہذا یہ ہر جگہ موجود لفظ ہے۔ آپ کے لیے (کہو typescriptاگر آپ TypeScript ڈویلپر ہیں)، تو آپ تمام سیشنز میں کلیدی الفاظ کو زندہ رکھ سکتے ہیں اور غیر متعلقہ تھریڈز کو ایک ساتھ باندھ سکتے ہیں۔
فکس فریکوئنسی پر مبنی مطلوبہ الفاظ کا پتہ لگانے والا ہے جو اس کی عکاسی کرتا ہے: detectAmbientDomains تقریباً سطر بہ سطر، ہم دن فی ڈومین کے بجائے فی کلیدی لفظ کے دنوں کا حساب لگاتے ہیں۔
export function detectAmbientKeywords(sessions: Session[]): Set {
const allEvents = sessions.flatMap((s) => s.events);
const activeDays = new Set(allEvents.map((e) => new Date(e.visitedAt).toDateString()));
const totalActiveDays = activeDays.size;
if (totalActiveDays < MIN_ACTIVE_DAYS) return new Set();
const keywordDayMap = new Map>();
for (const session of sessions) {
const day = new Date(session.startedAt).toDateString();
for (const kw of session.keywords) {
if (!keywordDayMap.has(kw)) keywordDayMap.set(kw, new Set());
keywordDayMap.get(kw)!.add(day);
}
}
const ambient = new Set();
for (const [kw, days] of keywordDayMap) {
if (days.size / totalActiveDays >= UBIQUITY_THRESHOLD) ambient.add(kw);
}
return ambient;
}
پھر ہم ان مطلوبہ الفاظ کو اندر سے ہٹا دیتے ہیں۔ similarity آج یہ بالکل ایسا ہی ہے جیسے دونوں ڈومینز کو فلٹر کرنے کے ساتھ ارد گرد کے ڈومینز کو ہٹا دیا جائے۔ sessionKeywords اور تھریڈ میں keywordSet جیکارڈ کال کرنے سے پہلے۔
دو چھوٹی مشقیں ختم ہوتی ہیں۔ سیشن کا وقفہ، مماثلت کی حد، اور پڑوس کی ہر جگہ کی حد سبھی ہارڈ کوڈ والے مستقل ہیں۔ معاون ترتیبات کے پینل پر لفٹ کریں۔ chrome.storage.local (وہی سٹوریج جو آپ کی API کیز پہلے ہی استعمال کر رہی ہیں)، آپ کلسٹرنگ کو اپنی تلاش کے مطابق بنا سکتے ہیں۔
اور extractDomain یہ صرف سابقہ کو ہٹاتا ہے۔ www.تو news.bbc.co.uk اور bbc.co.uk ان کے ساتھ مختلف ڈومینز کے طور پر سلوک کیا جاتا ہے۔ لائبریریوں کے لئے میزبان نام کی منطق کو تبدیل کریں جو عوامی لاحقہ فہرست استعمال کرتی ہیں (ڈومین لاحقوں کی ایک کینونیکل فہرست، جیسے: .co.uk آپ جس براؤزر کو یہ جاننے کے لیے استعمال کرتے ہیں کہ ایک رجسٹرڈ ڈومین دراصل کہاں ختم ہوتا ہے وہ اسی سائٹ کے ذیلی ڈومینز کو صحیح طریقے سے ختم کر دے گا۔
پوری پائپ لائن مقامی اور قابل معائنہ ہے، لہذا ہر پائپ لائن کو حقیقی ڈیٹا پر آزمایا جا سکتا ہے اور اس کے اثرات فوری طور پر دیکھے جا سکتے ہیں۔
ختم
اوپن لوپس آپ کو کھلے ہوئے لوپس کو بند کرنے اور اس سادہ، تاریخی تاریخ کو تبدیل کرنے میں مدد کرتے ہیں جسے براؤزر اس کے نقشے میں رکھتا ہے جو آپ اصل میں کرنے کی کوشش کر رہے تھے۔
وقت کے وقفے کی تقسیم، پڑوس میں ترمیم کے ساتھ وزنی جیکارڈ کلسٹرنگ، ہیورسٹک اسکورنگ، حقیقی کمپنی کے ڈیٹا پر مبنی AI لیبلنگ، نتائج پر بات چیت کی تہیں، وغیرہ۔ بنیادی انجینئرنگ ایک قسم کا درجہ بندی کا نظام ہے جہاں ہر قدم اپنے آپ میں آسان ہے اور قدر اس سے آتی ہے کہ اسے کیسے بنایا گیا ہے۔
وسائل
سورس کوڈ
- مکمل ماخذ MIT لائسنس کے تحت GitHub پر دستیاب ہے لہذا آپ اسے چلا سکتے ہیں، اسے پڑھ سکتے ہیں، اور اپنے نیویگیشنل انداز کے مطابق اس کی شکل تبدیل کر سکتے ہیں۔ اگر آپ کو یہ کارآمد لگا، تو براہ کرم اسے ستارہ کی درجہ بندی دینے پر غور کریں۔
اہم دستاویز
استعمال شدہ خدمات
اوزار بنائیں
-
وائٹ: ٹولز اور ڈویلپمنٹ سرور بنائیں
-
CRXJS Vite پلگ ان: ہاٹ ری لوڈنگ کے ساتھ مینی فیسٹ V3 ایکسٹینشن مرتب کریں۔
-
idb: وعدہ پر مبنی IndexedDB ریپر ٹائپ کیا گیا۔
-
ری ایکٹ مارک ڈاون: اپنے اسسٹنٹ سے مارک ڈاون جواب پیش کریں۔
ڈیبگنگ ٹولز
-
کروم ایکسٹینشن سروس ورکر ڈیو ٹولز: ریئل ٹائم کیپچر لاگز اور پائپ لائن کا معائنہ
console.tableحساب -
کہ درخواست → IndexedDB Chrome DevTools میں پینلز: براؤز کریں۔
raw_events,sessions,intent_threadsاورdomain_brandsہر قدم کو خود چیک کرنے کے لیے
مزید پڑھنا
-
جیکارڈ انڈیکس: تھریڈ کلسٹرنگ کے پیچھے ایک مماثلت سیٹ پیمانہ۔
-
عوامی لاحقہ فہرست: قابل رجسٹر ڈومینز نکالنے کا مناسب طریقہ، مستقبل میں بہتری کے طور پر ذکر کیا گیا ہے۔
اگر آپ کو یہ ٹیوٹوریل کارآمد معلوم ہوا تو بلا جھجک دوسروں کے ساتھ اس کا اشتراک کریں جو فائدہ اٹھا سکتے ہیں۔ ہم واقعی آپ کے تبصرے کی تعریف کرتے ہیں۔ آپ X پر @wani_shola کا ذکر کر سکتے ہیں یا LinkedIn پر مجھ سے رابطہ کر سکتے ہیں۔