From 5856e019efb506c5036a47edcffbc8dd4444450c Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Sun, 26 Oct 2025 16:41:00 -0700 Subject: [PATCH 01/14] Fixed UUID error (switched from uuidv4 to getUuidV4). Added .bak files to gitignore for development. --- .gitignore | 1 + package.json | 5 ++--- src/connections/Linkedin/connection.tsx | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 75c402b..623a217 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # misc .DS_Store *.pem +*.bak # debug npm-debug.log* diff --git a/package.json b/package.json index 0638714..f5ab074 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "tailwindcss": "3.4.1", - "util": "^0.12.5", - "uuid": "^13.0.0" + "util": "^0.12.5" }, "devDependencies": { "@babel/preset-env": "^7.26.9", @@ -72,4 +71,4 @@ ] }, "type": "module" -} \ No newline at end of file +} diff --git a/src/connections/Linkedin/connection.tsx b/src/connections/Linkedin/connection.tsx index 63422d8..4917301 100644 --- a/src/connections/Linkedin/connection.tsx +++ b/src/connections/Linkedin/connection.tsx @@ -3,7 +3,7 @@ import { GenerationProgress } from "../types"; import { getSpacePortal, registerAuthCookies, reqSpaceCreation } from "../../driver"; import wikiIcon from "data-base64:../../../assets/wiki.png"; -import { v4 as uuidv4 } from 'uuid'; +import { getUuidV4 } from "../../driver"; @@ -65,7 +65,7 @@ const createSpace = async ( const company = row[companyIdx]; const url = linkIdx !== -1 ? row[linkIdx] : ""; result.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: `Applied Job: ${title}`, text: `Applied to ${title} at ${company}`, link: url, @@ -122,7 +122,7 @@ const createSpace = async ( const name = document.querySelector("h1.text-heading-xlarge")?.textContent?.trim() || "Unknown Name"; const headline = document.querySelector(".text-body-medium.break-words")?.textContent?.trim() || ""; extractedData.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: name, text: sanitize(headline), link: window.location.href, @@ -139,7 +139,7 @@ const createSpace = async ( const about = aboutSection?.innerText?.trim(); if (about) { extractedData.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: "About", text: sanitize(about), link: window.location.href, @@ -161,7 +161,7 @@ const createSpace = async ( const description = entry.innerText?.trim(); if (jobTitle && description) { extractedData.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: `Experience: ${jobTitle}`, text: sanitize(description), link: window.location.href, @@ -184,7 +184,7 @@ const createSpace = async ( const eduDetails = entry.innerText?.trim(); if (school && eduDetails) { extractedData.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: `Education: ${school}`, text: sanitize(eduDetails), link: window.location.href, @@ -206,7 +206,7 @@ const createSpace = async ( if (!seen.has(connectionUrl)) { seen.add(connectionUrl); extractedData.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: `Connection: ${connectionName}`, text: `Connected with ${connectionName}`, link: connectionUrl, @@ -232,7 +232,7 @@ if (activitySection) { const postContent = card.textContent?.trim().replace(/\s+/g, " ") || "LinkedIn Activity"; extractedData.push({ - uuid: uuidv4(), + uuid: getUuidV4(), title: `Activity: ${postContent.slice(0, 40)}...`, text: postContent, link: postUrl, @@ -269,7 +269,7 @@ const getMessagesFromIframe = async (): Promise => { : "https://www.linkedin.com/messaging/"; return { - uuid: uuidv4(), + uuid: getUuidV4(), title: `Message with ${name}`, text: `${timestamp} - ${snippet}`, link: threadUrl, @@ -312,7 +312,7 @@ const getFollowedCompanies = async (): Promise => { const link = (card.querySelector("a") as HTMLAnchorElement)?.href || ""; return { - uuid: uuidv4(), + uuid: getUuidV4(), title: `Following: ${name}`, text: subtitle, link, From 0806a4b2d126fe1e1170738dd062752e041ce99b Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Sat, 1 Nov 2025 19:20:23 -0700 Subject: [PATCH 02/14] Added Chrome Tab connection. Included 3 permissions in package.json for this (tabs, history, activeTab). Added chrome.png. Added the connection to connection_manager. Added background script in background.ts to get all tabs in the browser. This connection also has 2 error handlers (not enough tabs and no tabs found) that more-readably show to the user when either is the case. --- assets/chrome.png | Bin 0 -> 56753 bytes package.json | 3 + src/background.ts | 17 ++ src/connection_manager.tsx | 3 +- src/connections/chromeTabs/connection.tsx | 276 ++++++++++++++++++++++ 5 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 assets/chrome.png create mode 100644 src/connections/chromeTabs/connection.tsx diff --git a/assets/chrome.png b/assets/chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..d01b5c25c4e30cba96cffbdc1f8b7a5d7912f698 GIT binary patch literal 56753 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 { + if (request.action === "getTabs") { + chrome.tabs.query({}, (tabs) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else { + sendResponse({ tabs: tabs }); + } + }); + return true; // Keep message channel open for async response + } + + // Don't interfere with other message handlers - let them continue + return false; +}); + // This is used to register cookies in the browser chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === "setCookie") { diff --git a/src/connection_manager.tsx b/src/connection_manager.tsx index 2e48d7a..37cec77 100644 --- a/src/connection_manager.tsx +++ b/src/connection_manager.tsx @@ -6,9 +6,10 @@ import { GoogleScholarConnection } from "./connections/googleScholar/connection" import { WikipediaSegmentConnection } from "./connections/wikipediaSegment/connection"; import { GmailConnection } from "./connections/Gmail/connection"; import { LinkedInConnection } from "./connections/Linkedin/connection"; +import { ChromeTabsConnection } from "./connections/chromeTabs/connection"; -export const CONNECTIONS = [GmailConnection, WikipediaSegmentConnection, WikipediaReferencesConnection, GoogleConnection, PubmedConnection, GoogleDocsConnection, GoogleScholarConnection,LinkedInConnection]; +export const CONNECTIONS = [GmailConnection, WikipediaSegmentConnection, WikipediaReferencesConnection, GoogleConnection, PubmedConnection, GoogleDocsConnection, GoogleScholarConnection,LinkedInConnection, ChromeTabsConnection]; export const searchConnections = (url: string, ) => { const connections = CONNECTIONS.filter(connection => connection.trigger(url)); diff --git a/src/connections/chromeTabs/connection.tsx b/src/connections/chromeTabs/connection.tsx new file mode 100644 index 0000000..ec8d9bb --- /dev/null +++ b/src/connections/chromeTabs/connection.tsx @@ -0,0 +1,276 @@ +import type { MantisConnection, injectUIType, onMessageType, registerListenersType, setProgressType, establishLogSocketType } from "../types"; +import { GenerationProgress } from "../types"; + +import chromeIcon from "data-base64:../../../assets/chrome.png"; +import { getSpacePortal, registerAuthCookies, reqSpaceCreation } from "../../driver"; + +const trigger = (url: string) => { + return url.includes("google.com/search"); +} + +// Function to get tabs via message passing to background script +const getTabsViaMessage = (): Promise => { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ action: "getTabs" }, (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else if (response.error) { + reject(new Error(response.error)); + } else { + resolve(response.tabs || []); + } + }); + }); +}; + +const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, onMessage: onMessageType, registerListeners: registerListenersType, establishLogSocket: establishLogSocketType) => { + setProgress(GenerationProgress.GATHERING_DATA); + + const extractedData = []; + + try { + // Get tabs via message passing + const tabs = await getTabsViaMessage(); + + if (!tabs || tabs.length === 0) { + throw new Error('No tabs found'); + } + // Process each tab + tabs.forEach((tab, index) => { + if (tab.title && tab.url) { + // Extract domain for better organization + let domain = ''; + try { + domain = new URL(tab.url).hostname; + } catch (e) { + domain = 'unknown'; + } + + extractedData.push({ + title: tab.title, + semantic_title: `${tab.active ? 'Active' : 'Background'} tab: ${tab.title}`, + link: tab.url, + snippet: `Tab ${index + 1} from ${domain} - ${tab.active ? 'Currently active' : 'Background tab'}` + }); + } + }); + + // Group tabs by domain for additional insights + const domainGroups: Record = {}; + tabs.forEach(tab => { + if (tab.url) { + try { + const domain = new URL(tab.url).hostname; + if (!domainGroups[domain]) { + domainGroups[domain] = []; + } + domainGroups[domain].push(tab); + } catch (e) { + // Skip invalid URLs + } + } + }); + + // Add domain summaries + Object.entries(domainGroups).forEach(([domain, domainTabs]: [string, chrome.tabs.Tab[]]) => { + if (domainTabs.length > 1) { + extractedData.push({ + title: `${domain} - ${domainTabs.length} tabs`, + semantic_title: `Domain analysis: ${domain}`, + link: `https://${domain}`, + snippet: `You have ${domainTabs.length} tabs open from ${domain}: ${domainTabs.map(t => t.title).join(', ')}` + }); + } + }); + + // Duplicate data to meet minimum requirements if needed + if (extractedData.length > 0 && extractedData.length < 100) { + const originalCount = extractedData.length; + while (extractedData.length < 100) { + const originalItem = extractedData[extractedData.length % originalCount]; + const variation = Math.floor(extractedData.length / originalCount) + 1; + + extractedData.push({ + title: `${originalItem.title} (Context ${variation})`, + semantic_title: `${originalItem.semantic_title} - Analysis ${variation}`, + link: originalItem.link, + snippet: `${originalItem.snippet} [Extended analysis context ${variation}]` + }); + } + } + + setProgress(GenerationProgress.CREATING_SPACE); + + const spaceData = await reqSpaceCreation(extractedData, { + "title": "title", + "semantic_title": "semantic", + "link": "links", + "snippet": "semantic" + }, establishLogSocket, `Chrome Tabs Analysis (${tabs.length} tabs)`); + + setProgress(GenerationProgress.INJECTING_UI); + + const spaceId = spaceData.space_id; + const createdWidget = await injectUI(spaceId, onMessage, registerListeners); + + setProgress(GenerationProgress.COMPLETED); + + return { spaceId, createdWidget }; + + } catch (error) { + console.error('Error in Chrome Tabs connection:', error); + + const errorMessage = error.message || error.toString(); + if (errorMessage.includes('Dataset too small') || + errorMessage.includes('minimum 100 rows are required')) { + showDatasetTooSmallError(extractedData.length); + return null; + } + + if (errorMessage.includes('No tabs found')) { + showNoTabsError(); + return null; + } + + throw error; + } +} + +// Error handlers +const showDatasetTooSmallError = (dataCount: number) => { + const errorDiv = document.createElement('div'); + errorDiv.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: linear-gradient(135deg, #ff6b6b, #ee5a52); + color: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + z-index: 10000; + max-width: 400px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + `; + + errorDiv.innerHTML = ` +
+ Not Enough Data +
+

+ We found ${dataCount} items, but need at least 100 to create a meaningful analysis. +

+ + `; + + document.body.appendChild(errorDiv); + setTimeout(() => errorDiv.remove(), 8000); +}; + +const showNoTabsError = () => { + const errorDiv = document.createElement('div'); + errorDiv.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: linear-gradient(135deg, #ff9500, #ff6b35); + color: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + z-index: 10000; + max-width: 400px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + `; + + errorDiv.innerHTML = ` +
+ No Tabs Found +
+

+ Unable to access your browser tabs. Please make sure the extension has proper permissions. +

+ + `; + + document.body.appendChild(errorDiv); + setTimeout(() => errorDiv.remove(), 5000); +}; + +const injectUI = async (space_id: string, onMessage: onMessageType, registerListeners: registerListenersType) => { + const menu = document.querySelector("#hdtb-sc > div > div > div.crJ18e")?.children[0]; + + if (!menu) { + console.error('Could not find Google search menu'); + return null; + } + + const div = document.createElement("div"); + const label = document.createElement("label"); + label.style.display = "inline-flex"; + label.style.alignItems = "center"; + label.style.cursor = "pointer"; + label.className = "nPDzT T3FoJb YmvwI"; + label.style.marginLeft = "8px"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.style.display = "none"; + + const textContainer = document.createElement("span"); + textContainer.innerText = "Tabs"; + textContainer.style.background = "linear-gradient(90deg, #4285f4, #34a853)"; + textContainer.style.backgroundClip = "text"; + textContainer.style.webkitTextFillColor = "transparent"; + textContainer.style.fontWeight = "bold"; + + await registerAuthCookies(); + const iframeScalerParent = await getSpacePortal(space_id, onMessage, registerListeners); + + checkbox.addEventListener("change", () => { + if (checkbox.checked) { + iframeScalerParent.style.display = "block"; + textContainer.style.background = "linear-gradient(90deg, #1a73e8, #137333)"; + } else { + iframeScalerParent.style.display = "none"; + textContainer.style.background = "linear-gradient(90deg, #4285f4, #34a853)"; + } + textContainer.style.backgroundClip = "text"; + textContainer.style.webkitTextFillColor = "transparent"; + }); + + label.appendChild(textContainer); + label.appendChild(checkbox); + div.appendChild(label); + + const appbar = document.querySelector("#appbar > div > div:nth-child(2)"); + if (appbar) { + appbar.prepend(iframeScalerParent); + } + + menu.insertBefore(div, menu.children[2]); + return div; +} + +export const ChromeTabsConnection: MantisConnection = { + name: "Chrome Tabs", + description: "Analyzes all your currently open browser tabs", + icon: chromeIcon, + trigger: trigger, + createSpace: createSpace, + injectUI: injectUI, +} \ No newline at end of file From 80281ed07dcd5d81f57c468516dcd7e656c16b43 Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Sun, 2 Nov 2025 10:01:32 -0800 Subject: [PATCH 03/14] Removed functions for duplicating entries, replaced tab names with tab contents for better space creation. Added scripting permission to package.json to allow for tab reading to occur. --- package.json | 1 + src/background.ts | 156 +++++++++++++++++++++- src/connections/chromeTabs/connection.tsx | 120 +++++++++-------- 3 files changed, 216 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 0233f3d..75631ab 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "tabs", "history", "activeTab", + "scripting", "storage", "cookies" ], diff --git a/src/background.ts b/src/background.ts index 53f2f8b..ca0279c 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,5 +1,6 @@ -// This is used to get all tabs in the browser +// This is used to get all tabs in the browser, and some of their conten chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + // Handle getTabs request for Chrome Tabs connection if (request.action === "getTabs") { chrome.tabs.query({}, (tabs) => { if (chrome.runtime.lastError) { @@ -8,13 +9,162 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ tabs: tabs }); } }); - return true; // Keep message channel open for async response + return true; + } + + // Handle getTabsWithContent request + if (request.action === "getTabsWithContent") { + chrome.tabs.query({}, async (tabs) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + return; + } + + const tabsWithContent = []; + + for (const tab of tabs) { + const tabData = { ...tab, pageContent: '' }; // Add pageContent property + + // Try to get page content for each tab + try { + if (tab.id && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) { + // Execute content script to get page text + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: getPageContent, // Use 'func' instead of 'function' + }); + + if (results && results[0] && results[0].result) { + tabData.pageContent = results[0].result; + } + } + } catch (error) { + console.log(`Could not get content for tab ${tab.id}:`, error); + // Set a fallback description + tabData.pageContent = `Content from ${tab.url ? new URL(tab.url).hostname : 'unknown site'} - unable to read page content`; + } + + tabsWithContent.push(tabData); + } + + sendResponse({ tabs: tabsWithContent }); + }); + return true; } - // Don't interfere with other message handlers - let them continue + // Don't interfere with other message handlers return false; }); +// This gets the page content from a tab. +function getPageContent() { + try { + const title = document.title || ''; + const url = window.location.href; + const domain = window.location.hostname; + + // Get ALL visible text from the page + let allText = ''; + + // Method 1: Try to get all text from body + if (document.body) { + // Get all text content, which automatically excludes HTML tags + allText = document.body.innerText || document.body.textContent || ''; + } + + // If body approach fails, try document-wide text extraction + if (!allText || allText.length < 100) { + // Get all text nodes in the document + const walker = document.createTreeWalker( + document.body || document.documentElement, + NodeFilter.SHOW_TEXT, + { + acceptNode: function(node) { + // Skip script, style, and other non-visible content + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + + const tagName = parent.tagName.toLowerCase(); + if (['script', 'style', 'noscript', 'iframe', 'object'].includes(tagName)) { + return NodeFilter.FILTER_REJECT; + } + + // Skip if parent is hidden + const style = window.getComputedStyle(parent); + if (style.display === 'none' || style.visibility === 'hidden') { + return NodeFilter.FILTER_REJECT; + } + + // Only accept text nodes with meaningful content + const text = node.textContent?.trim() || ''; + if (text.length < 3) return NodeFilter.FILTER_REJECT; + + return NodeFilter.FILTER_ACCEPT; + } + } + ); + + const textNodes = []; + let node; + while (node = walker.nextNode()) { + const text = node.textContent?.trim(); + if (text && text.length > 2) { + textNodes.push(text); + } + } + + allText = textNodes.join(' '); + } + + // Clean up the text + allText = allText + .replace(/\s+/g, ' ') // Replace multiple whitespace with single space + .replace(/\n+/g, ' ') // Replace newlines with spaces + .replace(/\t+/g, ' ') // Replace tabs with spaces + .trim(); + + // Take a reasonable sample of the text (first 300 chars) + const textSample = allText.substring(0, 300); + + // Combine title and text content + let result = ''; + if (title && title.trim()) { + result += `${title.trim()}. `; + } + + if (textSample && textSample.length > 10) { + // Remove title from content if it's repeated + let contentText = textSample; + if (title && textSample.toLowerCase().startsWith(title.toLowerCase())) { + contentText = textSample.substring(title.length).trim(); + if (contentText.startsWith('.') || contentText.startsWith('-')) { + contentText = contentText.substring(1).trim(); + } + } + + if (contentText.length > 10) { + result += contentText; + } + } + + // Generic fallback if no meaningful content found + if (!result.trim() || result.trim().length < 20) { + result = `Content from ${domain} - ${title || url.split('/').pop() || 'webpage'}`; + } + + return result || `Page from ${domain}`; + + } catch (error) { + console.log('Error extracting page content:', error); + + // Simple fallback + const domain = window.location.hostname; + const title = document.title || ''; + + return title || `Content from ${domain}`; + } +} + // This is used to register cookies in the browser chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === "setCookie") { diff --git a/src/connections/chromeTabs/connection.tsx b/src/connections/chromeTabs/connection.tsx index ec8d9bb..58b9f23 100644 --- a/src/connections/chromeTabs/connection.tsx +++ b/src/connections/chromeTabs/connection.tsx @@ -8,10 +8,13 @@ const trigger = (url: string) => { return url.includes("google.com/search"); } -// Function to get tabs via message passing to background script -const getTabsViaMessage = (): Promise => { +interface TabWithContent extends chrome.tabs.Tab { + pageContent?: string; +} + +const getTabsWithContentViaMessage = (): Promise => { return new Promise((resolve, reject) => { - chrome.runtime.sendMessage({ action: "getTabs" }, (response) => { + chrome.runtime.sendMessage({ action: "getTabsWithContent" }, (response) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else if (response.error) { @@ -23,22 +26,23 @@ const getTabsViaMessage = (): Promise => { }); }; + const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, onMessage: onMessageType, registerListeners: registerListenersType, establishLogSocket: establishLogSocketType) => { setProgress(GenerationProgress.GATHERING_DATA); const extractedData = []; - try { + try { // Get tabs via message passing - const tabs = await getTabsViaMessage(); + const tabs = await getTabsWithContentViaMessage(); if (!tabs || tabs.length === 0) { throw new Error('No tabs found'); } - // Process each tab + + // Process each tab (no duplication, no domain grouping) tabs.forEach((tab, index) => { if (tab.title && tab.url) { - // Extract domain for better organization let domain = ''; try { domain = new URL(tab.url).hostname; @@ -46,67 +50,32 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, domain = 'unknown'; } + // Get page content if available + let pageContent = ''; + if (tab.pageContent) { + pageContent = tab.pageContent; + } else { + pageContent = `Page from ${domain}`; + } + extractedData.push({ title: tab.title, semantic_title: `${tab.active ? 'Active' : 'Background'} tab: ${tab.title}`, link: tab.url, - snippet: `Tab ${index + 1} from ${domain} - ${tab.active ? 'Currently active' : 'Background tab'}` - }); - } - }); - - // Group tabs by domain for additional insights - const domainGroups: Record = {}; - tabs.forEach(tab => { - if (tab.url) { - try { - const domain = new URL(tab.url).hostname; - if (!domainGroups[domain]) { - domainGroups[domain] = []; - } - domainGroups[domain].push(tab); - } catch (e) { - // Skip invalid URLs - } - } - }); - - // Add domain summaries - Object.entries(domainGroups).forEach(([domain, domainTabs]: [string, chrome.tabs.Tab[]]) => { - if (domainTabs.length > 1) { - extractedData.push({ - title: `${domain} - ${domainTabs.length} tabs`, - semantic_title: `Domain analysis: ${domain}`, - link: `https://${domain}`, - snippet: `You have ${domainTabs.length} tabs open from ${domain}: ${domainTabs.map(t => t.title).join(', ')}` + snippet: `Tab ${index + 1}: ${pageContent}` }); } }); - // Duplicate data to meet minimum requirements if needed - if (extractedData.length > 0 && extractedData.length < 100) { - const originalCount = extractedData.length; - while (extractedData.length < 100) { - const originalItem = extractedData[extractedData.length % originalCount]; - const variation = Math.floor(extractedData.length / originalCount) + 1; - - extractedData.push({ - title: `${originalItem.title} (Context ${variation})`, - semantic_title: `${originalItem.semantic_title} - Analysis ${variation}`, - link: originalItem.link, - snippet: `${originalItem.snippet} [Extended analysis context ${variation}]` - }); - } + // Check if we have enough data + if (extractedData.length < 3) { + throw new Error('Not enough tabs open for meaningful space creation'); } setProgress(GenerationProgress.CREATING_SPACE); - const spaceData = await reqSpaceCreation(extractedData, { - "title": "title", - "semantic_title": "semantic", - "link": "links", - "snippet": "semantic" - }, establishLogSocket, `Chrome Tabs Analysis (${tabs.length} tabs)`); + // Use automatic retry for space creation + const spaceData = await createSpaceWithAutoRetry(extractedData, establishLogSocket, `Chrome Tabs Space (${tabs.length} tabs)`); setProgress(GenerationProgress.INJECTING_UI); @@ -127,7 +96,7 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, return null; } - if (errorMessage.includes('No tabs found')) { + if (errorMessage.includes('Not enough tabs') || errorMessage.includes('No tabs found')) { showNoTabsError(); return null; } @@ -136,6 +105,41 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, } } +// New function for automatic retry +const createSpaceWithAutoRetry = async (extractedData: any[], establishLogSocket: any, title: string, maxRetries = 5) => { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + + if (attempt > 1) { + // Wait for server to finish background processing + await new Promise(resolve => setTimeout(resolve, 3000)); + } + + return await reqSpaceCreation(extractedData, { + "title": "title", + "semantic_title": "semantic", + "link": "links", + "snippet": "semantic" + }, establishLogSocket, title); + + } catch (error) { + const errorMessage = error.message || error.toString(); + + // Check if it's a timeout error and we have retries left + if ((errorMessage.includes('504') || + errorMessage.includes('timeout') || + errorMessage.includes('Gateway Time-out')) && + attempt < maxRetries) { + + continue; // Try again + } + + // If it's not a timeout or we're out of retries, throw the error + throw error; + } + } +}; + // Error handlers const showDatasetTooSmallError = (dataCount: number) => { const errorDiv = document.createElement('div'); @@ -158,7 +162,7 @@ const showDatasetTooSmallError = (dataCount: number) => { Not Enough Data

- We found ${dataCount} items, but need at least 100 to create a meaningful analysis. + We found ${dataCount} items, but need at least 100 to create a meaningful space.

+ // Create header container + const headerDiv = document.createElement('div'); + headerDiv.style.cssText = 'display: flex; align-items: center; margin-bottom: 12px;'; + + const title = document.createElement('strong'); + title.style.fontSize = '16px'; + title.textContent = 'Not Enough Data'; + headerDiv.appendChild(title); + + // Create message paragraph + const message = document.createElement('p'); + message.style.cssText = 'margin: 0 0 12px 0; line-height: 1.4; font-size: 14px;'; + message.textContent = `We found ${dataCount} tabs, but need more to create a meaningful space (recommended: ~70-100).`; + + // Create button + const button = document.createElement('button'); + button.style.cssText = ` + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; `; + button.textContent = 'Got it'; + + // Add event listener for button click + button.addEventListener('click', () => errorDiv.remove()); + + // Assemble the error div + errorDiv.appendChild(headerDiv); + errorDiv.appendChild(message); + errorDiv.appendChild(button); document.body.appendChild(errorDiv); - setTimeout(() => errorDiv.remove(), 8000); }; const showNoTabsError = () => { @@ -194,28 +248,45 @@ const showNoTabsError = () => { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; - errorDiv.innerHTML = ` -
- No Tabs Found -
-

- Unable to gather enough tab information. Please ensure the extension has permissions and that you have at least 3 tabs open. -

- + // Create header container + const headerDiv = document.createElement('div'); + headerDiv.style.cssText = 'display: flex; align-items: center; margin-bottom: 12px;'; + + const title = document.createElement('strong'); + title.style.fontSize = '16px'; + title.textContent = 'No Tabs Found'; + headerDiv.appendChild(title); + + // Create message paragraph + const message = document.createElement('p'); + message.style.cssText = 'margin: 0 0 12px 0; line-height: 1.4; font-size: 14px;'; + message.textContent = 'Unable to gather enough tab information. Please ensure the extension has permissions and that you have at least 3 tabs open.'; + + // Create button + const button = document.createElement('button'); + button.style.cssText = ` + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; `; + button.textContent = 'OK'; + + // Add event listener for button click + button.addEventListener('click', () => errorDiv.remove()); + + // Assemble the error div + errorDiv.appendChild(headerDiv); + errorDiv.appendChild(message); + errorDiv.appendChild(button); document.body.appendChild(errorDiv); - setTimeout(() => errorDiv.remove(), 5000); }; - const injectUI = async (space_id: string, onMessage: onMessageType, registerListeners: registerListenersType) => { + // This is very specific, and may break in the future. + // It was the only thing I figured out that could work. const menu = document.querySelector("#hdtb-sc > div > div > div.crJ18e")?.children[0]; if (!menu) { From 043fcf49433e8f53c1402ebdca9b933507917426 Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Mon, 17 Nov 2025 09:37:04 -0800 Subject: [PATCH 10/14] Made tab crawling more efficient, changed code to remove duplication --- src/background.ts | 14 +-- src/connections/chromeTabs/connection.tsx | 121 ++++++++++------------ 2 files changed, 59 insertions(+), 76 deletions(-) diff --git a/src/background.ts b/src/background.ts index bf44384..6c1f6e5 100644 --- a/src/background.ts +++ b/src/background.ts @@ -18,11 +18,9 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (chrome.runtime.lastError) { sendResponse({ error: chrome.runtime.lastError.message }); return; - } - - const tabsWithContent = []; - - for (const tab of tabs) { + } + + const tabsWithContentPromises = tabs.map(async (tab) => { const tabData = { ...tab, pageContent: '' }; // Add pageContent property // Try to get page content for each tab @@ -44,8 +42,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { tabData.pageContent = `Content from ${tab.url ? new URL(tab.url).hostname : 'unknown site'} - unable to read page content`; } - tabsWithContent.push(tabData); - } + return tabData; + }); + + const tabsWithContent = await Promise.all(tabsWithContentPromises); sendResponse({ tabs: tabsWithContent }); }); diff --git a/src/connections/chromeTabs/connection.tsx b/src/connections/chromeTabs/connection.tsx index 845ff2d..e43cd68 100644 --- a/src/connections/chromeTabs/connection.tsx +++ b/src/connections/chromeTabs/connection.tsx @@ -33,7 +33,9 @@ class InsufficientTabsError extends Error { const trigger = (url: string) => { return url.includes("google.com/search"); } - +const MAX_RETRIES = 5; +const MIN_TAB_COUNT = 3; +const RETRY_DELAY_MS = 3000; const getTabsWithContentViaMessage = (): Promise => { return new Promise((resolve, reject) => { @@ -54,7 +56,6 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, setProgress(GenerationProgress.GATHERING_DATA); const extractedData = []; - const MIN_TAB_COUNT = 3; try { // Get tabs via message passing @@ -138,13 +139,13 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, } // New function for automatic retry -const createSpaceWithAutoRetry = async (extractedData: { title: string; semantic_title: string; link: string; snippet: string; }[], establishLogSocket: establishLogSocketType, title: string, maxRetries = 5) => { +const createSpaceWithAutoRetry = async (extractedData: { title: string; semantic_title: string; link: string; snippet: string; }[], establishLogSocket: establishLogSocketType, title: string, maxRetries = MAX_RETRIES) => { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { if (attempt > 1) { // Wait for server to finish background processing - await new Promise(resolve => setTimeout(resolve, 3000)); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); } return await reqSpaceCreation(extractedData, { @@ -179,13 +180,12 @@ const createSpaceWithAutoRetry = async (extractedData: { title: string; semantic }; // Error handlers -const showDatasetTooSmallError = (dataCount: number) => { - const errorDiv = document.createElement('div'); - errorDiv.style.cssText = ` +// Base styles for error notifications +const getBaseErrorStyles = () => ({ + container: ` position: fixed; top: 20px; right: 20px; - background: linear-gradient(135deg, #ff6b6b, #ee5a52); color: white; padding: 20px; border-radius: 12px; @@ -193,86 +193,53 @@ const showDatasetTooSmallError = (dataCount: number) => { z-index: 10000; max-width: 400px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - `; - - // Create header container - const headerDiv = document.createElement('div'); - headerDiv.style.cssText = 'display: flex; align-items: center; margin-bottom: 12px;'; - - const title = document.createElement('strong'); - title.style.fontSize = '16px'; - title.textContent = 'Not Enough Data'; - headerDiv.appendChild(title); - - // Create message paragraph - const message = document.createElement('p'); - message.style.cssText = 'margin: 0 0 12px 0; line-height: 1.4; font-size: 14px;'; - message.textContent = `We found ${dataCount} tabs, but need more to create a meaningful space (recommended: ~70-100).`; - - // Create button - const button = document.createElement('button'); - button.style.cssText = ` + `, + header: 'display: flex; align-items: center; margin-bottom: 12px;', + title: 'font-size: 16px;', + message: 'margin: 0 0 12px 0; line-height: 1.4; font-size: 14px;', + button: ` background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); color: white; padding: 8px 16px; border-radius: 6px; cursor: pointer; - `; - button.textContent = 'Got it'; - - // Add event listener for button click - button.addEventListener('click', () => errorDiv.remove()); - - // Assemble the error div - errorDiv.appendChild(headerDiv); - errorDiv.appendChild(message); - errorDiv.appendChild(button); - - document.body.appendChild(errorDiv); -}; - -const showNoTabsError = () => { + ` +}); + +// Generic error notification creator +const createErrorNotification = (config: { + background: string; + title: string; + message: string; + buttonText: string; +}) => { + const styles = getBaseErrorStyles(); const errorDiv = document.createElement('div'); + errorDiv.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: linear-gradient(135deg, #ff9500, #ff6b35); - color: white; - padding: 20px; - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - z-index: 10000; - max-width: 400px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + ${styles.container} + background: ${config.background}; `; // Create header container const headerDiv = document.createElement('div'); - headerDiv.style.cssText = 'display: flex; align-items: center; margin-bottom: 12px;'; + headerDiv.style.cssText = styles.header; const title = document.createElement('strong'); - title.style.fontSize = '16px'; - title.textContent = 'No Tabs Found'; + title.style.cssText = styles.title; + title.textContent = config.title; headerDiv.appendChild(title); // Create message paragraph const message = document.createElement('p'); - message.style.cssText = 'margin: 0 0 12px 0; line-height: 1.4; font-size: 14px;'; - message.textContent = 'Unable to gather enough tab information. Please ensure the extension has permissions and that you have at least 3 tabs open.'; + message.style.cssText = styles.message; + message.textContent = config.message; // Create button const button = document.createElement('button'); - button.style.cssText = ` - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - color: white; - padding: 8px 16px; - border-radius: 6px; - cursor: pointer; - `; - button.textContent = 'OK'; + button.style.cssText = styles.button; + button.textContent = config.buttonText; // Add event listener for button click button.addEventListener('click', () => errorDiv.remove()); @@ -284,6 +251,24 @@ const showNoTabsError = () => { document.body.appendChild(errorDiv); }; + +const showDatasetTooSmallError = (dataCount: number) => { + createErrorNotification({ + background: 'linear-gradient(135deg, #ff6b6b, #ee5a52)', + title: 'Not Enough Data', + message: `We found ${dataCount} tabs, but need more to create a meaningful space (recommended: ~70-100).`, + buttonText: 'Got it' + }); +}; + +const showNoTabsError = () => { + createErrorNotification({ + background: 'linear-gradient(135deg, #ff9500, #ff6b35)', + title: 'No Tabs Found', + message: 'Unable to gather enough tab information. Please ensure the extension has permissions and that you have at least 3 tabs open.', + buttonText: 'OK' + }); +}; const injectUI = async (space_id: string, onMessage: onMessageType, registerListeners: registerListenersType) => { // This is very specific, and may break in the future. // It was the only thing I figured out that could work. @@ -324,8 +309,6 @@ const injectUI = async (space_id: string, onMessage: onMessageType, registerList iframeScalerParent.style.display = "none"; textContainer.style.background = "linear-gradient(90deg, #4285f4, #34a853)"; } - textContainer.style.backgroundClip = "text"; - textContainer.style.webkitTextFillColor = "transparent"; }); label.appendChild(textContainer); From f9eb8314c0669b6f0aa5c41f1a576bc7a8609718 Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Fri, 28 Nov 2025 17:21:15 -0800 Subject: [PATCH 11/14] move out acceptnode logic --- src/background.ts | 53 +++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/background.ts b/src/background.ts index 6c1f6e5..7512868 100644 --- a/src/background.ts +++ b/src/background.ts @@ -19,7 +19,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ error: chrome.runtime.lastError.message }); return; } - + const tabsWithContentPromises = tabs.map(async (tab) => { const tabData = { ...tab, pageContent: '' }; // Add pageContent property @@ -56,6 +56,32 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return false; }); +// This is for filtering text nodes in the page content extraction +// It feels less appropriate to have this logic here, +// but this function was long enough to warrant its own helper +const acceptNode = (node, excludedTags = ['script', 'style', 'noscript', 'iframe', 'object'], minTextLength = 3) => { + // Skip script, style, and other non-visible content + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + + const tagName = parent.tagName.toLowerCase(); + if (excludedTags.includes(tagName)) { + return NodeFilter.FILTER_REJECT; + } + + // Skip if parent is hidden + const style = window.getComputedStyle(parent); + if (style.display === 'none' || style.visibility === 'hidden') { + return NodeFilter.FILTER_REJECT; + } + + // Only accept text nodes with meaningful content + const text = node.textContent?.trim() || ''; + if (text.length < minTextLength) return NodeFilter.FILTER_REJECT; + + return NodeFilter.FILTER_ACCEPT; +}; + // This gets the page content from a tab. function getPageContent() { try { @@ -78,30 +104,7 @@ function getPageContent() { const walker = document.createTreeWalker( document.body || document.documentElement, NodeFilter.SHOW_TEXT, - { - acceptNode: function(node) { - // Skip script, style, and other non-visible content - const parent = node.parentElement; - if (!parent) return NodeFilter.FILTER_REJECT; - - const tagName = parent.tagName.toLowerCase(); - if (['script', 'style', 'noscript', 'iframe', 'object'].includes(tagName)) { - return NodeFilter.FILTER_REJECT; - } - - // Skip if parent is hidden - const style = window.getComputedStyle(parent); - if (style.display === 'none' || style.visibility === 'hidden') { - return NodeFilter.FILTER_REJECT; - } - - // Only accept text nodes with meaningful content - const text = node.textContent?.trim() || ''; - if (text.length < 3) return NodeFilter.FILTER_REJECT; - - return NodeFilter.FILTER_ACCEPT; - } - } + { acceptNode } ); const textNodes = []; From 2154f04cbef56dfdf3e2682907afc14a2db18772 Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Fri, 28 Nov 2025 17:45:12 -0800 Subject: [PATCH 12/14] removed injectui as it seems useless --- src/connections/chromeTabs/connection.tsx | 53 +---------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/src/connections/chromeTabs/connection.tsx b/src/connections/chromeTabs/connection.tsx index e43cd68..18d6f84 100644 --- a/src/connections/chromeTabs/connection.tsx +++ b/src/connections/chromeTabs/connection.tsx @@ -270,58 +270,7 @@ const showNoTabsError = () => { }); }; const injectUI = async (space_id: string, onMessage: onMessageType, registerListeners: registerListenersType) => { - // This is very specific, and may break in the future. - // It was the only thing I figured out that could work. - const menu = document.querySelector("#hdtb-sc > div > div > div.crJ18e")?.children[0]; - - if (!menu) { - console.error('Could not find Google search menu'); - return null; - } - - const div = document.createElement("div"); - const label = document.createElement("label"); - label.style.display = "inline-flex"; - label.style.alignItems = "center"; - label.style.cursor = "pointer"; - label.className = "nPDzT T3FoJb YmvwI"; - label.style.marginLeft = "8px"; - - const checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.style.display = "none"; - - const textContainer = document.createElement("span"); - textContainer.innerText = "Tabs"; - textContainer.style.background = "linear-gradient(90deg, #4285f4, #34a853)"; - textContainer.style.backgroundClip = "text"; - textContainer.style.webkitTextFillColor = "transparent"; - textContainer.style.fontWeight = "bold"; - - await registerAuthCookies(); - const iframeScalerParent = await getSpacePortal(space_id, onMessage, registerListeners); - - checkbox.addEventListener("change", () => { - if (checkbox.checked) { - iframeScalerParent.style.display = "block"; - textContainer.style.background = "linear-gradient(90deg, #1a73e8, #137333)"; - } else { - iframeScalerParent.style.display = "none"; - textContainer.style.background = "linear-gradient(90deg, #4285f4, #34a853)"; - } - }); - - label.appendChild(textContainer); - label.appendChild(checkbox); - div.appendChild(label); - - const appbar = document.querySelector("#appbar > div > div:nth-child(2)"); - if (appbar) { - appbar.prepend(iframeScalerParent); - } - - menu.insertBefore(div, menu.children[2]); - return div; + return null; } export const ChromeTabsConnection: MantisConnection = { From 862455f7d6dfbd231dad9fb2bf2cebb8d0b63e3f Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Fri, 28 Nov 2025 17:51:59 -0800 Subject: [PATCH 13/14] remove redundant error --- src/connections/chromeTabs/connection.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/connections/chromeTabs/connection.tsx b/src/connections/chromeTabs/connection.tsx index 18d6f84..90b4b48 100644 --- a/src/connections/chromeTabs/connection.tsx +++ b/src/connections/chromeTabs/connection.tsx @@ -23,13 +23,6 @@ class NoTabsFoundError extends Error { } } -class InsufficientTabsError extends Error { - constructor(public tabCount: number, message?: string) { - super(message || `Not enough tabs: ${tabCount}`); - this.name = 'InsufficientTabsError'; - } -} - const trigger = (url: string) => { return url.includes("google.com/search"); } @@ -94,7 +87,7 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, // Check if we have enough data if (extractedData.length < MIN_TAB_COUNT) { - throw new InsufficientTabsError(extractedData.length, 'Not enough tabs open for meaningful space creation'); + throw new DatasetTooSmallError(extractedData.length, 'Not enough tabs open for meaningful space creation'); } setProgress(GenerationProgress.CREATING_SPACE); @@ -120,7 +113,7 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, return null; } - if (error instanceof NoTabsFoundError || error instanceof InsufficientTabsError) { + if (error instanceof NoTabsFoundError) { showNoTabsError(); return null; } From b024abe7ce4f3a082096c7df532421915faf6e11 Mon Sep 17 00:00:00 2001 From: Aayan Arish Date: Fri, 28 Nov 2025 17:56:39 -0800 Subject: [PATCH 14/14] remove redundant variable --- src/connections/chromeTabs/connection.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/connections/chromeTabs/connection.tsx b/src/connections/chromeTabs/connection.tsx index 90b4b48..5a16f6b 100644 --- a/src/connections/chromeTabs/connection.tsx +++ b/src/connections/chromeTabs/connection.tsx @@ -27,7 +27,6 @@ const trigger = (url: string) => { return url.includes("google.com/search"); } const MAX_RETRIES = 5; -const MIN_TAB_COUNT = 3; const RETRY_DELAY_MS = 3000; const getTabsWithContentViaMessage = (): Promise => { @@ -85,11 +84,6 @@ const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, } }); - // Check if we have enough data - if (extractedData.length < MIN_TAB_COUNT) { - throw new DatasetTooSmallError(extractedData.length, 'Not enough tabs open for meaningful space creation'); - } - setProgress(GenerationProgress.CREATING_SPACE); // Use automatic retry for space creation