aboutsummaryrefslogtreecommitdiff
path: root/apps/browser-extension/utils
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-09-10 19:13:40 -0700
committerDhravya Shah <[email protected]>2025-09-10 19:13:40 -0700
commita17655460f77f533bfbd4b15fa4a4ff9fe443008 (patch)
treee0f5f02d885a16509b32814e95849208611ce597 /apps/browser-extension/utils
parentmake docs public (diff)
parentfeat (extension) : Auto Search Toggle for Chat Applications (#418) (diff)
downloadsupermemory-a17655460f77f533bfbd4b15fa4a4ff9fe443008.tar.xz
supermemory-a17655460f77f533bfbd4b15fa4a4ff9fe443008.zip
Merge branch 'main' of https://github.com/supermemoryai/supermemory
Diffstat (limited to 'apps/browser-extension/utils')
-rw-r--r--apps/browser-extension/utils/constants.ts9
-rw-r--r--apps/browser-extension/utils/memory-popup.ts93
-rw-r--r--apps/browser-extension/utils/route-detection.ts117
-rw-r--r--apps/browser-extension/utils/types.ts3
-rw-r--r--apps/browser-extension/utils/ui-components.ts9
5 files changed, 224 insertions, 7 deletions
diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts
index 5ebd76d1..ac286717 100644
--- a/apps/browser-extension/utils/constants.ts
+++ b/apps/browser-extension/utils/constants.ts
@@ -21,6 +21,7 @@ export const STORAGE_KEYS = {
TWITTER_CSRF: "twitter-csrf",
TWITTER_AUTH_TOKEN: "twitter-auth-token",
DEFAULT_PROJECT: "sm-default-project",
+ AUTO_SEARCH_ENABLED: "sm-auto-search-enabled",
} as const
/**
@@ -44,6 +45,10 @@ export const UI_CONFIG = {
TOAST_DURATION: 3000, // milliseconds
RATE_LIMIT_BASE_WAIT: 60000, // 1 minute
PAGINATION_DELAY: 1000, // 1 second between requests
+ AUTO_SEARCH_DEBOUNCE_DELAY: 1500, // milliseconds to wait after user stops typing
+ OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions
+ ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks
+ API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout
} as const
/**
@@ -75,6 +80,7 @@ export const MESSAGE_TYPES = {
IMPORT_UPDATE: "sm-import-update",
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
+ CAPTURE_PROMPT: "sm-capture-prompt",
} as const
export const CONTEXT_MENU_IDS = {
@@ -87,6 +93,9 @@ export const POSTHOG_EVENT_KEY = {
SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed",
SOURCE: "extension",
T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched",
+ T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched",
CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched",
+ CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
+ CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
} as const
diff --git a/apps/browser-extension/utils/memory-popup.ts b/apps/browser-extension/utils/memory-popup.ts
new file mode 100644
index 00000000..ba2d2a1c
--- /dev/null
+++ b/apps/browser-extension/utils/memory-popup.ts
@@ -0,0 +1,93 @@
+/**
+ * Memory Popup Utilities
+ * Standardized popup positioning and styling for memory display across platforms
+ */
+
+export interface MemoryPopupConfig {
+ memoriesData: string
+ onClose: () => void
+ onRemove?: () => void
+}
+
+export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
+ const popup = document.createElement("div")
+ popup.style.cssText = `
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #1a1a1a;
+ color: white;
+ padding: 0;
+ border-radius: 12px;
+ font-size: 13px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ max-width: 500px;
+ max-height: 400px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ z-index: 999999;
+ display: none;
+ overflow: hidden;
+ `
+
+ const header = document.createElement("div")
+ header.style.cssText = `
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px;
+ border-bottom: 1px solid #333;
+ opacity: 0.8;
+ `
+ header.innerHTML = `
+ <span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span>
+ <div style="display: flex; gap: 4px;">
+ ${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""}
+ <button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;">✕</button>
+ </div>
+ `
+
+ const content = document.createElement("div")
+ content.style.cssText = `
+ padding: 8px;
+ max-height: 300px;
+ overflow-y: auto;
+ line-height: 1.4;
+ `
+ content.textContent = config.memoriesData
+
+ const closeBtn = header.querySelector("#close-popup-btn")
+ closeBtn?.addEventListener("click", config.onClose)
+
+ const removeBtn = header.querySelector("#remove-memories-btn")
+ if (removeBtn && config.onRemove) {
+ removeBtn.addEventListener("click", config.onRemove)
+ }
+
+ popup.appendChild(header)
+ popup.appendChild(content)
+
+ return popup
+}
+
+export function showMemoryPopup(popup: HTMLElement): void {
+ popup.style.display = "block"
+
+ setTimeout(() => {
+ if (popup.style.display === "block") {
+ hideMemoryPopup(popup)
+ }
+ }, 10000)
+}
+
+export function hideMemoryPopup(popup: HTMLElement): void {
+ popup.style.display = "none"
+}
+
+export function toggleMemoryPopup(popup: HTMLElement): void {
+ if (popup.style.display === "none" || popup.style.display === "") {
+ showMemoryPopup(popup)
+ } else {
+ hideMemoryPopup(popup)
+ }
+}
diff --git a/apps/browser-extension/utils/route-detection.ts b/apps/browser-extension/utils/route-detection.ts
new file mode 100644
index 00000000..a8a4714f
--- /dev/null
+++ b/apps/browser-extension/utils/route-detection.ts
@@ -0,0 +1,117 @@
+/**
+ * Route Detection Utilities
+ * Shared logic for detecting route changes across different AI chat platforms
+ */
+
+import { UI_CONFIG } from "./constants"
+
+export interface RouteDetectionConfig {
+ platform: string
+ selectors: string[]
+ reinitCallback: () => void
+ checkInterval?: number
+ observerThrottleDelay?: number
+}
+
+export interface RouteDetectionCleanup {
+ observer: MutationObserver | null
+ urlCheckInterval: NodeJS.Timeout | null
+ observerThrottle: NodeJS.Timeout | null
+}
+
+export function createRouteDetection(
+ config: RouteDetectionConfig,
+ cleanup: RouteDetectionCleanup,
+): void {
+ if (cleanup.observer) {
+ cleanup.observer.disconnect()
+ }
+ if (cleanup.urlCheckInterval) {
+ clearInterval(cleanup.urlCheckInterval)
+ }
+ if (cleanup.observerThrottle) {
+ clearTimeout(cleanup.observerThrottle)
+ cleanup.observerThrottle = null
+ }
+
+ let currentUrl = window.location.href
+
+ const checkForRouteChange = () => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href
+ console.log(`${config.platform} route changed, re-initializing`)
+ setTimeout(config.reinitCallback, 1000)
+ }
+ }
+
+ cleanup.urlCheckInterval = setInterval(
+ checkForRouteChange,
+ config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
+ )
+
+ cleanup.observer = new MutationObserver((mutations) => {
+ if (cleanup.observerThrottle) {
+ return
+ }
+
+ let shouldRecheck = false
+ mutations.forEach((mutation) => {
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element
+
+ for (const selector of config.selectors) {
+ if (
+ element.querySelector?.(selector) ||
+ element.matches?.(selector)
+ ) {
+ shouldRecheck = true
+ break
+ }
+ }
+ }
+ })
+ }
+ })
+
+ if (shouldRecheck) {
+ cleanup.observerThrottle = setTimeout(() => {
+ try {
+ cleanup.observerThrottle = null
+ config.reinitCallback()
+ } catch (error) {
+ console.error(`Error in ${config.platform} observer callback:`, error)
+ }
+ }, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
+ }
+ })
+
+ try {
+ cleanup.observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ })
+ } catch (error) {
+ console.error(`Failed to set up ${config.platform} route observer:`, error)
+ if (cleanup.urlCheckInterval) {
+ clearInterval(cleanup.urlCheckInterval)
+ }
+ cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
+ }
+}
+
+export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
+ if (cleanup.observer) {
+ cleanup.observer.disconnect()
+ cleanup.observer = null
+ }
+ if (cleanup.urlCheckInterval) {
+ clearInterval(cleanup.urlCheckInterval)
+ cleanup.urlCheckInterval = null
+ }
+ if (cleanup.observerThrottle) {
+ clearTimeout(cleanup.observerThrottle)
+ cleanup.observerThrottle = null
+ }
+}
diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts
index d20f899e..06a4ae72 100644
--- a/apps/browser-extension/utils/types.ts
+++ b/apps/browser-extension/utils/types.ts
@@ -24,7 +24,8 @@ export interface ExtensionMessage {
* Memory data structure for saving content
*/
export interface MemoryData {
- html: string
+ html?: string
+ content?: string
highlightedText?: string
url?: string
}
diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts
index 29388656..8a56ea5a 100644
--- a/apps/browser-extension/utils/ui-components.ts
+++ b/apps/browser-extension/utils/ui-components.ts
@@ -242,7 +242,7 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 24px;
+ width: auto;
height: 24px;
cursor: pointer;
transition: opacity 0.2s ease;
@@ -284,13 +284,12 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 32px;
+ width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
- border: 1px solid rgba(0, 0, 0, 0.1);
`
const iconFileName = "/icon-16.png"
@@ -329,13 +328,12 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 32px;
+ width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
- border: 1px solid rgba(0, 0, 0, 0.1);
`
const iconFileName = "/icon-16.png"
@@ -421,7 +419,6 @@ export const DOMUtils = {
const text = existingToast.querySelector("span")
if (icon && text) {
- // Update based on new state
if (state === "success") {
const iconUrl = browser.runtime.getURL("/icon-16.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`