fix(global search): CSS preload killed plugin load

This commit is contained in:
2026-06-27 15:10:24 +09:30
parent 1045d38f47
commit 3213d0ca28
29 changed files with 327 additions and 62 deletions
+3
View File
@@ -9,6 +9,9 @@ bun.lock
# PDF.js extension assets (copied by postinstall from pdfjs-dist) # PDF.js extension assets (copied by postinstall from pdfjs-dist)
src/public/resources/pdfjs/pdf.worker.min.mjs src/public/resources/pdfjs/pdf.worker.min.mjs
src/public/resources/pdfjs/pdf.legacy.min.mjs src/public/resources/pdfjs/pdf.legacy.min.mjs
# ONNX Runtime WASM assets (copied by postinstall from @huggingface/transformers)
src/public/resources/ort/ort-wasm-simd-threaded.jsep.mjs
src/public/resources/ort/ort-wasm-simd-threaded.jsep.wasm
# Build # Build
extension.zip extension.zip
+8 -1
View File
@@ -15,8 +15,15 @@ export function extensionChunkUrls(): Plugin {
base: "./", base: "./",
experimental: { experimental: {
renderBuiltUrl(filename, { hostType, type }) { renderBuiltUrl(filename, { hostType, type }) {
const path = filename.replace(/^\//, "");
if (type === "chunk" && hostType === "js") { if (type === "chunk" && hostType === "js") {
const path = filename.replace(/^\//, ""); return {
runtime: `chrome.runtime.getURL(${JSON.stringify(path)})`,
};
}
// Rewrite CSS preloads from JS dynamic imports (content scripts).
// Do not rewrite hostType "css" — extension HTML pages need static hrefs.
if (type === "asset" && hostType === "js" && path.endsWith(".css")) {
return { return {
runtime: `chrome.runtime.getURL(${JSON.stringify(path)})`, runtime: `chrome.runtime.getURL(${JSON.stringify(path)})`,
}; };
+6 -5
View File
@@ -43,12 +43,13 @@ export default function InlineWorkerDevPlugin(): Plugin {
// Note: Original code had `await fs.readFile(cleanPath, "utf-8");` but `code` wasn't used. // Note: Original code had `await fs.readFile(cleanPath, "utf-8");` but `code` wasn't used.
// `esbuild` directly takes `cleanPath` as an entry point. // `esbuild` directly takes `cleanPath` as an entry point.
const result = await build({ const result = await build({
entryPoints: [cleanPath], // esbuild uses the file path directly entryPoints: [cleanPath],
bundle: true, bundle: true,
write: false, // We want the output in memory, not written to disk write: false,
platform: "browser", // Target environment for the worker code platform: "browser",
format: "iife", // Immediately Invoked Function Expression, suitable for workers format: "iife",
target: "esnext", // Transpile to modern JavaScript target: "esnext",
external: ["webextension-polyfill"],
}); });
const workerCode = result.outputFiles[0].text; const workerCode = result.outputFiles[0].text;
+1 -1
View File
@@ -6,7 +6,7 @@
"browserslist": "> 0.5%, last 2 versions, not dead", "browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": { "scripts": {
"compile:layerchart": "node scripts/compile-layerchart-vendor.mjs", "compile:layerchart": "node scripts/compile-layerchart-vendor.mjs",
"postinstall": "node scripts/copy-pdfjs-assets.mjs && npm run compile:layerchart", "postinstall": "node scripts/copy-pdfjs-assets.mjs && node scripts/copy-ort-wasm-assets.mjs && npm run compile:layerchart",
"autoaudit": "npm audit && npm audit fix && npm run build", "autoaudit": "npm audit && npm audit fix && npm run build",
"dev": "cross-env MODE=chrome vite dev", "dev": "cross-env MODE=chrome vite dev",
"dev:firefox": "cross-env MODE=firefox vite build --watch", "dev:firefox": "cross-env MODE=firefox vite build --watch",
+24
View File
@@ -0,0 +1,24 @@
import { copyFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const transformersDist = join(
root,
"node_modules",
"@huggingface",
"transformers",
"dist",
);
const outDir = join(root, "src", "public", "resources", "ort");
mkdirSync(outDir, { recursive: true });
const ortFiles = [
"ort-wasm-simd-threaded.jsep.mjs",
"ort-wasm-simd-threaded.jsep.wasm",
];
for (const file of ortFiles) {
copyFileSync(join(transformersDist, file), join(outDir, file));
}
+21
View File
@@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
button {
@apply cursor-pointer;
}
::-webkit-scrollbar {
display: none;
}
input {
&:focus {
box-shadow: unset !important;
}
}
.no-scrollbar {
scrollbar-width: none !important;
}
+2 -1
View File
@@ -2,6 +2,7 @@ import "./index.css";
import Settings from "./pages/settings.svelte"; import Settings from "./pages/settings.svelte";
import IconFamily from "@/resources/fonts/IconFamily.woff"; import IconFamily from "@/resources/fonts/IconFamily.woff";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import renderSvelte from "./main"; import renderSvelte from "./main";
import { initializeSettingsState } from "@/seqta/utils/listeners/SettingsState"; import { initializeSettingsState } from "@/seqta/utils/listeners/SettingsState";
import { initVerboseLogging, verboseInfo } from "@/utils/verboseLog"; import { initVerboseLogging, verboseInfo } from "@/utils/verboseLog";
@@ -14,7 +15,7 @@ function InjectCustomIcons() {
style.innerHTML = ` style.innerHTML = `
@font-face { @font-face {
font-family: 'IconFamily'; font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff'); src: url('${resolveExtensionAssetUrl(IconFamily)}') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
}`; }`;
-2
View File
@@ -1,5 +1,3 @@
import "./index.css";
declare module "*.png"; declare module "*.png";
declare module "*.svg"; declare module "*.svg";
declare module "*.jpeg"; declare module "*.jpeg";
+26
View File
@@ -0,0 +1,26 @@
import { mount } from "svelte";
import type { SvelteComponent } from "svelte";
import style from "./contentShadow.css?inline";
/** Mount Svelte UI inside a shadow root from content scripts (decoupled from settings popup CSS). */
export default function renderInShadow(
Component: SvelteComponent | any,
mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {},
) {
const app = mount(Component, {
target: mountPoint,
props: {
standalone: false,
...props,
},
});
if (mountPoint instanceof ShadowRoot) {
const styleElement = document.createElement("style");
styleElement.textContent = style;
mountPoint.appendChild(styleElement);
}
return app;
}
+43
View File
@@ -0,0 +1,43 @@
import browser from "webextension-polyfill";
const ORT_RESOURCE_DIR = "resources/ort/";
let configured = false;
function extensionAssetUrl(relativePath: string): string {
return browser.runtime.getURL(relativePath.replace(/^\/+/, ""));
}
/**
* Point HuggingFace transformers / onnxruntime at extension-local WASM files
* instead of CDN (required on SEQTA pages where page CSP blocks jsdelivr).
* Safe to call multiple times; must run before embeddia `initializeModel()`.
*/
export async function ensureTransformersEnv(
ortWasmBase?: string,
): Promise<void> {
if (configured) return;
const { env } = await import("@huggingface/transformers");
const base = ortWasmBase ?? extensionAssetUrl(ORT_RESOURCE_DIR);
env.backends.onnx.wasm = env.backends.onnx.wasm ?? {};
env.backends.onnx.wasm.wasmPaths = base.endsWith("/") ? base : `${base}/`;
configured = true;
}
export function getOrtWasmBaseUrl(): string {
const base = extensionAssetUrl(ORT_RESOURCE_DIR);
return base.endsWith("/") ? base : `${base}/`;
}
/** For page-origin blob workers that cannot call `browser.runtime.getURL`. */
export async function configureTransformersEnvForBase(
ortWasmBase: string,
): Promise<void> {
configured = false;
await ensureTransformersEnv(ortWasmBase);
}
export { ORT_RESOURCE_DIR };
+3 -1
View File
@@ -36,7 +36,9 @@
"resources/icons/*", "resources/icons/*",
"resources/update-image.webp", "resources/update-image.webp",
"resources/pdfjs/pdf.worker.min.mjs", "resources/pdfjs/pdf.worker.min.mjs",
"resources/pdfjs/pdf.legacy.min.mjs" "resources/pdfjs/pdf.legacy.min.mjs",
"resources/ort/*",
"assets/*.css"
], ],
"matches": ["*://*/*"] "matches": ["*://*/*"]
} }
+5 -4
View File
@@ -7,7 +7,10 @@ import {
} from "../../core/settingsHelpers"; } from "../../core/settingsHelpers";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import styles from "./src/core/styles.css?inline"; import styles from "./src/core/styles.css?inline";
import { resetSearchIndexes } from "./src/indexing/resetIndexes"; import {
resetSearchIndexes,
notifyOpenTabsResetSearchIndex,
} from "./src/indexing/resetIndexes";
// Platform-aware default hotkey // Platform-aware default hotkey
const getDefaultHotkey = () => { const getDefaultHotkey = () => {
@@ -52,9 +55,7 @@ const settings = defineSettings({
if (!confirmed) return; if (!confirmed) return;
try { try {
// `resetSearchIndexes` is a tiny statically-imported helper: no await notifyOpenTabsResetSearchIndex();
// dynamic chunks to chase, so the button keeps working even when
// the settings page has been open across an extension update.
await resetSearchIndexes(); await resetSearchIndexes();
alert( alert(
"Search index and storage were reset.\n\nReload this tab to regenerate the index.", "Search index and storage were reset.\n\nReload this tab to regenerate the index.",
@@ -11,6 +11,8 @@ import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
import styles from "./styles.css?inline"; import styles from "./styles.css?inline";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing, ensureSchemaCurrent } from "../indexing/indexer"; import { runIndexing, ensureSchemaCurrent } from "../indexing/indexer";
import { installResetIndexMessageListener } from "../indexing/resetIndexes";
import { isIndexingPaused } from "../indexing/indexingPause";
import { initVectorSearch } from "../search/vector/vectorSearch"; import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar"; import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from "embeddia"; import { IndexedDbManager } from "embeddia";
@@ -168,6 +170,8 @@ const globalSearchPlugin: Plugin<typeof settings> = {
run: async (api) => { run: async (api) => {
const appRef = { current: null }; const appRef = { current: null };
installResetIndexMessageListener();
// Run the version check BEFORE we open any IndexedDB connections. // Run the version check BEFORE we open any IndexedDB connections.
// On a normal load (no version change) this is just a string compare // On a normal load (no version change) this is just a string compare
// and a manifest read, so the cost is negligible. On a real update, // and a manifest read, so the cost is negligible. On a real update,
@@ -287,8 +291,9 @@ const globalSearchPlugin: Plugin<typeof settings> = {
} }
} }
if (api.settings.runIndexingOnLoad) { if (api.settings.runIndexingOnLoad && !isIndexingPaused()) {
setTimeout(async () => { setTimeout(async () => {
if (isIndexingPaused()) return;
await runIndexing(); await runIndexing();
}, 2000); }, 2000);
} }
@@ -280,7 +280,7 @@ export async function mountSearchBar(
}); });
try { try {
const { default: renderSvelte } = await import("@/interface/main"); const { default: renderSvelte } = await import("@/interface/renderInShadow");
appRef.current = renderSvelte(SearchBar, searchRootShadow, { appRef.current = renderSvelte(SearchBar, searchRootShadow, {
transparencyEffects: api.settings.transparencyEffects ? true : false, transparencyEffects: api.settings.transparencyEffects ? true : false,
showRecentFirst: api.settings.showRecentFirst, showRecentFirst: api.settings.showRecentFirst,
@@ -7,6 +7,7 @@ import { loadDynamicItems } from "../utils/dynamicItems";
import { getVectorizedItemIds, pruneOrphanVectorEmbeddings } from "./utils"; import { getVectorizedItemIds, pruneOrphanVectorEmbeddings } from "./utils";
import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion"; import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion";
import { resetSearchIndexes } from "./resetIndexes"; import { resetSearchIndexes } from "./resetIndexes";
import { isIndexingPaused } from "./indexingPause";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog'; import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const META_STORE = "meta"; const META_STORE = "meta";
@@ -252,8 +253,19 @@ export async function loadAllStoredItems(): Promise<IndexItem[]> {
} }
export async function runIndexing(): Promise<void> { export async function runIndexing(): Promise<void> {
if (isIndexingPaused()) {
verboseDebug(
"[Indexer] Skipping indexing — index was reset; reload the page to rebuild.",
);
return;
}
await ensureSchemaCurrent(); await ensureSchemaCurrent();
if (isIndexingPaused()) {
return;
}
if (!(await acquireLock())) { if (!(await acquireLock())) {
verboseDebug( verboseDebug(
"%c[Indexer] Could not acquire lock - another tab is indexing or this tab is already indexing", "%c[Indexer] Could not acquire lock - another tab is indexing or this tab is already indexing",
@@ -272,6 +284,19 @@ export async function runIndexing(): Promise<void> {
dispatchProgress(completedJobs, totalSteps, true, "Starting jobs"); dispatchProgress(completedJobs, totalSteps, true, "Starting jobs");
for (const jobId of jobIds) { for (const jobId of jobIds) {
if (isIndexingPaused()) {
verboseDebug(
"[Indexer] Indexing stopped — index was reset; reload the page to rebuild.",
);
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing paused — reload to rebuild",
);
return;
}
dispatchProgress( dispatchProgress(
completedJobs, completedJobs,
totalSteps, totalSteps,
@@ -0,0 +1,10 @@
/** In-memory gate: after a manual reset, skip indexing until the tab reloads. */
let pausedUntilReload = false;
export function pauseIndexingUntilReload(): void {
pausedUntilReload = true;
}
export function isIndexingPaused(): boolean {
return pausedUntilReload;
}
@@ -395,7 +395,7 @@ export const messagesJob: Job = {
progress.totalEstimated = await estimateMessageCount(); progress.totalEstimated = await estimateMessageCount();
try { try {
await vectorWorker.startStreamingSession( progress.streamingStarted = await vectorWorker.startStreamingSession(
progress.totalEstimated, progress.totalEstimated,
(progressData) => { (progressData) => {
verboseLog( verboseLog(
@@ -405,10 +405,11 @@ export const messagesJob: Job = {
RATE_LIMIT_CONFIG.vectorBatchSize, RATE_LIMIT_CONFIG.vectorBatchSize,
"messages", "messages",
); );
progress.streamingStarted = true; if (progress.streamingStarted) {
verboseLog( verboseLog(
`[Messages job] Started streaming vectorization session for ~${progress.totalEstimated} items`, `[Messages job] Started streaming vectorization session for ~${progress.totalEstimated} items`,
); );
}
} catch (error) { } catch (error) {
console.warn( console.warn(
"[Messages job] Failed to start streaming session:", "[Messages job] Failed to start streaming session:",
@@ -199,7 +199,7 @@ export const notificationsJob: Job = {
const estimatedTotal = Math.min(notifications.length * 1.2, 100); const estimatedTotal = Math.min(notifications.length * 1.2, 100);
try { try {
await vectorWorker.startStreamingSession( progress.streamingStarted = await vectorWorker.startStreamingSession(
estimatedTotal, estimatedTotal,
(progressData) => { (progressData) => {
verboseLog( verboseLog(
@@ -209,10 +209,11 @@ export const notificationsJob: Job = {
NOTIFICATIONS_RATE_LIMIT.vectorBatchSize, NOTIFICATIONS_RATE_LIMIT.vectorBatchSize,
"notifications", "notifications",
); );
progress.streamingStarted = true; if (progress.streamingStarted) {
verboseLog( verboseLog(
`[Notifications job] Started streaming vectorization session for ~${estimatedTotal} items`, `[Notifications job] Started streaming vectorization session for ~${estimatedTotal} items`,
); );
}
} catch (error) { } catch (error) {
console.warn( console.warn(
"[Notifications job] Failed to start streaming session:", "[Notifications job] Failed to start streaming session:",
@@ -10,6 +10,7 @@ import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
import { isSensitiveSeqtaPath, normalizeSeqtaPath } from "./api"; import { isSensitiveSeqtaPath, normalizeSeqtaPath } from "./api";
import { mergeDynamicItems } from "../utils/dynamicItems"; import { mergeDynamicItems } from "../utils/dynamicItems";
import { decorateIndexItems } from "./renderComponents"; import { decorateIndexItems } from "./renderComponents";
import { isIndexingPaused } from "./indexingPause";
/** /**
* Passive network observer. * Passive network observer.
@@ -380,7 +381,7 @@ function synthesizeItems(
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
async function persistItems(items: IndexItem[]): Promise<void> { async function persistItems(items: IndexItem[]): Promise<void> {
if (items.length === 0) return; if (items.length === 0 || isIndexingPaused()) return;
// Dedupe against existing entries. We replace on collision so the latest // Dedupe against existing entries. We replace on collision so the latest
// observation wins (e.g. if a message changes title). // observation wins (e.g. if a message changes title).
@@ -401,16 +402,27 @@ async function persistItems(items: IndexItem[]): Promise<void> {
} }
function scheduleFlush() { function scheduleFlush() {
if (pendingFlush) return; if (pendingFlush || isIndexingPaused()) return;
pendingFlush = setTimeout(() => { pendingFlush = setTimeout(() => {
pendingFlush = null; pendingFlush = null;
if (!pendingDirty) return; if (!pendingDirty || isIndexingPaused()) return;
pendingDirty = false; pendingDirty = false;
void flushDynamicItems(); void flushDynamicItems();
}, FLUSH_DEBOUNCE_MS); }, FLUSH_DEBOUNCE_MS);
} }
/** Drop queued passive captures after a manual index reset. */
export function pausePassiveObserver(): void {
pendingChangedItems.clear();
pendingDirty = false;
if (pendingFlush) {
clearTimeout(pendingFlush);
pendingFlush = null;
}
}
async function flushDynamicItems(): Promise<void> { async function flushDynamicItems(): Promise<void> {
if (isIndexingPaused()) return;
if (pendingChangedItems.size === 0) return; if (pendingChangedItems.size === 0) return;
const rawChanged = Array.from(pendingChangedItems.values()); const rawChanged = Array.from(pendingChangedItems.values());
@@ -1,4 +1,48 @@
import { SCHEMA_VERSION_KEY } from "./schemaVersion"; import { SCHEMA_VERSION_KEY } from "./schemaVersion";
import { pauseIndexingUntilReload } from "./indexingPause";
import { pausePassiveObserver } from "./passiveObserver";
import browser from "webextension-polyfill";
export const RESET_INDEX_MESSAGE = "global-search-reset-index";
let resetMessageListenerInstalled = false;
/** Notify open SEQTA tabs to pause indexing and wipe page-origin stores. */
export async function notifyOpenTabsResetSearchIndex(): Promise<void> {
const tabs = await browser.tabs.query({});
await Promise.allSettled(
tabs.map((tab) =>
tab.id != null
? browser.tabs.sendMessage(tab.id, { type: RESET_INDEX_MESSAGE })
: Promise.resolve(),
),
);
}
/** Content scripts: handle reset broadcast from the settings popup. */
export function installResetIndexMessageListener(): void {
if (resetMessageListenerInstalled) return;
resetMessageListenerInstalled = true;
browser.runtime.onMessage.addListener((message) => {
if (message?.type !== RESET_INDEX_MESSAGE) return;
pauseIndexingUntilReload();
pausePassiveObserver();
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("indexing-progress", {
detail: {
completed: 0,
total: 0,
indexing: false,
status: "Indexing paused — reload to rebuild",
},
}),
);
}
void resetSearchIndexes();
});
}
/** /**
* Hard-reset of all global-search persistence. * Hard-reset of all global-search persistence.
@@ -1,7 +1,15 @@
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia"; import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { IndexItem } from "../types"; import type { IndexItem } from "../types";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog'; import { verboseDebug, verboseInfo, verboseLog } from "./workerVerboseLog";
let ortWasmBase: string | null = null;
async function configureOrtWasm(base: string): Promise<void> {
const { env } = await import("@huggingface/transformers");
env.backends.onnx.wasm = env.backends.onnx.wasm ?? {};
env.backends.onnx.wasm.wasmPaths = base.endsWith("/") ? base : `${base}/`;
}
let vectorIndex: EmbeddingIndex | null = null; let vectorIndex: EmbeddingIndex | null = null;
let isInitialized = false; let isInitialized = false;
let initializationFailed = false; let initializationFailed = false;
@@ -49,6 +57,9 @@ async function initWorker() {
verboseDebug("Initializing vector worker..."); verboseDebug("Initializing vector worker...");
try { try {
if (ortWasmBase) {
await configureOrtWasm(ortWasmBase);
}
await initializeModel(); await initializeModel();
vectorIndex = new EmbeddingIndex([]); vectorIndex = new EmbeddingIndex([]);
@@ -562,6 +573,9 @@ self.addEventListener("message", async (e) => {
switch (type) { switch (type) {
case "init": case "init":
if (data?.ortWasmBase) {
ortWasmBase = data.ortWasmBase;
}
await initWorker(); await initWorker();
self.postMessage({ type: "ready" }); self.postMessage({ type: "ready" });
break; break;
@@ -594,13 +608,3 @@ self.addEventListener("message", async (e) => {
console.warn("Unknown message type:", type); console.warn("Unknown message type:", type);
} }
}); });
initWorker()
.then(() => {
self.postMessage({ type: "ready" });
})
.catch((err) => {
console.error("Initial worker initialization failed:", err);
self.postMessage({ type: "ready" });
});
@@ -1,6 +1,7 @@
import { refreshVectorCache } from "../../search/vector/vectorSearch"; import { refreshVectorCache } from "../../search/vector/vectorSearch";
import type { IndexItem } from "../types"; import type { IndexItem } from "../types";
import { isVectorSearchSupported } from "../../utils/browserDetection"; import { isVectorSearchSupported } from "../../utils/browserDetection";
import { getOrtWasmBaseUrl } from "@/lib/transformersExtension";
import vectorWorker from "./vectorWorker.ts?inlineWorker"; import vectorWorker from "./vectorWorker.ts?inlineWorker";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog'; import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
@@ -91,7 +92,7 @@ export class VectorWorkerManager {
this.isInitialized = false; this.isInitialized = false;
reject(new Error("Worker initialization timed out")); reject(new Error("Worker initialization timed out"));
}, 10000); }, 60000);
this.worker!.addEventListener("message", (e) => { this.worker!.addEventListener("message", (e) => {
const { type, data } = e.data; const { type, data } = e.data;
@@ -158,7 +159,10 @@ export class VectorWorkerManager {
} }
}); });
this.worker!.postMessage({ type: "init" }); this.worker!.postMessage({
type: "init",
data: { ortWasmBase: getOrtWasmBaseUrl() },
});
}); });
} }
@@ -388,7 +392,7 @@ export class VectorWorkerManager {
onProgress?: ProgressCallback, onProgress?: ProgressCallback,
batchSize: number = 10, batchSize: number = 10,
jobId?: string, jobId?: string,
): Promise<void> { ): Promise<boolean> {
// Skip if vector search is not supported // Skip if vector search is not supported
if (!isVectorSearchSupported()) { if (!isVectorSearchSupported()) {
verboseDebug("[VectorWorker] Vector search not supported - skipping streaming session"); verboseDebug("[VectorWorker] Vector search not supported - skipping streaming session");
@@ -398,13 +402,13 @@ export class VectorWorkerManager {
message: "Vector search not available - using text search only", message: "Vector search not available - using text search only",
}); });
} }
return; return false;
} }
// Only initialize if we expect items to process // Only initialize if we expect items to process
if (totalExpectedItems === 0) { if (totalExpectedItems === 0) {
verboseDebug("[VectorWorker] No items expected, not starting streaming session"); verboseDebug("[VectorWorker] No items expected, not starting streaming session");
return; return false;
} }
await this.ensureReady(); await this.ensureReady();
@@ -419,7 +423,7 @@ export class VectorWorkerManager {
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} else { } else {
verboseDebug(`Streaming session for job ${jobId} already active`); verboseDebug(`Streaming session for job ${jobId} already active`);
return; return true;
} }
} }
@@ -455,13 +459,20 @@ export class VectorWorkerManager {
message: `Starting streaming vectorization for ${jobId}`, message: `Starting streaming vectorization for ${jobId}`,
}); });
} }
return true;
} }
async streamItems(items: IndexItem[]): Promise<void> { async streamItems(items: IndexItem[]): Promise<void> {
if (!isVectorSearchSupported()) {
return;
}
if (!this.streamingSession?.isActive) { if (!this.streamingSession?.isActive) {
throw new Error( verboseDebug(
"No active streaming session. Call startStreamingSession first.", "[VectorWorker] streamItems skipped — no active streaming session",
); );
return;
} }
const uniqueItems = items.filter((item, index, arr) => { const uniqueItems = items.filter((item, index, arr) => {
@@ -0,0 +1,13 @@
/** Worker-safe logging — no webextension-polyfill or SettingsState. */
export function verboseDebug(...args: unknown[]): void {
if (typeof console !== "undefined") console.debug(...args);
}
export function verboseInfo(...args: unknown[]): void {
if (typeof console !== "undefined") console.info(...args);
}
export function verboseLog(...args: unknown[]): void {
if (typeof console !== "undefined") console.log(...args);
}
@@ -2,6 +2,7 @@ import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { IndexItem } from "../../indexing/types"; import type { IndexItem } from "../../indexing/types";
import type { SearchResult } from "embeddia"; import type { SearchResult } from "embeddia";
import { isVectorSearchSupported } from "../../utils/browserDetection"; import { isVectorSearchSupported } from "../../utils/browserDetection";
import { ensureTransformersEnv } from "@/lib/transformersExtension";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog'; import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
let vectorIndex: EmbeddingIndex | null = null; let vectorIndex: EmbeddingIndex | null = null;
@@ -24,6 +25,7 @@ export async function initVectorSearch() {
initializationAttempted = true; initializationAttempted = true;
try { try {
await ensureTransformersEnv();
await initializeModel(); await initializeModel();
vectorIndex = new EmbeddingIndex([]); vectorIndex = new EmbeddingIndex([]);
vectorIndex.preloadIndexedDB(); vectorIndex.preloadIndexedDB();
+9 -3
View File
@@ -49,10 +49,16 @@ export function createLazyPlugin<T extends PluginSettings = PluginSettings, S =
// Execute the actual plugin's run function // Execute the actual plugin's run function
return await actualPlugin.run(api); return await actualPlugin.run(api);
} catch (error: any) { } catch (error: any) {
// Handle Firefox MIME type errors gracefully const msg = error?.message ?? "";
if (error?.message?.includes("MIME type") || error?.message?.includes("NS_ERROR_CORRUPTED_CONTENT")) { // Handle content-script asset loading failures gracefully (Firefox MIME
// errors, CSS preload blocked on host page, corrupted chunks).
if (
msg.includes("MIME type") ||
msg.includes("NS_ERROR_CORRUPTED_CONTENT") ||
msg.includes("preload CSS")
) {
console.error( console.error(
`[BetterSEQTA+] Failed to load plugin "${lazyPlugin.id}" due to Firefox module loading restrictions. ` + `[BetterSEQTA+] Failed to load plugin "${lazyPlugin.id}" due to module/asset loading restrictions. ` +
`This may be a build configuration issue. Error:`, `This may be a build configuration issue. Error:`,
error error
); );
+2 -1
View File
@@ -37,6 +37,7 @@ import { observeMenuItemPosition } from "@/seqta/utils/sidebarMenuIcons";
// Icons and fonts // Icons and fonts
import IconFamily from "@/resources/fonts/IconFamily.woff"; import IconFamily from "@/resources/fonts/IconFamily.woff";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
// Stylesheets // Stylesheets
import iframeCSS from "@/css/iframe.scss?raw"; import iframeCSS from "@/css/iframe.scss?raw";
@@ -818,7 +819,7 @@ function InjectCustomIcons() {
style.innerHTML = ` style.innerHTML = `
@font-face { @font-face {
font-family: 'IconFamily'; font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff'); src: url('${resolveExtensionAssetUrl(IconFamily)}') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
}`; }`;
@@ -1,6 +1,7 @@
import { animate } from "motion"; import { animate } from "motion";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import LogoLight from "@/resources/icons/betterseqta-light-icon.png"; import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"; import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat"; import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
import debounce from "@/seqta/utils/debounce"; import debounce from "@/seqta/utils/debounce";
@@ -133,7 +134,7 @@ function renderEngageDayLessons(): void {
if (lessons.length === 0) { if (lessons.length === 0) {
dayContainer.innerHTML = ` dayContainer.innerHTML = `
<div class="day-empty"> <div class="day-empty">
<img src="${browser.runtime.getURL(LogoLight)}" alt="" /> <img src="${resolveExtensionAssetUrl(LogoLight)}" alt="" />
<p>No lessons for this day.</p> <p>No lessons for this day.</p>
</div>`; </div>`;
} else { } else {
@@ -310,7 +311,7 @@ function appendEngageNoticeEmptyState(container: HTMLElement, message: string) {
const emptyState = document.createElement("div"); const emptyState = document.createElement("div");
emptyState.classList.add("day-empty"); emptyState.classList.add("day-empty");
const img = document.createElement("img"); const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight); img.src = resolveExtensionAssetUrl(LogoLight);
const text = document.createElement("p"); const text = document.createElement("p");
text.innerText = message; text.innerText = message;
emptyState.append(img, text); emptyState.append(img, text);
@@ -717,7 +718,7 @@ function showEngageTimetableError(message: string): void {
dayContainer.classList.remove("loading"); dayContainer.classList.remove("loading");
dayContainer.innerHTML = ` dayContainer.innerHTML = `
<div class="day-empty"> <div class="day-empty">
<img src="${browser.runtime.getURL(LogoLight)}" alt="" /> <img src="${resolveExtensionAssetUrl(LogoLight)}" alt="" />
<p>${message}</p> <p>${message}</p>
</div>`; </div>`;
} }
@@ -728,7 +729,7 @@ function showEngageNoticesSectionError(message: string): void {
noticeContainer.classList.remove("loading"); noticeContainer.classList.remove("loading");
noticeContainer.innerHTML = ` noticeContainer.innerHTML = `
<div class="day-empty"> <div class="day-empty">
<img src="${browser.runtime.getURL(LogoLight)}" alt="" /> <img src="${resolveExtensionAssetUrl(LogoLight)}" alt="" />
<p>${message}</p> <p>${message}</p>
</div>`; </div>`;
} }
+4 -3
View File
@@ -1,6 +1,7 @@
import { animate, stagger } from "motion"; import { animate, stagger } from "motion";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import LogoLight from "@/resources/icons/betterseqta-light-icon.png"; import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import assessmentsicon from "@/seqta/icons/assessmentsIcon"; import assessmentsicon from "@/seqta/icons/assessmentsIcon";
import coursesicon from "@/seqta/icons/coursesIcon"; import coursesicon from "@/seqta/icons/coursesIcon";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"; import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
@@ -385,7 +386,7 @@ function appendNoticeEmptyState(container: HTMLElement, message: string) {
const emptyState = document.createElement("div"); const emptyState = document.createElement("div");
emptyState.classList.add("day-empty"); emptyState.classList.add("day-empty");
const img = document.createElement("img"); const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight); img.src = resolveExtensionAssetUrl(LogoLight);
const text = document.createElement("p"); const text = document.createElement("p");
text.innerText = message; text.innerText = message;
emptyState.append(img, text); emptyState.append(img, text);
@@ -786,7 +787,7 @@ function callHomeTimetable(date: string, change?: any) {
const dummyDay = document.createElement("div"); const dummyDay = document.createElement("div");
dummyDay.classList.add("day-empty"); dummyDay.classList.add("day-empty");
const img = document.createElement("img"); const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight); img.src = resolveExtensionAssetUrl(LogoLight);
const text = document.createElement("p"); const text = document.createElement("p");
text.innerText = "No lessons available."; text.innerText = "No lessons available.";
dummyDay.append(img, text); dummyDay.append(img, text);
@@ -1096,7 +1097,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
if (assessments.length === 0) { if (assessments.length === 0) {
upcomingitemcontainer!.innerHTML = ` upcomingitemcontainer!.innerHTML = `
<div class="day-empty"> <div class="day-empty">
<img src="${browser.runtime.getURL(LogoLight)}" /> <img src="${resolveExtensionAssetUrl(LogoLight)}" />
<p>No assessments available.</p> <p>No assessments available.</p>
</div>`; </div>`;
} }
+3 -2
View File
@@ -4,6 +4,7 @@ import { delay } from "./delay";
import { settingsState } from "./listeners/SettingsState"; import { settingsState } from "./listeners/SettingsState";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import LogoLightOutline from "@/resources/icons/betterseqta-light-outline.png"; import LogoLightOutline from "@/resources/icons/betterseqta-light-outline.png";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { animate, stagger } from "motion"; import { animate, stagger } from "motion";
import { verboseInfo } from "@/utils/verboseLog"; import { verboseInfo } from "@/utils/verboseLog";
@@ -65,7 +66,7 @@ export async function SendNewsPage() {
const emptyState = document.createElement("div"); const emptyState = document.createElement("div");
emptyState.classList.add("day-empty"); emptyState.classList.add("day-empty");
const img = document.createElement("img"); const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLightOutline); img.src = resolveExtensionAssetUrl(LogoLightOutline);
const text = document.createElement("p"); const text = document.createElement("p");
text.innerText = "No news articles available right now."; text.innerText = "No news articles available right now.";
emptyState.append(img, text); emptyState.append(img, text);
@@ -86,7 +87,7 @@ export async function SendNewsPage() {
if (article.urlToImage == "null" || article.urlToImage == null) { if (article.urlToImage == "null" || article.urlToImage == null) {
articleimage.style.cssText = ` articleimage.style.cssText = `
background-image: url(${browser.runtime.getURL(LogoLightOutline)}); background-image: url(${resolveExtensionAssetUrl(LogoLightOutline)});
width: 20%; width: 20%;
margin: 0 7.5%; margin: 0 7.5%;
`; `;