diff options
| author | Dhravya Shah <[email protected]> | 2025-09-10 19:13:40 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2025-09-10 19:13:40 -0700 |
| commit | a17655460f77f533bfbd4b15fa4a4ff9fe443008 (patch) | |
| tree | e0f5f02d885a16509b32814e95849208611ce597 /apps/browser-extension/utils | |
| parent | make docs public (diff) | |
| parent | feat (extension) : Auto Search Toggle for Chat Applications (#418) (diff) | |
| download | supermemory-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.ts | 9 | ||||
| -rw-r--r-- | apps/browser-extension/utils/memory-popup.ts | 93 | ||||
| -rw-r--r-- | apps/browser-extension/utils/route-detection.ts | 117 | ||||
| -rw-r--r-- | apps/browser-extension/utils/types.ts | 3 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 9 |
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;" />` |