Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"$schema": "https://json.schemastore.org/mocharc.json",
"require": "tsx"
"require": ["tsx", "tests/setup.ts"]
}
76 changes: 43 additions & 33 deletions background.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
export { }

import { getDB, getUrlId, lockAndRunPglite, deletePagesOlderThan, storeEmbeddings, urlIsPresentOrInDatetimeRange } from "~db";
import { getDB, getOrCreatePage, lockAndRunPglite, deletePagesOlderThan, storeEmbeddings, urlIsPresentOrInDatetimeRange } from "~db";
import { pipeline, env, type PipelineType } from "@xenova/transformers";
import { PGliteWorker } from "~dist/electric-sql/worker";
import type { Chunk } from "~lib/chunk";
import { MODEL_TYPE } from "~lib/chunk";

Expand All @@ -19,18 +18,14 @@ class PipelineSingleton {
if (this.instance === null) {
this.instance = pipeline(this.task, this.model, {
progress_callback,
// dtype: "fp32",
// device: !!navigator.gpu ? "webgpu" : "wasm"
})
}
return this.instance;
}
}

const getLLMPipeline = async () => {
return await PipelineSingleton.getInstance((x) => {
console.log("Progress update", x)
});
return await PipelineSingleton.getInstance();
}

const runInference = async (pipeline, chunk: string) => {
Expand All @@ -49,49 +44,64 @@ const deleteIfUrlIsExpired = async ({ db }) => {

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {

// check the URL - if it exists, check if age of the URL. if older than a day, delete the old entries, chunk new and get new embs
if (message.type === "process_page") {

const { type, url, textChunks }: { type: string, url: string, textChunks: Chunk[] } = message;

(async () => {
const urlIsPresent = await lockAndRunPglite(urlIsPresentOrInDatetimeRange, { url });

if (urlIsPresent) {
// URL exists OR is still valid. Do not process, and return.
sendResponse({ ok: true, error: null, msg: `URL ${url} has been processed before. Skipping...` })
} else {
const urlId = await lockAndRunPglite(getUrlId, { url });
const pipeline = await getLLMPipeline();

if (textChunks.length <= 0) {
try {
const urlIsPresent = await lockAndRunPglite(urlIsPresentOrInDatetimeRange, { url });

if (urlIsPresent) {
console.log(`[casper] Skipping ${url} (already processed)`);
sendResponse({ ok: true, error: null, msg: `URL ${url} has been processed before. Skipping...` })
} else if (textChunks.length <= 0) {
console.log(`[casper] Skipping ${url} (no chunks extracted)`);
sendResponse({ ok: false, error: null, msg: `No chunks sent.` })
} else {
navigator.locks.request("pglite", async (lock) => {
let pg = await getDB()
for (let chunk of textChunks) {
let embedding = await runInference(pipeline, chunk.content);
await storeEmbeddings(pg, urlId, chunk, embedding);
console.log(`[casper] Processing ${url} (${textChunks.length} chunks)...`);
const llmPipeline = await getLLMPipeline();

await navigator.locks.request("pglite", async () => {
const pg = await getDB();
const pageId = await getOrCreatePage(pg, url);

for (const chunk of textChunks) {
const embedding = await runInference(llmPipeline, chunk.content);
await storeEmbeddings(pg, pageId, chunk, embedding);
}
});

sendResponse({ ok: true, error: null, msg: `Done processing ${textChunks.length} chunks.` })
})
console.log(`[casper] Done processing ${url}`);
sendResponse({ ok: true, error: null, msg: `Done processing ${textChunks.length} chunks.` })
}
} catch (err) {
console.error(`[casper] process_page failed for ${url}:`, err);
sendResponse({ ok: false, error: String(err), msg: "process_page failed" })
}
})()
} else if (message.type === "get_embedding") {
(async () => {
const { chunk } = message;
const pipeline = await getLLMPipeline();
let embedding = await runInference(pipeline, chunk);

sendResponse({ ok: true, error: null, embedding })
try {
const { chunk } = message;
const llmPipeline = await getLLMPipeline();
const embedding = await runInference(llmPipeline, chunk);

sendResponse({ ok: true, error: null, embedding })
} catch (err) {
console.error("[casper] get_embedding failed:", err);
sendResponse({ ok: false, error: String(err), embedding: null })
}
})()
} else if (message.type === "clean_up") {
(async () => {
await lockAndRunPglite(deleteIfUrlIsExpired, {});

sendResponse({ ok: true, error: null })
try {
await lockAndRunPglite(deleteIfUrlIsExpired, {});
sendResponse({ ok: true, error: null })
} catch (err) {
console.error("[casper] clean_up failed:", err);
sendResponse({ ok: false, error: String(err) })
}
})()
}

Expand Down
28 changes: 18 additions & 10 deletions components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,22 @@ export const Search: React.FC<SearchProps> = ({ worker, searchResults, setSearch
if (worker && textToSearch) {
setIsSearching(true);
setPrevSearchText("");
const backgroundResponse = await sendTextChunkToBackground(textToSearch);
const results = await search(worker, backgroundResponse.embedding, 0.3, 5);
try {
const backgroundResponse = await sendTextChunkToBackground(textToSearch);
const results = await search(worker, backgroundResponse.embedding, 0.3, 5);

if (results.length === 0) {
if (results.length === 0) {
setNoSearchResults(true);
} else {
setNoSearchResults(false);
setSearchResults(results);
}
} catch (err) {
console.error("Search failed:", err);
setNoSearchResults(true);
} else {
setNoSearchResults(false);
setSearchResults(results);
} finally {
setIsSearching(false);
}
setIsSearching(false);
}
}

Expand All @@ -56,7 +62,7 @@ export const Search: React.FC<SearchProps> = ({ worker, searchResults, setSearch
}, [worker])

useEffect(() => {
if (searchResults && searchResults.length > 0 && textToSearch) {
if (worker && searchResults && searchResults.length > 0 && textToSearch) {
const cacheResults = async () => {
await storeSearchCache(
worker,
Expand All @@ -68,12 +74,14 @@ export const Search: React.FC<SearchProps> = ({ worker, searchResults, setSearch

cacheResults().catch(console.error)
}
}, [searchResults])
}, [searchResults, worker, textToSearch])

const clearSearchResults = async () => {
setPrevSearchText("")
setSearchResults([])
await deleteStoreCache(worker)
if (worker) {
await deleteStoreCache(worker)
}
}

return (
Expand Down
10 changes: 5 additions & 5 deletions components/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ export const SearchResultsTable = ({ results }: { results: SearchResult[] }) =>
{results.map((res) => (
<TableRow key={res.id}>
<TableCell className="font-bold">
<a href={`${res.url}#${res.chunk_tag_id}`} className="link-underline-animation">
<a href={`${res.url ?? ""}${res.chunk_tag_id ? `#${res.chunk_tag_id}` : ""}`} className="link-underline-animation">
{extractDomain(res.url)}
</a>
</TableCell>
<TableCell>
<a href={`${res.url}#${res.chunk_tag_id}`} className="link-underline-animation">
{res.content.slice(0, 100) + "..."}
<a href={`${res.url ?? ""}${res.chunk_tag_id ? `#${res.chunk_tag_id}` : ""}`} className="link-underline-animation">
{(res.content ?? "").slice(0, 100) + "..."}
</a>
</TableCell>
<TableCell className="text-right">{res.prob.toFixed(2)}</TableCell>
<TableCell className="text-right">{res.prob != null ? res.prob.toFixed(2) : "—"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
}
25 changes: 11 additions & 14 deletions components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"
import { Button } from "./Button"
import { Card } from "./Card"
import { type PGliteWorker } from "~dist/electric-sql/worker";
import { removeFilterSites, saveFilterSites, saveModelType, getFilterSites, nukeDb } from "~db";
import { removeFilterSites, saveFilterSites, getFilterSites, nukeDb } from "~db";
import { Label } from "./Label"
import { ModelSelector } from "./ModelSelector"
import { SelectTagInput } from "./TagInputs"
Expand Down Expand Up @@ -42,9 +42,10 @@ export const Settings: React.FC<SettingsProps> = ({ pg, sitesToFilter, setSitesT
getExistingFilterSites().catch(console.error);
setComponentSetup(true);
}
}, []);
}, [pg]);

const updateSites = async () => {
if (!pg) return;
const { addedSites, removedSites } = getDifferenceBetweenOldAndNew(originalSitesToFilter, sitesToFilter);

if (removedSites.size > 0) {
Expand All @@ -70,32 +71,28 @@ export const Settings: React.FC<SettingsProps> = ({ pg, sitesToFilter, setSitesT
}

const onSettingsSave = async () => {

// TODO:
// saveModelType(pg, "")
setHasChanged(false);
await updateSites()
try {
await updateSites();
setHasChanged(false);
} catch (err) {
console.error("[casper] Failed to save settings:", err);
}
}

useEffect(() => {
// check if the sites have been updated and changed
// we also need to wait for the component to setup as the initial useEffect populates the originalSitesToFilter, otherwise
// this if statement executes before the originalSitesToFilter has been populated
if (!hasChanged && componentSetup) {
console.log("sites to filter has changed. updating..")
console.log("ori sites", originalSitesToFilter)
console.log("new sites", sitesToFilter)
const { addedSites, removedSites } = getDifferenceBetweenOldAndNew(originalSitesToFilter, sitesToFilter);
if (addedSites.size > 0 || removedSites.size > 0) {
setHasChanged(true);
}
}
}, [sitesToFilter])
}, [sitesToFilter, hasChanged, componentSetup])

const deleteDb = async () => {
if (pg) {
await nukeDb(pg);
setShowDeleteConfirmation(true);
setTimeout(() => setShowDeleteConfirmation(false), 3000);
}
}

Expand Down
25 changes: 17 additions & 8 deletions content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,29 @@ const PlasmoOverlay = () => {
const webpageBodyContent = extractHtmlBody();

const processPage = async () => {
const chunks = await parseChunkHtmlContent(webpageBodyContent);
const backgroundResponse = await processPageInBackground(location.href, chunks)
console.log("Page has been processed. Background response: ", backgroundResponse)
try {
const chunks = await parseChunkHtmlContent(webpageBodyContent);
const response = await processPageInBackground(location.href, chunks);
if (response && !response.ok) {
console.warn("[casper] Background processing failed:", response.error || response.msg);
}
} catch (err) {
console.error("Failed to process page:", err);
}
}

const cleanUp = async () => {
if (isRandomlyBelow(0.05)) {
const backgroundResponse = await cleanUpUrlEmbeddings();
console.log("Delete check ran. Background response:", backgroundResponse);
try {
if (isRandomlyBelow(0.05)) {
await cleanUpUrlEmbeddings();
}
} catch (err) {
console.error("Cleanup failed:", err);
}
}

processPage().catch(console.error);
cleanUp().catch(console.error)
processPage();
cleanUp();

}, [])

Expand Down
Loading