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 | |
| 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
| -rw-r--r-- | apps/browser-extension/entrypoints/background.ts | 41 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content.ts | 674 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/chatgpt.ts | 724 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/claude.ts | 648 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/index.ts | 67 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/shared.ts | 77 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/t3.ts | 692 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content/twitter.ts | 102 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/popup/App.tsx | 77 | ||||
| -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 | ||||
| -rw-r--r-- | apps/browser-extension/wxt.config.ts | 4 |
15 files changed, 2641 insertions, 696 deletions
diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 05fef55d..9d46a5e9 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -23,7 +23,7 @@ export default defineBackground(() => { browser.runtime.onInstalled.addListener(async (details) => { browser.contextMenus.create({ id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, - title: "Save to supermemory", + title: "sync to supermemory", contexts: ["selection", "page", "link"], }) @@ -114,7 +114,9 @@ export default defineBackground(() => { const payload: MemoryPayload = { containerTags: [containerTag], - content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`, + content: + data.content || + `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`, metadata: { sm_source: "consumer" }, } @@ -144,9 +146,9 @@ export default defineBackground(() => { const response = responseData as { results?: Array<{ memory?: string }> } - let memories = "" + const memories: string[] = [] response.results?.forEach((result, index) => { - memories += `[${index + 1}] ${result.memory} ` + memories.push(`${index + 1}. ${result.memory} \n`) }) console.log("Memories:", memories) await trackEvent(eventSource) @@ -216,6 +218,37 @@ export default defineBackground(() => { })() return true } + + if (message.action === MESSAGE_TYPES.CAPTURE_PROMPT) { + ;(async () => { + try { + const messageData = message.data as { + prompt: string + platform: string + source: string + } + console.log("=== PROMPT CAPTURED ===") + console.log(messageData) + console.log("========================") + + const memoryData: MemoryData = { + content: messageData.prompt, + } + + const result = await saveMemoryToSupermemory( + memoryData, + `prompt_capture_${messageData.platform}`, + ) + sendResponse(result) + } catch (error) { + sendResponse({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }) + } + })() + return true + } }, ) }) diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts deleted file mode 100644 index e755efa6..00000000 --- a/apps/browser-extension/entrypoints/content.ts +++ /dev/null @@ -1,674 +0,0 @@ -import { - DOMAINS, - ELEMENT_IDS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, - STORAGE_KEYS, -} from "../utils/constants" -import { trackEvent } from "../utils/posthog" -import { - createChatGPTInputBarElement, - createClaudeInputBarElement, - createT3InputBarElement, - createTwitterImportButton, - DOMUtils, -} from "../utils/ui-components" - -export default defineContentScript({ - matches: ["<all_urls>"], - main() { - - browser.runtime.onMessage.addListener(async (message) => { - if (message.action === MESSAGE_TYPES.SHOW_TOAST) { - DOMUtils.showToast(message.state) - } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - await saveMemory() - } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - updateTwitterImportUI(message) - } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - updateTwitterImportUI(message) - } - }) - - const observeForMemoriesDialog = () => { - const observer = new MutationObserver(() => { - if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - addSupermemoryButtonToMemoriesDialog() - addSaveChatGPTElementBeforeComposerBtn() - } - if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - addSupermemoryIconToClaudeInput() - } - if (DOMUtils.isOnDomain(DOMAINS.T3)) { - addSupermemoryIconToT3Input() - } - if ( - DOMUtils.isOnDomain(DOMAINS.TWITTER) && - window.location.pathname === "/i/bookmarks" - ) { - addTwitterImportButton() - } else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } - } - }) - - observer.observe(document.body, { - childList: true, - subtree: true, - }) - } - - if ( - DOMUtils.isOnDomain(DOMAINS.TWITTER) && - window.location.pathname === "/i/bookmarks" - ) { - setTimeout(() => { - addTwitterImportButton() // Wait 2 seconds for page to load - //addSaveTweetElement(); - }, 2000) - } else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } - } - - if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - setTimeout(() => { - addSupermemoryIconToClaudeInput() // Wait 2 seconds for Claude page to load - }, 2000) - } - - if (DOMUtils.isOnDomain(DOMAINS.T3)) { - setTimeout(() => { - addSupermemoryIconToT3Input() // Wait 2 seconds for T3 page to load - }, 2000) - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", observeForMemoriesDialog) - } else { - observeForMemoriesDialog() - } - - async function saveMemory() { - try { - DOMUtils.showToast("loading") - - const highlightedText = window.getSelection()?.toString() || "" - - const url = window.location.href - - const html = document.documentElement.outerHTML - - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.SAVE_MEMORY, - data: { - html, - highlightedText, - url, - }, - actionSource: "context_menu", - }) - - console.log("Response from enxtension:", response) - if (response.success) { - DOMUtils.showToast("success") - } else { - DOMUtils.showToast("error") - } - } catch (error) { - console.error("Error saving memory:", error) - DOMUtils.showToast("error") - } - } - - async function getRelatedMemories(actionSource: string) { - try { - const userQuery = - document.getElementById("prompt-textarea")?.textContent || "" - - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource: actionSource, - }) - - if (response.success && response.data) { - const promptElement = document.getElementById("prompt-textarea") - if (promptElement) { - const currentContent = promptElement.innerHTML - promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}` - } - } - } catch (error) { - console.error("Error getting related memories:", error) - } - } - - function addSupermemoryButtonToMemoriesDialog() { - const dialogs = document.querySelectorAll('[role="dialog"]') - let memoriesDialog: HTMLElement | null = null - - for (const dialog of dialogs) { - const headerText = dialog.querySelector("h2") - if (headerText?.textContent?.includes("Saved memories")) { - memoriesDialog = dialog as HTMLElement - break - } - } - - if (!memoriesDialog) return - - if (memoriesDialog.querySelector("#supermemory-save-button")) return - - const deleteAllContainer = memoriesDialog.querySelector( - ".mt-5.flex.justify-end", - ) - if (!deleteAllContainer) return - - const supermemoryButton = document.createElement("button") - supermemoryButton.id = "supermemory-save-button" - supermemoryButton.className = "btn relative btn-primary-outline mr-2" - - const iconUrl = browser.runtime.getURL("/icon-16.png") - - supermemoryButton.innerHTML = ` - <div class="flex items-center justify-center gap-2"> - <img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" /> - Save to supermemory - </div> - ` - - supermemoryButton.style.cssText = ` - background: #1C2026 !important; - color: white !important; - border: 1px solid #1C2026 !important; - border-radius: 9999px !important; - padding: 10px 16px !important; - font-weight: 500 !important; - font-size: 14px !important; - margin-right: 8px !important; - cursor: pointer !important; - ` - - supermemoryButton.addEventListener("mouseenter", () => { - supermemoryButton.style.backgroundColor = "#2B2E33" - }) - - supermemoryButton.addEventListener("mouseleave", () => { - supermemoryButton.style.backgroundColor = "#1C2026" - }) - - supermemoryButton.addEventListener("click", async () => { - await saveMemoriesToSupermemory() - }) - - deleteAllContainer.insertBefore( - supermemoryButton, - deleteAllContainer.firstChild, - ) - } - - async function saveMemoriesToSupermemory() { - try { - DOMUtils.showToast("loading") - - const memoriesTable = document.querySelector( - '[role="dialog"] table tbody', - ) - if (!memoriesTable) { - DOMUtils.showToast("error") - return - } - - const memoryRows = memoriesTable.querySelectorAll("tr") - const memories: string[] = [] - - memoryRows.forEach((row) => { - const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap") - if (memoryCell?.textContent) { - memories.push(memoryCell.textContent.trim()) - } - }) - - console.log("Memories:", memories) - - if (memories.length === 0) { - DOMUtils.showToast("error") - return - } - - const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}` - - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.SAVE_MEMORY, - data: { - html: combinedContent, - }, - actionSource: "chatgpt_memories_dialog", - }) - - console.log({ response }) - - if (response.success) { - DOMUtils.showToast("success") - } else { - DOMUtils.showToast("error") - } - } catch (error) { - console.error("Error saving memories to supermemory:", error) - DOMUtils.showToast("error") - } - } - - function addTwitterImportButton() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return - } - - // Only show the import button on the bookmarks page - if (window.location.pathname !== "/i/bookmarks") { - return - } - - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - return - } - - const button = createTwitterImportButton(async () => { - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - }) - await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, { - source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`, - }) - } catch (error) { - console.error("Error starting import:", error) - } - }) - - document.body.appendChild(button) - } - - - function updateTwitterImportUI(message: { - type: string - importedMessage?: string - totalImported?: number - }) { - const importButton = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - if (!importButton) return - - const iconUrl = browser.runtime.getURL("/icon-16.png") - - if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - importButton.innerHTML = ` - <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> - <span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span> - ` - importButton.style.cursor = "default" - } - - if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - importButton.innerHTML = ` - <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> - <span style="font-weight: 500; font-size: 14px; color: #059669;">✓ Imported ${message.totalImported} tweets!</span> - ` - - setTimeout(() => { - importButton.innerHTML = ` - <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> - <span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span> - ` - importButton.style.cursor = "pointer" - }, 3000) - } - } - - function addSaveChatGPTElementBeforeComposerBtn() { - if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - return - } - - const composerButtons = document.querySelectorAll("button.composer-btn") - - composerButtons.forEach((button) => { - if (button.hasAttribute("data-supermemory-icon-added-before")) { - return - } - - const parent = button.parentElement - if (!parent) return - - const parentSiblings = parent.parentElement?.children - if (!parentSiblings) return - - let hasSpeechButtonSibling = false - for (const sibling of parentSiblings) { - if ( - sibling.getAttribute("data-testid") === - "composer-speech-button-container" - ) { - hasSpeechButtonSibling = true - break - } - } - - if (!hasSpeechButtonSibling) return - - const grandParent = parent.parentElement - if (!grandParent) return - - const existingIcon = grandParent.querySelector( - `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`, - ) - if (existingIcon) { - button.setAttribute("data-supermemory-icon-added-before", "true") - return - } - - const saveChatGPTElement = createChatGPTInputBarElement(async () => { - await getRelatedMemories( - POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED, - ) - }) - - saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - button.setAttribute("data-supermemory-icon-added-before", "true") - - grandParent.insertBefore(saveChatGPTElement, parent) - }) - } - - function addSupermemoryIconToClaudeInput() { - if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - return - } - - const targetContainers = document.querySelectorAll( - ".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0", - ) - - targetContainers.forEach((container) => { - if (container.hasAttribute("data-supermemory-icon-added")) { - return - } - - const existingIcon = container.querySelector( - `#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`, - ) - if (existingIcon) { - container.setAttribute("data-supermemory-icon-added", "true") - return - } - - const supermemoryIcon = createClaudeInputBarElement(async () => { - await getRelatedMemoriesForClaude() - }) - - supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - container.setAttribute("data-supermemory-icon-added", "true") - - container.insertBefore(supermemoryIcon, container.firstChild) - }) - } - - async function getRelatedMemoriesForClaude() { - try { - let userQuery = "" - - const supermemoryContainer = document.querySelector( - '[data-supermemory-icon-added="true"]', - ) - if (supermemoryContainer?.parentElement?.previousElementSibling) { - const pTag = - supermemoryContainer.parentElement.previousElementSibling.querySelector( - "p", - ) - userQuery = pTag?.innerText || pTag?.textContent || "" - } - - if (!userQuery.trim()) { - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - userQuery = - textareaElement?.innerText || textareaElement?.textContent || "" - } - - if (!userQuery.trim()) { - const inputElements = document.querySelectorAll( - 'div[contenteditable="true"], textarea, input[type="text"]', - ) - for (const element of inputElements) { - const text = - (element as HTMLElement).innerText || - (element as HTMLInputElement).value - if (text?.trim()) { - userQuery = text.trim() - break - } - } - } - - console.log("Claude query extracted:", userQuery) - - if (!userQuery.trim()) { - console.log("No query text found") - DOMUtils.showToast("error") - return - } - - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource: POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED, - }) - - console.log("Claude memories response:", response) - - if (response.success && response.data) { - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - - if (textareaElement) { - const currentContent = textareaElement.innerHTML - textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}` - - textareaElement.dispatchEvent(new Event("input", { bubbles: true })) - } else { - console.log("Could not find Claude input area") - } - } else { - console.log( - "Failed to get memories:", - response.error || "Unknown error", - ) - } - } catch (error) { - console.error("Error getting related memories for Claude:", error) - } - } - - function addSupermemoryIconToT3Input() { - if (!DOMUtils.isOnDomain(DOMAINS.T3)) { - return - } - - const targetContainers = document.querySelectorAll( - ".flex.min-w-0.items-center.gap-2.overflow-hidden", - ) - - targetContainers.forEach((container) => { - if (container.hasAttribute("data-supermemory-icon-added")) { - return - } - - const existingIcon = container.querySelector( - `#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`, - ) - if (existingIcon) { - container.setAttribute("data-supermemory-icon-added", "true") - return - } - - const supermemoryIcon = createT3InputBarElement(async () => { - await getRelatedMemoriesForT3() - }) - - supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - container.setAttribute("data-supermemory-icon-added", "true") - - container.insertBefore(supermemoryIcon, container.firstChild) - }) - } - - async function getRelatedMemoriesForT3() { - try { - let userQuery = "" - - const supermemoryContainer = document.querySelector( - '[data-supermemory-icon-added="true"]', - ) - if ( - supermemoryContainer?.parentElement?.parentElement - ?.previousElementSibling - ) { - const textareaElement = - supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector( - "textarea", - ) - userQuery = textareaElement?.value || "" - } - - if (!userQuery.trim()) { - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - userQuery = - textareaElement?.innerText || textareaElement?.textContent || "" - } - - if (!userQuery.trim()) { - const textareas = document.querySelectorAll("textarea") - for (const textarea of textareas) { - const text = (textarea as HTMLTextAreaElement).value - if (text?.trim()) { - userQuery = text.trim() - break - } - } - } - - console.log("T3 query extracted:", userQuery) - - if (!userQuery.trim()) { - console.log("No query text found") - return - } - - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource: POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED, - }) - - console.log("T3 memories response:", response) - - if (response.success && response.data) { - let textareaElement = null - const supermemoryContainer = document.querySelector( - '[data-supermemory-icon-added="true"]', - ) - if ( - supermemoryContainer?.parentElement?.parentElement - ?.previousElementSibling - ) { - textareaElement = - supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector( - "textarea", - ) - } - - if (!textareaElement) { - textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - } - - if (textareaElement) { - if (textareaElement.tagName === "TEXTAREA") { - const currentContent = (textareaElement as HTMLTextAreaElement) - .value - ;(textareaElement as HTMLTextAreaElement).value = - `${currentContent}\n\nSupermemories: ${response.data}` - } else { - const currentContent = textareaElement.innerHTML - textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}` - } - - textareaElement.dispatchEvent(new Event("input", { bubbles: true })) - } else { - console.log("Could not find T3 input area") - } - } else { - console.log( - "Failed to get memories:", - response.error || "Unknown error", - ) - } - } catch (error) { - console.error("Error getting related memories for T3:", error) - } - } - - - document.addEventListener("keydown", async (event) => { - if ( - (event.ctrlKey || event.metaKey) && - event.shiftKey && - event.key === "m" - ) { - event.preventDefault() - await saveMemory() - } - }) - - window.addEventListener("message", (event) => { - if (event.source !== window) { - return - } - const bearerToken = event.data.token - const userData = event.data.userData - if (bearerToken && userData) { - if ( - !( - window.location.hostname === "localhost" || - window.location.hostname === "supermemory.ai" || - window.location.hostname === "app.supermemory.ai" - ) - ) { - console.log( - "Bearer token and user data is only allowed to be used on localhost or supermemory.ai", - ) - return - } - - chrome.storage.local.set( - { - [STORAGE_KEYS.BEARER_TOKEN]: bearerToken, - [STORAGE_KEYS.USER_DATA]: userData, - }, - () => {}, - ) - } - }) - }, -}) diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts new file mode 100644 index 00000000..73e0354f --- /dev/null +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -0,0 +1,724 @@ +import { + DOMAINS, + ELEMENT_IDS, + MESSAGE_TYPES, + POSTHOG_EVENT_KEY, + STORAGE_KEYS, + UI_CONFIG, +} from "../../utils/constants" +import { + createChatGPTInputBarElement, + DOMUtils, +} from "../../utils/ui-components" + +let chatGPTDebounceTimeout: NodeJS.Timeout | null = null +let chatGPTRouteObserver: MutationObserver | null = null +let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null +let chatGPTObserverThrottle: NodeJS.Timeout | null = null + +export function initializeChatGPT() { + if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { + return + } + + if (document.body.hasAttribute("data-chatgpt-initialized")) { + return + } + + setTimeout(() => { + addSupermemoryButtonToMemoriesDialog() + addSaveChatGPTElementBeforeComposerBtn() + setupChatGPTAutoFetch() + }, 2000) + + setupChatGPTPromptCapture() + + setupChatGPTRouteChangeDetection() + + document.body.setAttribute("data-chatgpt-initialized", "true") +} + +function setupChatGPTRouteChangeDetection() { + if (chatGPTRouteObserver) { + chatGPTRouteObserver.disconnect() + } + if (chatGPTUrlCheckInterval) { + clearInterval(chatGPTUrlCheckInterval) + } + if (chatGPTObserverThrottle) { + clearTimeout(chatGPTObserverThrottle) + chatGPTObserverThrottle = null + } + + let currentUrl = window.location.href + + const checkForRouteChange = () => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href + console.log("ChatGPT route changed, re-adding supermemory elements") + setTimeout(() => { + addSupermemoryButtonToMemoriesDialog() + addSaveChatGPTElementBeforeComposerBtn() + setupChatGPTAutoFetch() + }, 1000) + } + } + + chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000) + + chatGPTRouteObserver = new MutationObserver((mutations) => { + if (chatGPTObserverThrottle) { + 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 + if ( + element.querySelector?.("#prompt-textarea") || + element.querySelector?.("button.composer-btn") || + element.querySelector?.('[role="dialog"]') || + element.matches?.("#prompt-textarea") || + element.id === "prompt-textarea" + ) { + shouldRecheck = true + } + } + }) + } + }) + + if (shouldRecheck) { + chatGPTObserverThrottle = setTimeout(() => { + try { + chatGPTObserverThrottle = null + addSupermemoryButtonToMemoriesDialog() + addSaveChatGPTElementBeforeComposerBtn() + setupChatGPTAutoFetch() + } catch (error) { + console.error("Error in ChatGPT observer callback:", error) + } + }, 300) + } + }) + + try { + chatGPTRouteObserver.observe(document.body, { + childList: true, + subtree: true, + }) + } catch (error) { + console.error("Failed to set up ChatGPT route observer:", error) + if (chatGPTUrlCheckInterval) { + clearInterval(chatGPTUrlCheckInterval) + } + chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000) + } +} + +async function getRelatedMemoriesForChatGPT(actionSource: string) { + try { + const userQuery = + document.getElementById("prompt-textarea")?.textContent || "" + + const icon = document.querySelectorAll( + '[id*="sm-chatgpt-input-bar-element-before-composer"]', + )[0] + + const iconElement = icon as HTMLElement + + if (!iconElement) { + console.warn("ChatGPT icon element not found, cannot update feedback") + return + } + + updateChatGPTIconFeedback("Searching memories...", iconElement) + + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Memory search timeout")), + UI_CONFIG.API_REQUEST_TIMEOUT, + ), + ) + + const response = await Promise.race([ + browser.runtime.sendMessage({ + action: MESSAGE_TYPES.GET_RELATED_MEMORIES, + data: userQuery, + actionSource: actionSource, + }), + timeoutPromise, + ]) + + if (response?.success && response?.data) { + const promptElement = document.getElementById("prompt-textarea") + if (promptElement) { + promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>` + console.log( + "Prompt element dataset:", + promptElement.dataset.supermemories, + ) + + iconElement.dataset.memoriesData = response.data + + updateChatGPTIconFeedback("Included Memories", iconElement) + } else { + console.warn( + "ChatGPT prompt element not found after successful memory fetch", + ) + updateChatGPTIconFeedback("Memories found", iconElement) + } + } else { + console.warn("No memories found or API response invalid") + updateChatGPTIconFeedback("No memories found", iconElement) + } + } catch (error) { + console.error("Error getting related memories:", error) + try { + const icon = document.querySelectorAll( + '[id*="sm-chatgpt-input-bar-element-before-composer"]', + )[0] as HTMLElement + if (icon) { + updateChatGPTIconFeedback("Error fetching memories", icon) + } + } catch (feedbackError) { + console.error("Failed to update error feedback:", feedbackError) + } + } +} + +function addSupermemoryButtonToMemoriesDialog() { + const dialogs = document.querySelectorAll('[role="dialog"]') + let memoriesDialog: HTMLElement | null = null + + for (const dialog of dialogs) { + const headerText = dialog.querySelector("h2") + if (headerText?.textContent?.includes("Saved memories")) { + memoriesDialog = dialog as HTMLElement + break + } + } + + if (!memoriesDialog) return + + if (memoriesDialog.querySelector("#supermemory-save-button")) return + + const deleteAllContainer = memoriesDialog.querySelector( + ".mt-5.flex.justify-end", + ) + if (!deleteAllContainer) return + + const supermemoryButton = document.createElement("button") + supermemoryButton.id = "supermemory-save-button" + supermemoryButton.className = "btn relative btn-primary-outline mr-2" + + const iconUrl = browser.runtime.getURL("/icon-16.png") + + supermemoryButton.innerHTML = ` + <div class="flex items-center justify-center gap-2"> + <img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" /> + Save to supermemory + </div> + ` + + supermemoryButton.style.cssText = ` + background: #1C2026 !important; + color: white !important; + border: 1px solid #1C2026 !important; + border-radius: 9999px !important; + padding: 10px 16px !important; + font-weight: 500 !important; + font-size: 14px !important; + margin-right: 8px !important; + cursor: pointer !important; + ` + + supermemoryButton.addEventListener("mouseenter", () => { + supermemoryButton.style.backgroundColor = "#2B2E33" + }) + + supermemoryButton.addEventListener("mouseleave", () => { + supermemoryButton.style.backgroundColor = "#1C2026" + }) + + supermemoryButton.addEventListener("click", async () => { + await saveMemoriesToSupermemory() + }) + + deleteAllContainer.insertBefore( + supermemoryButton, + deleteAllContainer.firstChild, + ) +} + +async function saveMemoriesToSupermemory() { + try { + DOMUtils.showToast("loading") + + const memoriesTable = document.querySelector('[role="dialog"] table tbody') + if (!memoriesTable) { + DOMUtils.showToast("error") + return + } + + const memoryRows = memoriesTable.querySelectorAll("tr") + const memories: string[] = [] + + memoryRows.forEach((row) => { + const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap") + if (memoryCell?.textContent) { + memories.push(memoryCell.textContent.trim()) + } + }) + + console.log("Memories:", memories) + + if (memories.length === 0) { + DOMUtils.showToast("error") + return + } + + const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}` + + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.SAVE_MEMORY, + data: { + html: combinedContent, + }, + actionSource: "chatgpt_memories_dialog", + }) + + console.log({ response }) + + if (response.success) { + DOMUtils.showToast("success") + } else { + DOMUtils.showToast("error") + } + } catch (error) { + console.error("Error saving memories to supermemory:", error) + DOMUtils.showToast("error") + } +} + +function updateChatGPTIconFeedback( + message: string, + iconElement: HTMLElement, + resetAfter = 0, +) { + if (!iconElement.dataset.originalHtml) { + iconElement.dataset.originalHtml = iconElement.innerHTML + } + + const feedbackDiv = document.createElement("div") + feedbackDiv.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: #513EA9; + border-radius: 12px; + color: white; + font-size: 12px; + font-weight: 500; + cursor: ${message === "Included Memories" ? "pointer" : "default"}; + position: relative; + ` + + feedbackDiv.innerHTML = ` + <span>✓</span> + <span>${message}</span> + ` + + if (message === "Included Memories" && iconElement.dataset.memoriesData) { + 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; + border: 1px solid #333; + ` + + 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-weight: 600; color: #fff;">Included Memories</span> + ` + + const content = document.createElement("div") + content.style.cssText = ` + padding: 0; + max-height: 300px; + overflow-y: auto; + ` + + const memoriesText = iconElement.dataset.memoriesData || "" + console.log("Memories text:", memoriesText) + const individualMemories = memoriesText + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + console.log("Individual memories:", individualMemories) + + individualMemories.forEach((memory, index) => { + const memoryItem = document.createElement("div") + memoryItem.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + padding: 10px; + font-size: 13px; + line-height: 1.4; + ` + + const memoryText = document.createElement("div") + memoryText.style.cssText = ` + flex: 1; + color: #e5e5e5; + ` + memoryText.textContent = memory.trim() + + const removeBtn = document.createElement("button") + removeBtn.style.cssText = ` + background: transparent; + color: #9ca3af; + border: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + flex-shrink: 0; + height: fit-content; + display: flex; + align-items: center; + justify-content: center; + ` + removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>` + removeBtn.dataset.memoryIndex = index.toString() + + removeBtn.addEventListener("mouseenter", () => { + removeBtn.style.color = "#ef4444" + }) + removeBtn.addEventListener("mouseleave", () => { + removeBtn.style.color = "#9ca3af" + }) + + memoryItem.appendChild(memoryText) + memoryItem.appendChild(removeBtn) + content.appendChild(memoryItem) + }) + + popup.appendChild(header) + popup.appendChild(content) + document.body.appendChild(popup) + + feedbackDiv.addEventListener("mouseenter", () => { + const textSpan = feedbackDiv.querySelector("span:last-child") + if (textSpan) { + textSpan.textContent = "Click to see memories" + } + }) + + feedbackDiv.addEventListener("mouseleave", () => { + const textSpan = feedbackDiv.querySelector("span:last-child") + if (textSpan) { + textSpan.textContent = "Included Memories" + } + }) + + feedbackDiv.addEventListener("click", (e) => { + e.stopPropagation() + popup.style.display = "block" + }) + + document.addEventListener("click", (e) => { + if (!popup.contains(e.target as Node)) { + popup.style.display = "none" + } + }) + + content.querySelectorAll("button[data-memory-index]").forEach((button) => { + const htmlButton = button as HTMLButtonElement + htmlButton.addEventListener("click", () => { + const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) + const memoryItem = htmlButton.parentElement + + if (memoryItem) { + content.removeChild(memoryItem) + } + + const currentMemories = (iconElement.dataset.memoriesData || "") + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + currentMemories.splice(index, 1) + + const updatedMemories = currentMemories.join(" ,") + + iconElement.dataset.memoriesData = updatedMemories + + const promptElement = document.getElementById("prompt-textarea") + if (promptElement) { + promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>` + } + + content + .querySelectorAll("button[data-memory-index]") + .forEach((btn, newIndex) => { + const htmlBtn = btn as HTMLButtonElement + htmlBtn.dataset.memoryIndex = newIndex.toString() + }) + + if (currentMemories.length <= 1) { + if (promptElement?.dataset.supermemories) { + delete promptElement.dataset.supermemories + delete iconElement.dataset.memoriesData + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + } + popup.style.display = "none" + if (document.body.contains(popup)) { + document.body.removeChild(popup) + } + } + }) + }) + + setTimeout(() => { + if (document.body.contains(popup)) { + document.body.removeChild(popup) + } + }, 300000) + } + + iconElement.innerHTML = "" + iconElement.appendChild(feedbackDiv) + + if (resetAfter > 0) { + setTimeout(() => { + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + }, resetAfter) + } +} + +function addSaveChatGPTElementBeforeComposerBtn() { + const composerButtons = document.querySelectorAll("button.composer-btn") + + composerButtons.forEach((button) => { + if (button.hasAttribute("data-supermemory-icon-added-before")) { + return + } + + const parent = button.parentElement + if (!parent) return + + const parentSiblings = parent.parentElement?.children + if (!parentSiblings) return + + let hasSpeechButtonSibling = false + for (const sibling of parentSiblings) { + if ( + sibling.getAttribute("data-testid") === + "composer-speech-button-container" + ) { + hasSpeechButtonSibling = true + break + } + } + + if (!hasSpeechButtonSibling) return + + const grandParent = parent.parentElement + if (!grandParent) return + + const existingIcon = grandParent.querySelector( + `#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`, + ) + if (existingIcon) { + button.setAttribute("data-supermemory-icon-added-before", "true") + return + } + + const saveChatGPTElement = createChatGPTInputBarElement(async () => { + await getRelatedMemoriesForChatGPT( + POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED, + ) + }) + + saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + + button.setAttribute("data-supermemory-icon-added-before", "true") + + grandParent.insertBefore(saveChatGPTElement, parent) + + setupChatGPTAutoFetch() + }) +} + +async function setupChatGPTAutoFetch() { + const result = await chrome.storage.local.get([ + STORAGE_KEYS.AUTO_SEARCH_ENABLED, + ]) + const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false + + if (!autoSearchEnabled) { + return + } + + const promptTextarea = document.getElementById("prompt-textarea") + if ( + !promptTextarea || + promptTextarea.hasAttribute("data-supermemory-auto-fetch") + ) { + return + } + + promptTextarea.setAttribute("data-supermemory-auto-fetch", "true") + + const handleInput = () => { + if (chatGPTDebounceTimeout) { + clearTimeout(chatGPTDebounceTimeout) + } + + chatGPTDebounceTimeout = setTimeout(async () => { + const content = promptTextarea.textContent?.trim() || "" + + if (content.length > 2) { + await getRelatedMemoriesForChatGPT( + POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED, + ) + } else if (content.length === 0) { + const icons = document.querySelectorAll( + '[id*="sm-chatgpt-input-bar-element-before-composer"]', + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + delete iconElement.dataset.memoriesData + } + }) + + if (promptTextarea.dataset.supermemories) { + delete promptTextarea.dataset.supermemories + } + } + }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) + } + + promptTextarea.addEventListener("input", handleInput) +} + +function setupChatGPTPromptCapture() { + if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) { + return + } + document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true") + + const capturePromptContent = async (source: string) => { + const promptTextarea = document.getElementById("prompt-textarea") + + let promptContent = "" + if (promptTextarea) { + promptContent = promptTextarea.textContent || "" + } + + const storedMemories = promptTextarea?.dataset.supermemories + if ( + storedMemories && + promptTextarea && + !promptContent.includes("Supermemories of user") + ) { + promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}` + promptContent = promptTextarea.textContent || "" + } + + if (promptTextarea && promptContent.trim()) { + console.log(`ChatGPT prompt submitted via ${source}:`, promptContent) + + try { + await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.CAPTURE_PROMPT, + data: { + prompt: promptContent, + platform: "chatgpt", + source: source, + }, + }) + } catch (error) { + console.error("Error sending ChatGPT prompt to background:", error) + } + } + + const icons = document.querySelectorAll( + '[id*="sm-chatgpt-input-bar-element-before-composer"]', + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + delete iconElement.dataset.memoriesData + } + }) + + if (promptTextarea?.dataset.supermemories) { + delete promptTextarea.dataset.supermemories + } + } + + document.addEventListener( + "click", + async (event) => { + const target = event.target as HTMLElement + if ( + target.id === "composer-submit-button" || + target.closest("#composer-submit-button") + ) { + await capturePromptContent("button click") + } + }, + true, + ) + + document.addEventListener( + "keydown", + async (event) => { + const target = event.target as HTMLElement + + if ( + target.id === "prompt-textarea" && + event.key === "Enter" && + !event.shiftKey + ) { + await capturePromptContent("Enter key") + } + }, + true, + ) +} diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts new file mode 100644 index 00000000..e0853d41 --- /dev/null +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -0,0 +1,648 @@ +import { + DOMAINS, + ELEMENT_IDS, + MESSAGE_TYPES, + POSTHOG_EVENT_KEY, + STORAGE_KEYS, + UI_CONFIG, +} from "../../utils/constants" +import { + createClaudeInputBarElement, + DOMUtils, +} from "../../utils/ui-components" + +let claudeDebounceTimeout: NodeJS.Timeout | null = null +let claudeRouteObserver: MutationObserver | null = null +let claudeUrlCheckInterval: NodeJS.Timeout | null = null +let claudeObserverThrottle: NodeJS.Timeout | null = null + +export function initializeClaude() { + if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { + return + } + + if (document.body.hasAttribute("data-claude-initialized")) { + return + } + + setTimeout(() => { + addSupermemoryIconToClaudeInput() + setupClaudeAutoFetch() + }, 2000) + + setupClaudePromptCapture() + + setupClaudeRouteChangeDetection() + + document.body.setAttribute("data-claude-initialized", "true") +} + +function setupClaudeRouteChangeDetection() { + if (claudeRouteObserver) { + claudeRouteObserver.disconnect() + } + if (claudeUrlCheckInterval) { + clearInterval(claudeUrlCheckInterval) + } + if (claudeObserverThrottle) { + clearTimeout(claudeObserverThrottle) + claudeObserverThrottle = null + } + + let currentUrl = window.location.href + + const checkForRouteChange = () => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href + console.log("Claude route changed, re-adding supermemory icon") + setTimeout(() => { + addSupermemoryIconToClaudeInput() + setupClaudeAutoFetch() + }, 1000) + } + } + + claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000) + + claudeRouteObserver = new MutationObserver((mutations) => { + if (claudeObserverThrottle) { + 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 + if ( + element.querySelector?.('div[contenteditable="true"]') || + element.querySelector?.("textarea") || + element.matches?.('div[contenteditable="true"]') || + element.matches?.("textarea") + ) { + shouldRecheck = true + } + } + }) + } + }) + + if (shouldRecheck) { + claudeObserverThrottle = setTimeout(() => { + try { + claudeObserverThrottle = null + addSupermemoryIconToClaudeInput() + setupClaudeAutoFetch() + } catch (error) { + console.error("Error in Claude observer callback:", error) + } + }, 300) + } + }) + + try { + claudeRouteObserver.observe(document.body, { + childList: true, + subtree: true, + }) + } catch (error) { + console.error("Failed to set up Claude route observer:", error) + if (claudeUrlCheckInterval) { + clearInterval(claudeUrlCheckInterval) + } + claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000) + } +} + +function addSupermemoryIconToClaudeInput() { + const targetContainers = document.querySelectorAll( + ".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0", + ) + + targetContainers.forEach((container) => { + if (container.hasAttribute("data-supermemory-icon-added")) { + return + } + + const existingIcon = container.querySelector( + `#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`, + ) + if (existingIcon) { + container.setAttribute("data-supermemory-icon-added", "true") + return + } + + const supermemoryIcon = createClaudeInputBarElement(async () => { + await getRelatedMemoriesForClaude( + POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED, + ) + }) + + supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + + container.setAttribute("data-supermemory-icon-added", "true") + + container.insertBefore(supermemoryIcon, container.firstChild) + }) +} + +async function getRelatedMemoriesForClaude(actionSource: string) { + try { + let userQuery = "" + + const supermemoryContainer = document.querySelector( + '[data-supermemory-icon-added="true"]', + ) + if (supermemoryContainer?.parentElement?.previousElementSibling) { + const pTag = + supermemoryContainer.parentElement.previousElementSibling.querySelector( + "p", + ) + userQuery = pTag?.innerText || pTag?.textContent || "" + } + + if (!userQuery.trim()) { + const textareaElement = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + userQuery = + textareaElement?.innerText || textareaElement?.textContent || "" + } + + if (!userQuery.trim()) { + const inputElements = document.querySelectorAll( + 'div[contenteditable="true"], textarea, input[type="text"]', + ) + for (const element of inputElements) { + const text = + (element as HTMLElement).innerText || + (element as HTMLInputElement).value + if (text?.trim()) { + userQuery = text.trim() + break + } + } + } + + console.log("Claude query extracted:", userQuery) + + if (!userQuery.trim()) { + console.log("No query text found for Claude") + return + } + + const icon = document.querySelector('[id*="sm-claude-input-bar-element"]') + + const iconElement = icon as HTMLElement + + if (!iconElement) { + console.warn("Claude icon element not found, cannot update feedback") + return + } + + updateClaudeIconFeedback("Searching memories...", iconElement) + + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Memory search timeout")), + UI_CONFIG.API_REQUEST_TIMEOUT, + ), + ) + + const response = await Promise.race([ + browser.runtime.sendMessage({ + action: MESSAGE_TYPES.GET_RELATED_MEMORIES, + data: userQuery, + actionSource: actionSource, + }), + timeoutPromise, + ]) + + console.log("Claude memories response:", response) + + if (response?.success && response?.data) { + const textareaElement = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + + if (textareaElement) { + textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>` + console.log( + "Text element dataset:", + textareaElement.dataset.supermemories, + ) + + iconElement.dataset.memoriesData = response.data + + updateClaudeIconFeedback("Included Memories", iconElement) + } else { + console.warn( + "Claude input area not found after successful memory fetch", + ) + updateClaudeIconFeedback("Memories found", iconElement) + } + } else { + console.warn("No memories found or API response invalid for Claude") + updateClaudeIconFeedback("No memories found", iconElement) + } + } catch (error) { + console.error("Error getting related memories for Claude:", error) + try { + const icon = document.querySelector( + '[id*="sm-claude-input-bar-element"]', + ) as HTMLElement + if (icon) { + updateClaudeIconFeedback("Error fetching memories", icon) + } + } catch (feedbackError) { + console.error("Failed to update Claude error feedback:", feedbackError) + } + } +} + +function updateClaudeIconFeedback( + message: string, + iconElement: HTMLElement, + resetAfter = 0, +) { + if (!iconElement.dataset.originalHtml) { + iconElement.dataset.originalHtml = iconElement.innerHTML + } + + const feedbackDiv = document.createElement("div") + feedbackDiv.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: #513EA9; + border-radius: 6px; + color: white; + font-size: 12px; + font-weight: 500; + cursor: ${message === "Included Memories" ? "pointer" : "default"}; + position: relative; + ` + + feedbackDiv.innerHTML = ` + <span>✓</span> + <span>${message}</span> + ` + + if (message === "Included Memories" && iconElement.dataset.memoriesData) { + 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; + border: 1px solid #333; + ` + + 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-weight: 600; color: #fff;">Included Memories</span> + ` + + const content = document.createElement("div") + content.style.cssText = ` + padding: 0; + max-height: 300px; + overflow-y: auto; + ` + + const memoriesText = iconElement.dataset.memoriesData || "" + console.log("Memories text:", memoriesText) + const individualMemories = memoriesText + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + console.log("Individual memories:", individualMemories) + + individualMemories.forEach((memory, index) => { + const memoryItem = document.createElement("div") + memoryItem.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + padding: 10px; + font-size: 13px; + line-height: 1.4; + ` + + const memoryText = document.createElement("div") + memoryText.style.cssText = ` + flex: 1; + color: #e5e5e5; + ` + memoryText.textContent = memory.trim() + + const removeBtn = document.createElement("button") + removeBtn.style.cssText = ` + background: transparent; + color: #9ca3af; + border: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + flex-shrink: 0; + height: fit-content; + display: flex; + align-items: center; + justify-content: center; + ` + removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>` + removeBtn.dataset.memoryIndex = index.toString() + + removeBtn.addEventListener("mouseenter", () => { + removeBtn.style.color = "#ef4444" + }) + removeBtn.addEventListener("mouseleave", () => { + removeBtn.style.color = "#9ca3af" + }) + + memoryItem.appendChild(memoryText) + memoryItem.appendChild(removeBtn) + content.appendChild(memoryItem) + }) + + popup.appendChild(header) + popup.appendChild(content) + document.body.appendChild(popup) + + feedbackDiv.addEventListener("mouseenter", () => { + const textSpan = feedbackDiv.querySelector("span:last-child") + if (textSpan) { + textSpan.textContent = "Click to see memories" + } + }) + + feedbackDiv.addEventListener("mouseleave", () => { + const textSpan = feedbackDiv.querySelector("span:last-child") + if (textSpan) { + textSpan.textContent = "Included Memories" + } + }) + + feedbackDiv.addEventListener("click", (e) => { + e.stopPropagation() + popup.style.display = "block" + }) + + document.addEventListener("click", (e) => { + if (!popup.contains(e.target as Node)) { + popup.style.display = "none" + } + }) + + content.querySelectorAll("button[data-memory-index]").forEach((button) => { + const htmlButton = button as HTMLButtonElement + htmlButton.addEventListener("click", () => { + const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) + const memoryItem = htmlButton.parentElement + + if (memoryItem) { + content.removeChild(memoryItem) + } + + const currentMemories = (iconElement.dataset.memoriesData || "") + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + currentMemories.splice(index, 1) + + const updatedMemories = currentMemories.join(" ,") + + iconElement.dataset.memoriesData = updatedMemories + + const textareaElement = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + if (textareaElement) { + textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>` + } + + content + .querySelectorAll("button[data-memory-index]") + .forEach((btn, newIndex) => { + const htmlBtn = btn as HTMLButtonElement + htmlBtn.dataset.memoryIndex = newIndex.toString() + }) + + if (currentMemories.length <= 1) { + if (textareaElement?.dataset.supermemories) { + delete textareaElement.dataset.supermemories + delete iconElement.dataset.memoriesData + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + } + popup.style.display = "none" + if (document.body.contains(popup)) { + document.body.removeChild(popup) + } + } + }) + }) + + setTimeout(() => { + if (document.body.contains(popup)) { + document.body.removeChild(popup) + } + }, 300000) + } + + iconElement.innerHTML = "" + iconElement.appendChild(feedbackDiv) + + if (resetAfter > 0) { + setTimeout(() => { + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + }, resetAfter) + } +} + +function setupClaudePromptCapture() { + if (document.body.hasAttribute("data-claude-prompt-capture-setup")) { + return + } + document.body.setAttribute("data-claude-prompt-capture-setup", "true") + const captureClaudePromptContent = async (source: string) => { + let promptContent = "" + + const contentEditableDiv = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + if (contentEditableDiv) { + promptContent = + contentEditableDiv.textContent || contentEditableDiv.innerText || "" + } + + if (!promptContent) { + const textarea = document.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + promptContent = textarea.value || "" + } + } + + const storedMemories = contentEditableDiv?.dataset.supermemories + if ( + storedMemories && + contentEditableDiv && + !promptContent.includes("Supermemories of user") + ) { + contentEditableDiv.innerHTML = `${contentEditableDiv.innerHTML} ${storedMemories}` + promptContent = + contentEditableDiv.textContent || contentEditableDiv.innerText || "" + } + + if (promptContent.trim()) { + console.log(`Claude prompt submitted via ${source}:`, promptContent) + + try { + await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.CAPTURE_PROMPT, + data: { + prompt: promptContent, + platform: "claude", + source: source, + }, + }) + } catch (error) { + console.error("Error sending Claude prompt to background:", error) + } + } + + const icons = document.querySelectorAll( + '[id*="sm-claude-input-bar-element"]', + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + delete iconElement.dataset.memoriesData + } + }) + + if (contentEditableDiv?.dataset.supermemories) { + delete contentEditableDiv.dataset.supermemories + } + } + + document.addEventListener( + "click", + async (event) => { + const target = event.target as HTMLElement + const sendButton = + target.closest( + "button.inline-flex.items-center.justify-center.relative.shrink-0.can-focus.select-none", + ) || + target.closest('button[class*="bg-accent-main-000"]') || + target.closest('button[class*="rounded-lg"]') + + if (sendButton) { + await captureClaudePromptContent("button click") + } + }, + true, + ) + + document.addEventListener( + "keydown", + async (event) => { + const target = event.target as HTMLElement + + if ( + (target.matches('div[contenteditable="true"]') || + target.matches(".ProseMirror") || + target.matches("textarea") || + target.closest(".ProseMirror")) && + event.key === "Enter" && + !event.shiftKey + ) { + await captureClaudePromptContent("Enter key") + } + }, + true, + ) +} + +async function setupClaudeAutoFetch() { + const result = await chrome.storage.local.get([ + STORAGE_KEYS.AUTO_SEARCH_ENABLED, + ]) + const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false + if (!autoSearchEnabled) { + return + } + + const textareaElement = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + + if ( + !textareaElement || + textareaElement.hasAttribute("data-supermemory-auto-fetch") + ) { + return + } + + textareaElement.setAttribute("data-supermemory-auto-fetch", "true") + + const handleInput = () => { + if (claudeDebounceTimeout) { + clearTimeout(claudeDebounceTimeout) + } + + claudeDebounceTimeout = setTimeout(async () => { + const content = textareaElement.textContent?.trim() || "" + + if (content.length > 2) { + await getRelatedMemoriesForClaude( + POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED, + ) + } else if (content.length === 0) { + const icons = document.querySelectorAll( + '[id*="sm-claude-input-bar-element"]', + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + delete iconElement.dataset.memoriesData + } + }) + + if (textareaElement.dataset.supermemories) { + delete textareaElement.dataset.supermemories + } + } + }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) + } + + textareaElement.addEventListener("input", handleInput) +} diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts new file mode 100644 index 00000000..a79f50fb --- /dev/null +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -0,0 +1,67 @@ +import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants" +import { DOMUtils } from "../../utils/ui-components" +import { initializeChatGPT } from "./chatgpt" +import { initializeClaude } from "./claude" +import { saveMemory, setupGlobalKeyboardShortcut, setupStorageListener } from "./shared" +import { initializeT3 } from "./t3" +import { handleTwitterNavigation, initializeTwitter, updateTwitterImportUI } from "./twitter" + +export default defineContentScript({ + matches: ["<all_urls>"], + main() { + // Setup global event listeners + browser.runtime.onMessage.addListener(async (message) => { + if (message.action === MESSAGE_TYPES.SHOW_TOAST) { + DOMUtils.showToast(message.state) + } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { + await saveMemory() + } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { + updateTwitterImportUI(message) + } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { + updateTwitterImportUI(message) + } + }) + + // Setup global keyboard shortcuts + setupGlobalKeyboardShortcut() + + // Setup storage listener + setupStorageListener() + + // Observer for dynamic content changes + const observeForDynamicChanges = () => { + const observer = new MutationObserver(() => { + if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { + initializeChatGPT() + } + if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { + initializeClaude() + } + if (DOMUtils.isOnDomain(DOMAINS.T3)) { + initializeT3() + } + if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + handleTwitterNavigation() + } + }) + + observer.observe(document.body, { + childList: true, + subtree: true, + }) + } + + // Initialize platform-specific functionality + initializeChatGPT() + initializeClaude() + initializeT3() + initializeTwitter() + + // Start observing for dynamic changes + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", observeForDynamicChanges) + } else { + observeForDynamicChanges() + } + }, +})
\ No newline at end of file diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts new file mode 100644 index 00000000..d8b665c5 --- /dev/null +++ b/apps/browser-extension/entrypoints/content/shared.ts @@ -0,0 +1,77 @@ +import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants" +import { DOMUtils } from "../../utils/ui-components" + +export async function saveMemory() { + try { + DOMUtils.showToast("loading") + + const highlightedText = window.getSelection()?.toString() || "" + const url = window.location.href + const html = document.documentElement.outerHTML + + const response = await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.SAVE_MEMORY, + data: { + html, + highlightedText, + url, + }, + actionSource: "context_menu", + }) + + console.log("Response from enxtension:", response) + if (response.success) { + DOMUtils.showToast("success") + } else { + DOMUtils.showToast("error") + } + } catch (error) { + console.error("Error saving memory:", error) + DOMUtils.showToast("error") + } +} + +export function setupGlobalKeyboardShortcut() { + document.addEventListener("keydown", async (event) => { + if ( + (event.ctrlKey || event.metaKey) && + event.shiftKey && + event.key === "m" + ) { + event.preventDefault() + await saveMemory() + } + }) +} + +export function setupStorageListener() { + window.addEventListener("message", (event) => { + if (event.source !== window) { + return + } + const bearerToken = event.data.token + const userData = event.data.userData + if (bearerToken && userData) { + if ( + !( + window.location.hostname === "localhost" || + window.location.hostname === "supermemory.ai" || + window.location.hostname === "app.supermemory.ai" + ) + ) { + console.log( + "Bearer token and user data is only allowed to be used on localhost or supermemory.ai", + ) + return + } + + chrome.storage.local.set( + { + [STORAGE_KEYS.BEARER_TOKEN]: bearerToken, + [STORAGE_KEYS.USER_DATA]: userData, + }, + () => {}, + ) + } + }) +}
\ No newline at end of file diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts new file mode 100644 index 00000000..4332076e --- /dev/null +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -0,0 +1,692 @@ +import { + DOMAINS, + ELEMENT_IDS, + MESSAGE_TYPES, + POSTHOG_EVENT_KEY, + STORAGE_KEYS, + UI_CONFIG, +} from "../../utils/constants" +import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components" + +let t3DebounceTimeout: NodeJS.Timeout | null = null +let t3RouteObserver: MutationObserver | null = null +let t3UrlCheckInterval: NodeJS.Timeout | null = null +let t3ObserverThrottle: NodeJS.Timeout | null = null + +export function initializeT3() { + if (!DOMUtils.isOnDomain(DOMAINS.T3)) { + return + } + + if (document.body.hasAttribute("data-t3-initialized")) { + return + } + + setTimeout(() => { + console.log("Adding supermemory icon to T3 input") + addSupermemoryIconToT3Input() + setupT3AutoFetch() + }, 2000) + + setupT3PromptCapture() + + setupT3RouteChangeDetection() + + document.body.setAttribute("data-t3-initialized", "true") +} + +function setupT3RouteChangeDetection() { + if (t3RouteObserver) { + t3RouteObserver.disconnect() + } + if (t3UrlCheckInterval) { + clearInterval(t3UrlCheckInterval) + } + if (t3ObserverThrottle) { + clearTimeout(t3ObserverThrottle) + t3ObserverThrottle = null + } + + let currentUrl = window.location.href + + const checkForRouteChange = () => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href + console.log("T3 route changed, re-adding supermemory icon") + setTimeout(() => { + addSupermemoryIconToT3Input() + setupT3AutoFetch() + }, 1000) + } + } + + t3UrlCheckInterval = setInterval(checkForRouteChange, 2000) + + t3RouteObserver = new MutationObserver((mutations) => { + if (t3ObserverThrottle) { + 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 + if ( + element.querySelector?.("textarea") || + element.querySelector?.('div[contenteditable="true"]') || + element.matches?.("textarea") || + element.matches?.('div[contenteditable="true"]') + ) { + shouldRecheck = true + } + } + }) + } + }) + + if (shouldRecheck) { + t3ObserverThrottle = setTimeout(() => { + try { + t3ObserverThrottle = null + addSupermemoryIconToT3Input() + setupT3AutoFetch() + } catch (error) { + console.error("Error in T3 observer callback:", error) + } + }, 300) + } + }) + + try { + t3RouteObserver.observe(document.body, { + childList: true, + subtree: true, + }) + } catch (error) { + console.error("Failed to set up T3 route observer:", error) + if (t3UrlCheckInterval) { + clearInterval(t3UrlCheckInterval) + } + t3UrlCheckInterval = setInterval(checkForRouteChange, 1000) + } +} + +function addSupermemoryIconToT3Input() { + const targetContainers = document.querySelectorAll( + ".flex.min-w-0.items-center.gap-2.overflow-hidden", + ) + + targetContainers.forEach((container) => { + if (container.hasAttribute("data-supermemory-icon-added")) { + return + } + + const existingIcon = container.querySelector( + `#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`, + ) + if (existingIcon) { + container.setAttribute("data-supermemory-icon-added", "true") + return + } + + const supermemoryIcon = createT3InputBarElement(async () => { + await getRelatedMemoriesForT3(POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED) + }) + + supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + + container.setAttribute("data-supermemory-icon-added", "true") + + container.insertBefore(supermemoryIcon, container.firstChild) + }) +} + +async function getRelatedMemoriesForT3(actionSource: string) { + try { + let userQuery = "" + + const supermemoryContainer = document.querySelector( + '[data-supermemory-icon-added="true"]', + ) + if ( + supermemoryContainer?.parentElement?.parentElement?.previousElementSibling + ) { + const textareaElement = + supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector( + "textarea", + ) + userQuery = textareaElement?.value || "" + } + + if (!userQuery.trim()) { + const textareaElement = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + userQuery = + textareaElement?.innerText || textareaElement?.textContent || "" + } + + if (!userQuery.trim()) { + const textareas = document.querySelectorAll("textarea") + for (const textarea of textareas) { + const text = (textarea as HTMLTextAreaElement).value + if (text?.trim()) { + userQuery = text.trim() + break + } + } + } + + console.log("T3 query extracted:", userQuery) + + if (!userQuery.trim()) { + console.log("No query text found for T3") + return + } + + const icon = document.querySelector('[id*="sm-t3-input-bar-element"]') + + const iconElement = icon as HTMLElement + + if (!iconElement) { + console.warn("T3 icon element not found, cannot update feedback") + return + } + + updateT3IconFeedback("Searching memories...", iconElement) + + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Memory search timeout")), + UI_CONFIG.API_REQUEST_TIMEOUT, + ), + ) + + const response = await Promise.race([ + browser.runtime.sendMessage({ + action: MESSAGE_TYPES.GET_RELATED_MEMORIES, + data: userQuery, + actionSource: actionSource, + }), + timeoutPromise, + ]) + + console.log("T3 memories response:", response) + + if (response?.success && response?.data) { + let textareaElement = null + const supermemoryContainer = document.querySelector( + '[data-supermemory-icon-added="true"]', + ) + if ( + supermemoryContainer?.parentElement?.parentElement + ?.previousElementSibling + ) { + textareaElement = + supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector( + "textarea", + ) + } + + if (!textareaElement) { + textareaElement = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + } + + if (textareaElement) { + if (textareaElement.tagName === "TEXTAREA") { + ;(textareaElement as HTMLTextAreaElement).dataset.supermemories = + `<br>Supermemories of user (only for the reference): ${response.data}</br>` + } else { + ;(textareaElement as HTMLElement).dataset.supermemories = + `<br>Supermemories of user (only for the reference): ${response.data}</br>` + } + + iconElement.dataset.memoriesData = response.data + + updateT3IconFeedback("Included Memories", iconElement) + } else { + console.warn("T3 input area not found after successful memory fetch") + updateT3IconFeedback("Memories found", iconElement) + } + } else { + console.warn("No memories found or API response invalid for T3") + updateT3IconFeedback("No memories found", iconElement) + } + } catch (error) { + console.error("Error getting related memories for T3:", error) + try { + const icon = document.querySelector( + '[id*="sm-t3-input-bar-element"]', + ) as HTMLElement + if (icon) { + updateT3IconFeedback("Error fetching memories", icon) + } + } catch (feedbackError) { + console.error("Failed to update T3 error feedback:", feedbackError) + } + } +} + +function updateT3IconFeedback( + message: string, + iconElement: HTMLElement, + resetAfter = 0, +) { + if (!iconElement.dataset.originalHtml) { + iconElement.dataset.originalHtml = iconElement.innerHTML + } + + const feedbackDiv = document.createElement("div") + feedbackDiv.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: #513EA9; + border-radius: 6px; + color: white; + font-size: 12px; + font-weight: 500; + cursor: ${message === "Included Memories" ? "pointer" : "default"}; + position: relative; + ` + + feedbackDiv.innerHTML = ` + <span>✓</span> + <span>${message}</span> + ` + + if (message === "Included Memories" && iconElement.dataset.memoriesData) { + 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; + border: 1px solid #333; + ` + + 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-weight: 600; color: #fff;">Included Memories</span> + ` + + const content = document.createElement("div") + content.style.cssText = ` + padding: 0; + max-height: 300px; + overflow-y: auto; + ` + + const memoriesText = iconElement.dataset.memoriesData || "" + console.log("Memories text:", memoriesText) + const individualMemories = memoriesText + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + console.log("Individual memories:", individualMemories) + + individualMemories.forEach((memory, index) => { + const memoryItem = document.createElement("div") + memoryItem.style.cssText = ` + display: flex; + align-items: center; + gap: 6px; + padding: 10px; + font-size: 13px; + line-height: 1.4; + ` + + const memoryText = document.createElement("div") + memoryText.style.cssText = ` + flex: 1; + color: #e5e5e5; + ` + memoryText.textContent = memory.trim() + + const removeBtn = document.createElement("button") + removeBtn.style.cssText = ` + background: transparent; + color: #9ca3af; + border: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + flex-shrink: 0; + height: fit-content; + display: flex; + align-items: center; + justify-content: center; + ` + removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>` + removeBtn.dataset.memoryIndex = index.toString() + + removeBtn.addEventListener("mouseenter", () => { + removeBtn.style.color = "#ef4444" + }) + removeBtn.addEventListener("mouseleave", () => { + removeBtn.style.color = "#9ca3af" + }) + + memoryItem.appendChild(memoryText) + memoryItem.appendChild(removeBtn) + content.appendChild(memoryItem) + }) + + popup.appendChild(header) + popup.appendChild(content) + document.body.appendChild(popup) + + feedbackDiv.addEventListener("mouseenter", () => { + const textSpan = feedbackDiv.querySelector("span:last-child") + if (textSpan) { + textSpan.textContent = "Click to see memories" + } + }) + + feedbackDiv.addEventListener("mouseleave", () => { + const textSpan = feedbackDiv.querySelector("span:last-child") + if (textSpan) { + textSpan.textContent = "Included Memories" + } + }) + + feedbackDiv.addEventListener("click", (e) => { + e.stopPropagation() + popup.style.display = "block" + }) + + document.addEventListener("click", (e) => { + if (!popup.contains(e.target as Node)) { + popup.style.display = "none" + } + }) + + content.querySelectorAll("button[data-memory-index]").forEach((button) => { + const htmlButton = button as HTMLButtonElement + htmlButton.addEventListener("click", () => { + const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) + const memoryItem = htmlButton.parentElement + + if (memoryItem) { + content.removeChild(memoryItem) + } + + const currentMemories = (iconElement.dataset.memoriesData || "") + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") + currentMemories.splice(index, 1) + + const updatedMemories = currentMemories.join(" ,") + + iconElement.dataset.memoriesData = updatedMemories + + const textareaElement = + (document.querySelector("textarea") as HTMLTextAreaElement) || + (document.querySelector('div[contenteditable="true"]') as HTMLElement) + if (textareaElement) { + textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>` + } + + content + .querySelectorAll("button[data-memory-index]") + .forEach((btn, newIndex) => { + const htmlBtn = btn as HTMLButtonElement + htmlBtn.dataset.memoryIndex = newIndex.toString() + }) + + if (currentMemories.length <= 1) { + if (textareaElement?.dataset.supermemories) { + delete textareaElement.dataset.supermemories + delete iconElement.dataset.memoriesData + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + } + popup.style.display = "none" + if (document.body.contains(popup)) { + document.body.removeChild(popup) + } + } + }) + }) + + setTimeout(() => { + if (document.body.contains(popup)) { + document.body.removeChild(popup) + } + }, 300000) + } + + iconElement.innerHTML = "" + iconElement.appendChild(feedbackDiv) + + if (resetAfter > 0) { + setTimeout(() => { + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + }, resetAfter) + } +} + +function setupT3PromptCapture() { + if (document.body.hasAttribute("data-t3-prompt-capture-setup")) { + return + } + document.body.setAttribute("data-t3-prompt-capture-setup", "true") + + const captureT3PromptContent = async (source: string) => { + let promptContent = "" + + const textarea = document.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + promptContent = textarea.value || "" + } + + if (!promptContent) { + const contentEditableDiv = document.querySelector( + 'div[contenteditable="true"]', + ) as HTMLElement + if (contentEditableDiv) { + promptContent = + contentEditableDiv.textContent || contentEditableDiv.innerText || "" + } + } + + const textareaElement = + textarea || + (document.querySelector('div[contenteditable="true"]') as HTMLElement) + const storedMemories = textareaElement?.dataset.supermemories + if ( + storedMemories && + textareaElement && + !promptContent.includes("Supermemories of user") + ) { + if (textareaElement.tagName === "TEXTAREA") { + ;(textareaElement as HTMLTextAreaElement).value = + `${promptContent} ${storedMemories}` + promptContent = (textareaElement as HTMLTextAreaElement).value + } else { + textareaElement.innerHTML = `${textareaElement.innerHTML} ${storedMemories}` + promptContent = + textareaElement.textContent || textareaElement.innerText || "" + } + } + + if (promptContent.trim()) { + console.log(`T3 prompt submitted via ${source}:`, promptContent) + + try { + await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.CAPTURE_PROMPT, + data: { + prompt: promptContent, + platform: "t3", + source: source, + }, + }) + } catch (error) { + console.error("Error sending T3 prompt to background:", error) + } + } + + const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]') + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + delete iconElement.dataset.memoriesData + } + }) + + if (textareaElement?.dataset.supermemories) { + delete textareaElement.dataset.supermemories + } + } + + document.addEventListener( + "click", + async (event) => { + const target = event.target as HTMLElement + const sendButton = + target.closest("button.focus-visible\\:ring-ring") || + target.closest('button[class*="bg-[rgb(162,59,103)]"]') || + target.closest('button[class*="rounded-lg"]') + + if (sendButton) { + await captureT3PromptContent("button click") + } + }, + true, + ) + + document.addEventListener( + "keydown", + async (event) => { + const target = event.target as HTMLElement + + if ( + (target.matches("textarea") || + target.matches('div[contenteditable="true"]')) && + event.key === "Enter" && + !event.shiftKey + ) { + if (target.matches("textarea")) { + const promptContent = (target as HTMLTextAreaElement).value || "" + if (promptContent.trim()) { + console.log("T3 prompt submitted via Enter key:", promptContent) + + try { + await browser.runtime.sendMessage({ + action: MESSAGE_TYPES.CAPTURE_PROMPT, + data: { + prompt: promptContent, + platform: "t3", + source: "Enter key", + }, + actionSource: "t3", + }) + } catch (error) { + console.error( + "Error sending T3 textarea prompt to background:", + error, + ) + } + } + } else { + await captureT3PromptContent("Enter key") + } + } + }, + true, + ) +} + +async function setupT3AutoFetch() { + const result = await chrome.storage.local.get([ + STORAGE_KEYS.AUTO_SEARCH_ENABLED, + ]) + const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false + + if (!autoSearchEnabled) { + return + } + + const textareaElement = + (document.querySelector("textarea") as HTMLTextAreaElement) || + (document.querySelector('div[contenteditable="true"]') as HTMLElement) + + if ( + !textareaElement || + textareaElement.hasAttribute("data-supermemory-auto-fetch") + ) { + return + } + + textareaElement.setAttribute("data-supermemory-auto-fetch", "true") + + const handleInput = () => { + if (t3DebounceTimeout) { + clearTimeout(t3DebounceTimeout) + } + + t3DebounceTimeout = setTimeout(async () => { + let content = "" + if (textareaElement.tagName === "TEXTAREA") { + content = (textareaElement as HTMLTextAreaElement).value?.trim() || "" + } else { + content = textareaElement.textContent?.trim() || "" + } + + if (content.length > 2) { + await getRelatedMemoriesForT3( + POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_AUTO_SEARCHED, + ) + } else if (content.length === 0) { + const icons = document.querySelectorAll( + '[id*="sm-t3-input-bar-element"]', + ) + + icons.forEach((icon) => { + const iconElement = icon as HTMLElement + if (iconElement.dataset.originalHtml) { + iconElement.innerHTML = iconElement.dataset.originalHtml + delete iconElement.dataset.originalHtml + delete iconElement.dataset.memoriesData + } + }) + + if (textareaElement.dataset.supermemories) { + delete textareaElement.dataset.supermemories + } + } + }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) + } + + textareaElement.addEventListener("input", handleInput) +} diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts new file mode 100644 index 00000000..15c6ec50 --- /dev/null +++ b/apps/browser-extension/entrypoints/content/twitter.ts @@ -0,0 +1,102 @@ +import { + DOMAINS, + ELEMENT_IDS, + MESSAGE_TYPES, + POSTHOG_EVENT_KEY, +} from "../../utils/constants" +import { trackEvent } from "../../utils/posthog" +import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components" + +export function initializeTwitter() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return + } + + // Initial setup + if (window.location.pathname === "/i/bookmarks") { + setTimeout(() => { + addTwitterImportButton() + }, 2000) + } else { + // Remove button if not on bookmarks page + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) + } + } +} + +function addTwitterImportButton() { + // Only show the import button on the bookmarks page + if (window.location.pathname !== "/i/bookmarks") { + return + } + + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + return + } + + const button = createTwitterImportButton(async () => { + try { + await browser.runtime.sendMessage({ + type: MESSAGE_TYPES.BATCH_IMPORT_ALL, + }) + await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, { + source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`, + }) + } catch (error) { + console.error("Error starting import:", error) + } + }) + + document.body.appendChild(button) +} + +export function updateTwitterImportUI(message: { + type: string + importedMessage?: string + totalImported?: number +}) { + const importButton = document.getElementById( + ELEMENT_IDS.TWITTER_IMPORT_BUTTON, + ) + if (!importButton) return + + const iconUrl = browser.runtime.getURL("/icon-16.png") + + if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { + importButton.innerHTML = ` + <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> + <span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span> + ` + importButton.style.cursor = "default" + } + + if (message.type === MESSAGE_TYPES.IMPORT_DONE) { + importButton.innerHTML = ` + <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> + <span style="font-weight: 500; font-size: 14px; color: #059669;">✓ Imported ${message.totalImported} tweets!</span> + ` + + setTimeout(() => { + importButton.innerHTML = ` + <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> + <span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span> + ` + importButton.style.cursor = "pointer" + }, 3000) + } +} + +export function handleTwitterNavigation() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return + } + + if (window.location.pathname === "/i/bookmarks") { + addTwitterImportButton() + } else { + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) + } + } +}
\ No newline at end of file diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index ddb498c6..3e4f15e2 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -16,7 +16,10 @@ function App() { const [currentUrl, setCurrentUrl] = useState<string>("") const [currentTitle, setCurrentTitle] = useState<string>("") const [saving, setSaving] = useState<boolean>(false) - const [activeTab, setActiveTab] = useState<"save" | "imports">("save") + const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">( + "save", + ) + const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false) const queryClient = useQueryClient() const { data: projects = [], isLoading: loadingProjects } = useProjects({ @@ -32,9 +35,14 @@ function App() { try { const result = await chrome.storage.local.get([ STORAGE_KEYS.BEARER_TOKEN, + STORAGE_KEYS.AUTO_SEARCH_ENABLED, ]) const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN] setUserSignedIn(isSignedIn) + + const autoSearchSetting = + result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false + setAutoSearchEnabled(autoSearchSetting) } catch (error) { console.error("Error checking auth status:", error) setUserSignedIn(false) @@ -103,11 +111,6 @@ function App() { action: MESSAGE_TYPES.SHOW_TOAST, state: "success", }) - } else { - await chrome.tabs.sendMessage(tabs[0].id, { - action: MESSAGE_TYPES.SHOW_TOAST, - state: "error", - }) } window.close() @@ -136,6 +139,17 @@ function App() { } } + const handleAutoSearchToggle = async (enabled: boolean) => { + try { + await chrome.storage.local.set({ + [STORAGE_KEYS.AUTO_SEARCH_ENABLED]: enabled, + }) + setAutoSearchEnabled(enabled) + } catch (error) { + console.error("Error updating auto search setting:", error) + } + } + const handleSignOut = async () => { try { await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]) @@ -208,7 +222,7 @@ function App() { {/* Tab Navigation */} <div className="flex bg-gray-100 rounded-lg p-1 mb-4"> <button - className={`flex-1 py-2 px-4 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${ + className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${ activeTab === "save" ? "bg-white text-black shadow-sm" : "text-gray-500 hover:text-gray-700" @@ -219,7 +233,7 @@ function App() { Save </button> <button - className={`flex-1 py-2 px-4 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${ + className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${ activeTab === "imports" ? "bg-white text-black shadow-sm" : "text-gray-500 hover:text-gray-700" @@ -229,6 +243,17 @@ function App() { > Imports </button> + <button + className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${ + activeTab === "settings" + ? "bg-white text-black shadow-sm" + : "text-gray-500 hover:text-gray-700" + }`} + onClick={() => setActiveTab("settings")} + type="button" + > + Settings + </button> </div> {/* Tab Content */} @@ -295,7 +320,7 @@ function App() { </button> </div> </div> - ) : ( + ) : activeTab === "imports" ? ( <div className="flex flex-col gap-4 min-h-[200px]"> {/* Import Actions */} <div className="flex flex-col gap-4"> @@ -370,6 +395,40 @@ function App() { </div> </div> </div> + ) : ( + <div className="flex flex-col gap-4 min-h-[200px]"> + <div className="mb-4"> + <h3 className="text-base font-semibold text-black mb-3"> + Chat Integration + </h3> + <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200"> + <div className="flex flex-col"> + <span className="text-sm font-medium text-black"> + Auto Search Memories + </span> + <span className="text-xs text-gray-500"> + Automatically search your memories while typing in chat + apps + </span> + </div> + <label className="relative inline-flex items-center cursor-pointer"> + <input + checked={autoSearchEnabled} + className="sr-only peer" + onChange={(e) => + handleAutoSearchToggle(e.target.checked) + } + type="checkbox" + /> + <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gray-700" /> + </label> + </div> + <p className="text-xs text-gray-500 mt-2"> + When enabled, supermemory will search your memories as you + type in ChatGPT, Claude, and T3.chat + </p> + </div> + </div> )} {showProjectSelector && ( 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;" />` diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 61b83517..1bd7910d 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ manifest: { name: "supermemory", homepage_url: "https://supermemory.ai", - version: "6.0.001", + version: "6.0.002", permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"], host_permissions: [ "*://x.com/*", @@ -30,6 +30,6 @@ export default defineConfig({ ], }, webExt: { - disabled: true, + chromiumArgs: ["--user-data-dir=./.wxt/chrome-data"], }, }) |