diff options
| author | MaheshtheDev <[email protected]> | 2025-09-08 05:19:10 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-09-08 05:19:10 +0000 |
| commit | 3e5ea2fd9ed210644ae29b013b579703e30986de (patch) | |
| tree | 7de64cf7bddca72cf56e89ee4320005dad9f2ac5 | |
| parent | fix: billing page (#416) (diff) | |
| download | supermemory-3e5ea2fd9ed210644ae29b013b579703e30986de.tar.xz supermemory-3e5ea2fd9ed210644ae29b013b579703e30986de.zip | |
extension: updated telemetry and batch upload (#415)
| -rw-r--r-- | apps/browser-extension/.env.example | 2 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/background.ts | 130 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/content.ts | 565 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/popup/App.tsx | 194 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/popup/style.css | 1 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/welcome/Welcome.tsx | 6 | ||||
| -rw-r--r-- | apps/browser-extension/package.json | 1 | ||||
| -rw-r--r-- | apps/browser-extension/utils/api.ts | 92 | ||||
| -rw-r--r-- | apps/browser-extension/utils/constants.ts | 15 | ||||
| -rw-r--r-- | apps/browser-extension/utils/posthog.ts | 74 | ||||
| -rw-r--r-- | apps/browser-extension/utils/twitter-import.ts | 77 | ||||
| -rw-r--r-- | apps/browser-extension/utils/types.ts | 4 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 149 | ||||
| -rw-r--r-- | apps/browser-extension/wxt.config.ts | 15 | ||||
| -rw-r--r-- | apps/web/app/page.tsx | 30 | ||||
| -rw-r--r-- | bun.lock | 3 |
16 files changed, 706 insertions, 652 deletions
diff --git a/apps/browser-extension/.env.example b/apps/browser-extension/.env.example new file mode 100644 index 00000000..aca1ac3d --- /dev/null +++ b/apps/browser-extension/.env.example @@ -0,0 +1,2 @@ +# PostHog Configuration +WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here
\ No newline at end of file diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 1f4ae24e..05fef55d 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -1,47 +1,52 @@ -import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"; +import { getDefaultProject, saveMemory, searchMemories } from "../utils/api" import { CONTAINER_TAGS, CONTEXT_MENU_IDS, MESSAGE_TYPES, -} from "../utils/constants"; -import { captureTwitterTokens } from "../utils/twitter-auth"; + POSTHOG_EVENT_KEY, +} from "../utils/constants" +import { trackEvent } from "../utils/posthog" +import { captureTwitterTokens } from "../utils/twitter-auth" import { type TwitterImportConfig, TwitterImporter, -} from "../utils/twitter-import"; +} from "../utils/twitter-import" import type { ExtensionMessage, MemoryData, MemoryPayload, -} from "../utils/types"; +} from "../utils/types" export default defineBackground(() => { - let twitterImporter: TwitterImporter | null = null; + let twitterImporter: TwitterImporter | null = null - browser.runtime.onInstalled.addListener((details) => { + browser.runtime.onInstalled.addListener(async (details) => { browser.contextMenus.create({ id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, title: "Save to supermemory", contexts: ["selection", "page", "link"], - }); + }) - // Open welcome tab on first install if (details.reason === "install") { + await trackEvent("extension_installed", { + reason: details.reason, + version: browser.runtime.getManifest().version, + }) browser.tabs.create({ url: browser.runtime.getURL("/welcome.html"), - }); + }) } - }); + }) // Intercept Twitter requests to capture authentication headers. browser.webRequest.onBeforeSendHeaders.addListener( (details) => { - captureTwitterTokens(details); - return {}; + captureTwitterTokens(details) + return {} }, { urls: ["*://x.com/*", "*://twitter.com/*"] }, ["requestHeaders", "extraHeaders"], - ); + ) // Handle context menu clicks. browser.contextMenus.onClicked.addListener(async (info, tab) => { @@ -50,27 +55,28 @@ export default defineBackground(() => { try { await browser.tabs.sendMessage(tab.id, { action: MESSAGE_TYPES.SAVE_MEMORY, - }); + actionSource: "context_menu", + }) } catch (error) { - console.error("Failed to send message to content script:", error); + console.error("Failed to send message to content script:", error) } } } - }); + }) // Send message to current active tab. const sendMessageToCurrentTab = async (message: string) => { const tabs = await browser.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].id) { await browser.tabs.sendMessage(tabs[0].id, { type: MESSAGE_TYPES.IMPORT_UPDATE, importedMessage: message, - }); + }) } - }; + } /** * Send import completion message @@ -79,61 +85,71 @@ export default defineBackground(() => { const tabs = await browser.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].id) { await browser.tabs.sendMessage(tabs[0].id, { type: MESSAGE_TYPES.IMPORT_DONE, totalImported, - }); + }) } - }; + } /** * Save memory to supermemory API */ const saveMemoryToSupermemory = async ( data: MemoryData, + actionSource: string, ): Promise<{ success: boolean; data?: unknown; error?: string }> => { try { - let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT; + let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT try { - const defaultProject = await getDefaultProject(); + const defaultProject = await getDefaultProject() if (defaultProject?.containerTag) { - containerTag = defaultProject.containerTag; + containerTag = defaultProject.containerTag } } catch (error) { - console.warn("Failed to get default project, using fallback:", error); + console.warn("Failed to get default project, using fallback:", error) } const payload: MemoryPayload = { containerTags: [containerTag], content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`, metadata: { sm_source: "consumer" }, - }; + } + + const responseData = await saveMemory(payload) + + await trackEvent(POSTHOG_EVENT_KEY.SAVE_MEMORY_ATTEMPTED, { + source: `${POSTHOG_EVENT_KEY.SOURCE}_${actionSource}`, + has_highlight: !!data.highlightedText, + url_domain: data.url ? new URL(data.url).hostname : undefined, + }) - const responseData = await saveMemory(payload); - return { success: true, data: responseData }; + return { success: true, data: responseData } } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error", - }; + } } - }; + } const getRelatedMemories = async ( data: string, + eventSource: string, ): Promise<{ success: boolean; data?: unknown; error?: string }> => { try { const responseData = await searchMemories(data) const response = responseData as { results?: Array<{ memory?: string }> } - let memories = ""; + let memories = "" response.results?.forEach((result, index) => { memories += `[${index + 1}] ${result.memory} ` - }) + }) console.log("Memories:", memories) + await trackEvent(eventSource) return { success: true, data: memories } } catch (error) { return { @@ -141,7 +157,7 @@ export default defineBackground(() => { error: error instanceof Error ? error.message : "Unknown error", } } - }; + } /** * Handle extension messages @@ -154,48 +170,52 @@ export default defineBackground(() => { onProgress: sendMessageToCurrentTab, onComplete: sendImportDoneMessage, onError: async (error: Error) => { - await sendMessageToCurrentTab(`Error: ${error.message}`); + await sendMessageToCurrentTab(`Error: ${error.message}`) }, - }; + } - twitterImporter = new TwitterImporter(importConfig); - twitterImporter.startImport().catch(console.error); - sendResponse({ success: true }); - return true; + twitterImporter = new TwitterImporter(importConfig) + twitterImporter.startImport().catch(console.error) + sendResponse({ success: true }) + return true } // Handle regular memory save request if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - (async () => { + ;(async () => { try { const result = await saveMemoryToSupermemory( message.data as MemoryData, - ); - sendResponse(result); + message.actionSource || "unknown", + ) + sendResponse(result) } catch (error) { sendResponse({ success: false, error: error instanceof Error ? error.message : "Unknown error", - }); + }) } - })(); - return true; + })() + return true } if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) { - (async () => { + ;(async () => { try { - const result = await getRelatedMemories(message.data as string); - sendResponse(result); + const result = await getRelatedMemories( + message.data as string, + message.actionSource || "unknown", + ) + sendResponse(result) } catch (error) { sendResponse({ success: false, error: error instanceof Error ? error.message : "Unknown error", - }); + }) } - })(); - return true; + })() + return true } }, - ); -}); + ) +}) diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index 6c8a96c5..e755efa6 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -2,94 +2,105 @@ import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES, + POSTHOG_EVENT_KEY, STORAGE_KEYS, -} from "../utils/constants"; +} from "../utils/constants" +import { trackEvent } from "../utils/posthog" import { createChatGPTInputBarElement, createClaudeInputBarElement, - createSaveTweetElement, createT3InputBarElement, createTwitterImportButton, - createTwitterImportUI, DOMUtils, -} from "../utils/ui-components"; +} from "../utils/ui-components" export default defineContentScript({ matches: ["<all_urls>"], main() { - let twitterImportUI: HTMLElement | null = null; - let isTwitterImportOpen = false; browser.runtime.onMessage.addListener(async (message) => { if (message.action === MESSAGE_TYPES.SHOW_TOAST) { - DOMUtils.showToast(message.state); + DOMUtils.showToast(message.state) } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - await saveMemory(); + await saveMemory() } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - updateTwitterImportUI(message); + updateTwitterImportUI(message) } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - updateTwitterImportUI(message); + updateTwitterImportUI(message) } - }); + }) const observeForMemoriesDialog = () => { const observer = new MutationObserver(() => { if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - addSupermemoryButtonToMemoriesDialog(); - addSaveChatGPTElementBeforeComposerBtn(); + addSupermemoryButtonToMemoriesDialog() + addSaveChatGPTElementBeforeComposerBtn() } if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - addSupermemoryIconToClaudeInput(); + addSupermemoryIconToClaudeInput() } if (DOMUtils.isOnDomain(DOMAINS.T3)) { - addSupermemoryIconToT3Input(); + addSupermemoryIconToT3Input() } - if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - addTwitterImportButton(); - //addSaveTweetElement(); + 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)) { + if ( + DOMUtils.isOnDomain(DOMAINS.TWITTER) && + window.location.pathname === "/i/bookmarks" + ) { setTimeout(() => { - addTwitterImportButton(); // Wait 2 seconds for page to load + addTwitterImportButton() // Wait 2 seconds for page to load //addSaveTweetElement(); - }, 2000); + }, 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); + 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); + addSupermemoryIconToT3Input() // Wait 2 seconds for T3 page to load + }, 2000) } if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", observeForMemoriesDialog); + document.addEventListener("DOMContentLoaded", observeForMemoriesDialog) } else { - observeForMemoriesDialog(); + observeForMemoriesDialog() } async function saveMemory() { try { - DOMUtils.showToast("loading"); + DOMUtils.showToast("loading") - const highlightedText = window.getSelection()?.toString() || ""; + const highlightedText = window.getSelection()?.toString() || "" - const url = window.location.href; + const url = window.location.href - const html = document.documentElement.outerHTML; + const html = document.documentElement.outerHTML const response = await browser.runtime.sendMessage({ action: MESSAGE_TYPES.SAVE_MEMORY, @@ -98,75 +109,77 @@ export default defineContentScript({ highlightedText, url, }, - }); + actionSource: "context_menu", + }) - console.log("Response from enxtension:", response); + console.log("Response from enxtension:", response) if (response.success) { - DOMUtils.showToast("success"); + DOMUtils.showToast("success") } else { - DOMUtils.showToast("error"); + DOMUtils.showToast("error") } } catch (error) { - console.error("Error saving memory:", error); - DOMUtils.showToast("error"); + console.error("Error saving memory:", error) + DOMUtils.showToast("error") } } - async function getRelatedMemories() { + async function getRelatedMemories(actionSource: string) { try { const userQuery = - document.getElementById("prompt-textarea")?.textContent || ""; + 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"); + const promptElement = document.getElementById("prompt-textarea") if (promptElement) { - const currentContent = promptElement.innerHTML; - promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`; + const currentContent = promptElement.innerHTML + promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}` } } } catch (error) { - console.error("Error getting related memories:", error); + console.error("Error getting related memories:", error) } } function addSupermemoryButtonToMemoriesDialog() { - const dialogs = document.querySelectorAll('[role="dialog"]'); - let memoriesDialog: HTMLElement | null = null; + const dialogs = document.querySelectorAll('[role="dialog"]') + let memoriesDialog: HTMLElement | null = null for (const dialog of dialogs) { - const headerText = dialog.querySelector("h2"); + const headerText = dialog.querySelector("h2") if (headerText?.textContent?.includes("Saved memories")) { - memoriesDialog = dialog as HTMLElement; - break; + memoriesDialog = dialog as HTMLElement + break } } - if (!memoriesDialog) return; + if (!memoriesDialog) return - if (memoriesDialog.querySelector("#supermemory-save-button")) return; + if (memoriesDialog.querySelector("#supermemory-save-button")) return const deleteAllContainer = memoriesDialog.querySelector( ".mt-5.flex.justify-end", - ); - if (!deleteAllContainer) return; + ) + if (!deleteAllContainer) return - const supermemoryButton = document.createElement("button"); - supermemoryButton.id = "supermemory-save-button"; - supermemoryButton.className = "btn relative btn-primary-outline mr-2"; + 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"); + 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; @@ -178,388 +191,352 @@ export default defineContentScript({ font-size: 14px !important; margin-right: 8px !important; cursor: pointer !important; - `; + ` supermemoryButton.addEventListener("mouseenter", () => { - supermemoryButton.style.backgroundColor = "#2B2E33"; - }); + supermemoryButton.style.backgroundColor = "#2B2E33" + }) supermemoryButton.addEventListener("mouseleave", () => { - supermemoryButton.style.backgroundColor = "#1C2026"; - }); + supermemoryButton.style.backgroundColor = "#1C2026" + }) supermemoryButton.addEventListener("click", async () => { - await saveMemoriesToSupermemory(); - }); + await saveMemoriesToSupermemory() + }) deleteAllContainer.insertBefore( supermemoryButton, deleteAllContainer.firstChild, - ); + ) } async function saveMemoriesToSupermemory() { try { - DOMUtils.showToast("loading"); + DOMUtils.showToast("loading") const memoriesTable = document.querySelector( '[role="dialog"] table tbody', - ); + ) if (!memoriesTable) { - DOMUtils.showToast("error"); - return; + DOMUtils.showToast("error") + return } - const memoryRows = memoriesTable.querySelectorAll("tr"); - const memories: string[] = []; + const memoryRows = memoriesTable.querySelectorAll("tr") + const memories: string[] = [] memoryRows.forEach((row) => { - const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap"); + const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap") if (memoryCell?.textContent) { - memories.push(memoryCell.textContent.trim()); + memories.push(memoryCell.textContent.trim()) } - }); + }) - console.log("Memories:", memories); + console.log("Memories:", memories) if (memories.length === 0) { - DOMUtils.showToast("error"); - return; + DOMUtils.showToast("error") + return } - const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`; + 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 }); + console.log({ response }) if (response.success) { - DOMUtils.showToast("success"); + DOMUtils.showToast("success") } else { - DOMUtils.showToast("error"); + DOMUtils.showToast("error") } } catch (error) { - console.error("Error saving memories to supermemory:", error); - DOMUtils.showToast("error"); + console.error("Error saving memories to supermemory:", error) + DOMUtils.showToast("error") } } function addTwitterImportButton() { if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return; + return } // Only show the import button on the bookmarks page if (window.location.pathname !== "/i/bookmarks") { - return; + return } if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - return; - } - - const button = createTwitterImportButton(() => { - showTwitterImportUI(); - }); - - document.body.appendChild(button); - } - - function showTwitterImportUI() { - if (twitterImportUI) { - twitterImportUI.remove(); + return } - isTwitterImportOpen = true; - - // Check if user is authenticated - browser.storage.local.get([STORAGE_KEYS.BEARER_TOKEN], (result) => { - const isAuthenticated = !!result[STORAGE_KEYS.BEARER_TOKEN]; - - twitterImportUI = createTwitterImportUI( - hideTwitterImportUI, - async () => { - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - }); - } catch (error) { - console.error("Error starting import:", error); - } - }, - isAuthenticated, - ); + 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(twitterImportUI); - }); + document.body.appendChild(button) } - function hideTwitterImportUI() { - if (twitterImportUI) { - twitterImportUI.remove(); - twitterImportUI = null; - } - isTwitterImportOpen = false; - } function updateTwitterImportUI(message: { - type: string; - importedMessage?: string; - totalImported?: number; + type: string + importedMessage?: string + totalImported?: number }) { - if (!isTwitterImportOpen || !twitterImportUI) return; + const importButton = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) + if (!importButton) return - const statusDiv = twitterImportUI.querySelector( - `#${ELEMENT_IDS.TWITTER_IMPORT_STATUS}`, - ); - const button = twitterImportUI.querySelector( - `#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`, - ); + const iconUrl = browser.runtime.getURL("/icon-16.png") if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - if (statusDiv) { - statusDiv.innerHTML = ` - <div style="display: flex; align-items: center; gap: 8px; color: #92400e; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 8px 12px; font-size: 13px;"> - <div style="width: 12px; height: 12px; border: 2px solid #f59e0b; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div> - <span>${message.importedMessage}</span> - </div> - `; - } - if (button) { - (button as HTMLButtonElement).disabled = true; - (button as HTMLButtonElement).textContent = "Importing..."; - } + 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) { - if (statusDiv) { - statusDiv.innerHTML = ` - <div style="display: flex; align-items: center; gap: 8px; color: #0369a1; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 8px 12px; font-size: 13px;"> - <span style="color: #059669;">✓</span> - <span>Successfully imported ${message.totalImported} tweets!</span> - </div> - `; - } - + 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(() => { - hideTwitterImportUI(); - }, 3000); + 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; + return } - const composerButtons = document.querySelectorAll("button.composer-btn"); + const composerButtons = document.querySelectorAll("button.composer-btn") composerButtons.forEach((button) => { if (button.hasAttribute("data-supermemory-icon-added-before")) { - return; + return } - const parent = button.parentElement; - if (!parent) return; + const parent = button.parentElement + if (!parent) return - const parentSiblings = parent.parentElement?.children; - if (!parentSiblings) return; + const parentSiblings = parent.parentElement?.children + if (!parentSiblings) return - let hasSpeechButtonSibling = false; + let hasSpeechButtonSibling = false for (const sibling of parentSiblings) { if ( sibling.getAttribute("data-testid") === "composer-speech-button-container" ) { - hasSpeechButtonSibling = true; - break; + hasSpeechButtonSibling = true + break } } - if (!hasSpeechButtonSibling) return; + if (!hasSpeechButtonSibling) return - const grandParent = parent.parentElement; - if (!grandParent) 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; + button.setAttribute("data-supermemory-icon-added-before", "true") + return } const saveChatGPTElement = createChatGPTInputBarElement(async () => { - await getRelatedMemories(); - }); + 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)}`; + 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"); + button.setAttribute("data-supermemory-icon-added-before", "true") - grandParent.insertBefore(saveChatGPTElement, parent); - }); + grandParent.insertBefore(saveChatGPTElement, parent) + }) } function addSupermemoryIconToClaudeInput() { if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - return; + 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; + return } const existingIcon = container.querySelector( `#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`, - ); + ) if (existingIcon) { - container.setAttribute("data-supermemory-icon-added", "true"); - return; + container.setAttribute("data-supermemory-icon-added", "true") + return } const supermemoryIcon = createClaudeInputBarElement(async () => { - await getRelatedMemoriesForClaude(); - }); + await getRelatedMemoriesForClaude() + }) - supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + 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.setAttribute("data-supermemory-icon-added", "true") - container.insertBefore(supermemoryIcon, container.firstChild); - }); + container.insertBefore(supermemoryIcon, container.firstChild) + }) } async function getRelatedMemoriesForClaude() { try { - let userQuery = ""; + 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 || ""; + ) + userQuery = pTag?.innerText || pTag?.textContent || "" } if (!userQuery.trim()) { const textareaElement = document.querySelector( 'div[contenteditable="true"]', - ) as HTMLElement; + ) as HTMLElement userQuery = - textareaElement?.innerText || textareaElement?.textContent || ""; + 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; + (element as HTMLInputElement).value if (text?.trim()) { - userQuery = text.trim(); - break; + userQuery = text.trim() + break } } } - console.log("Claude query extracted:", userQuery); + console.log("Claude query extracted:", userQuery) if (!userQuery.trim()) { - console.log("No query text found"); - DOMUtils.showToast("error"); - return; + 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); + console.log("Claude memories response:", response) if (response.success && response.data) { const textareaElement = document.querySelector( 'div[contenteditable="true"]', - ) as HTMLElement; + ) as HTMLElement if (textareaElement) { - const currentContent = textareaElement.innerHTML; - textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`; + const currentContent = textareaElement.innerHTML + textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}` - textareaElement.dispatchEvent( - new Event("input", { bubbles: true }), - ); + textareaElement.dispatchEvent(new Event("input", { bubbles: true })) } else { - console.log("Could not find Claude input area"); + 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); + console.error("Error getting related memories for Claude:", error) } } function addSupermemoryIconToT3Input() { if (!DOMUtils.isOnDomain(DOMAINS.T3)) { - return; + 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; + return } const existingIcon = container.querySelector( `#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`, - ); + ) if (existingIcon) { - container.setAttribute("data-supermemory-icon-added", "true"); - return; + container.setAttribute("data-supermemory-icon-added", "true") + return } const supermemoryIcon = createT3InputBarElement(async () => { - await getRelatedMemoriesForT3(); - }); + await getRelatedMemoriesForT3() + }) - supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + 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.setAttribute("data-supermemory-icon-added", "true") - container.insertBefore(supermemoryIcon, container.firstChild); - }); + container.insertBefore(supermemoryIcon, container.firstChild) + }) } async function getRelatedMemoriesForT3() { try { - let userQuery = ""; + let userQuery = "" const supermemoryContainer = document.querySelector( '[data-supermemory-icon-added="true"]', - ); + ) if ( supermemoryContainer?.parentElement?.parentElement ?.previousElementSibling @@ -567,48 +544,49 @@ export default defineContentScript({ const textareaElement = supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector( "textarea", - ); - userQuery = textareaElement?.value || ""; + ) + userQuery = textareaElement?.value || "" } if (!userQuery.trim()) { const textareaElement = document.querySelector( 'div[contenteditable="true"]', - ) as HTMLElement; + ) as HTMLElement userQuery = - textareaElement?.innerText || textareaElement?.textContent || ""; + textareaElement?.innerText || textareaElement?.textContent || "" } if (!userQuery.trim()) { - const textareas = document.querySelectorAll("textarea"); + const textareas = document.querySelectorAll("textarea") for (const textarea of textareas) { - const text = (textarea as HTMLTextAreaElement).value; + const text = (textarea as HTMLTextAreaElement).value if (text?.trim()) { - userQuery = text.trim(); - break; + userQuery = text.trim() + break } } } - console.log("T3 query extracted:", userQuery); + console.log("T3 query extracted:", userQuery) if (!userQuery.trim()) { - console.log("No query text found"); - return; + 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); + console.log("T3 memories response:", response) if (response.success && response.data) { - let textareaElement = null; + let textareaElement = null const supermemoryContainer = document.querySelector( '[data-supermemory-icon-added="true"]', - ); + ) if ( supermemoryContainer?.parentElement?.parentElement ?.previousElementSibling @@ -616,75 +594,41 @@ export default defineContentScript({ textareaElement = supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector( "textarea", - ); + ) } if (!textareaElement) { textareaElement = document.querySelector( 'div[contenteditable="true"]', - ) as HTMLElement; + ) as HTMLElement } if (textareaElement) { if (textareaElement.tagName === "TEXTAREA") { const currentContent = (textareaElement as HTMLTextAreaElement) - .value; - (textareaElement as HTMLTextAreaElement).value = - `${currentContent}\n\nSupermemories: ${response.data}`; + .value + ;(textareaElement as HTMLTextAreaElement).value = + `${currentContent}\n\nSupermemories: ${response.data}` } else { - const currentContent = textareaElement.innerHTML; - textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`; + const currentContent = textareaElement.innerHTML + textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}` } - textareaElement.dispatchEvent( - new Event("input", { bubbles: true }), - ); + textareaElement.dispatchEvent(new Event("input", { bubbles: true })) } else { - console.log("Could not find T3 input area"); + 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); + console.error("Error getting related memories for T3:", error) } } - // TODO: Add Tweet Capture Functionality - function _addSaveTweetElement() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return; - } - - const targetDivs = document.querySelectorAll( - "div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08", - ); - - targetDivs.forEach((targetDiv) => { - if (targetDiv.hasAttribute("data-supermemory-icon-added")) { - return; - } - - const previousElement = targetDiv.previousElementSibling; - if (previousElement?.id?.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) { - targetDiv.setAttribute("data-supermemory-icon-added", "true"); - return; - } - - const saveTweetElement = createSaveTweetElement(async () => { - await saveMemory(); - }); - - saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; - - targetDiv.setAttribute("data-supermemory-icon-added", "true"); - - targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv); - }); - } document.addEventListener("keydown", async (event) => { if ( @@ -692,18 +636,18 @@ export default defineContentScript({ event.shiftKey && event.key === "m" ) { - event.preventDefault(); - await saveMemory(); + event.preventDefault() + await saveMemory() } - }); + }) window.addEventListener("message", (event) => { if (event.source !== window) { - return; + return } - const bearerToken = event.data.token; - - if (bearerToken) { + const bearerToken = event.data.token + const userData = event.data.userData + if (bearerToken && userData) { if ( !( window.location.hostname === "localhost" || @@ -712,18 +656,19 @@ export default defineContentScript({ ) ) { console.log( - "Bearer token is only allowed to be used on localhost or supermemory.ai", - ); - return; + "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/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 3f960b34..ddb498c6 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -1,118 +1,152 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; -import "./App.css"; -import { STORAGE_KEYS } from "../../utils/constants"; +import { useQueryClient } from "@tanstack/react-query" +import { useEffect, useState } from "react" +import "./App.css" +import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants" import { useDefaultProject, useProjects, useSetDefaultProject, -} from "../../utils/query-hooks"; -import type { Project } from "../../utils/types"; +} from "../../utils/query-hooks" +import type { Project } from "../../utils/types" function App() { - const [userSignedIn, setUserSignedIn] = useState<boolean>(false); - const [loading, setLoading] = useState<boolean>(true); - const [showProjectSelector, setShowProjectSelector] = - useState<boolean>(false); - const [currentUrl, setCurrentUrl] = useState<string>(""); - const [currentTitle, setCurrentTitle] = useState<string>(""); - const [saving, setSaving] = useState<boolean>(false); - const [activeTab, setActiveTab] = useState<"save" | "imports">("save"); + const [userSignedIn, setUserSignedIn] = useState<boolean>(false) + const [loading, setLoading] = useState<boolean>(true) + const [showProjectSelector, setShowProjectSelector] = useState<boolean>(false) + const [currentUrl, setCurrentUrl] = useState<string>("") + const [currentTitle, setCurrentTitle] = useState<string>("") + const [saving, setSaving] = useState<boolean>(false) + const [activeTab, setActiveTab] = useState<"save" | "imports">("save") - const queryClient = useQueryClient(); + const queryClient = useQueryClient() const { data: projects = [], isLoading: loadingProjects } = useProjects({ enabled: userSignedIn, - }); + }) const { data: defaultProject } = useDefaultProject({ enabled: userSignedIn, - }); - const setDefaultProjectMutation = useSetDefaultProject(); + }) + const setDefaultProjectMutation = useSetDefaultProject() useEffect(() => { const checkAuthStatus = async () => { try { const result = await chrome.storage.local.get([ STORAGE_KEYS.BEARER_TOKEN, - ]); - const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN]; - setUserSignedIn(isSignedIn); + ]) + const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN] + setUserSignedIn(isSignedIn) } catch (error) { - console.error("Error checking auth status:", error); - setUserSignedIn(false); + console.error("Error checking auth status:", error) + setUserSignedIn(false) } finally { - setLoading(false); + setLoading(false) } - }; + } const getCurrentTab = async () => { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].url && tabs[0].title) { - setCurrentUrl(tabs[0].url); - setCurrentTitle(tabs[0].title); + setCurrentUrl(tabs[0].url) + setCurrentTitle(tabs[0].title) } } catch (error) { - console.error("Error getting current tab:", error); + console.error("Error getting current tab:", error) } - }; + } - checkAuthStatus(); - getCurrentTab(); - }, []); + checkAuthStatus() + getCurrentTab() + }, []) const handleProjectSelect = (project: Project) => { setDefaultProjectMutation.mutate(project, { onSuccess: () => { - setShowProjectSelector(false); + setShowProjectSelector(false) }, onError: (error) => { - console.error("Error setting default project:", error); + console.error("Error setting default project:", error) }, - }); - }; + }) + } const handleShowProjectSelector = () => { - setShowProjectSelector(true); - }; + setShowProjectSelector(true) + } useEffect(() => { if (!defaultProject && projects.length > 0) { - const firstProject = projects[0]; - setDefaultProjectMutation.mutate(firstProject); + const firstProject = projects[0] + setDefaultProjectMutation.mutate(firstProject) } - }, [defaultProject, projects, setDefaultProjectMutation]); + }, [defaultProject, projects, setDefaultProjectMutation]) const handleSaveCurrentPage = async () => { - setSaving(true); + setSaving(true) + try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true, - }); + }) if (tabs.length > 0 && tabs[0].id) { - await chrome.tabs.sendMessage(tabs[0].id, { - action: "saveMemory", - }); + const response = await chrome.tabs.sendMessage(tabs[0].id, { + action: MESSAGE_TYPES.SAVE_MEMORY, + actionSource: "popup", + }) + + if (response?.success) { + await chrome.tabs.sendMessage(tabs[0].id, { + action: MESSAGE_TYPES.SHOW_TOAST, + state: "success", + }) + } else { + await chrome.tabs.sendMessage(tabs[0].id, { + action: MESSAGE_TYPES.SHOW_TOAST, + state: "error", + }) + } + + window.close() } } catch (error) { - console.error("Failed to save current page:", error); + console.error("Failed to save current page:", error) + + try { + const tabs = await chrome.tabs.query({ + active: true, + currentWindow: true, + }) + if (tabs.length > 0 && tabs[0].id) { + await chrome.tabs.sendMessage(tabs[0].id, { + action: MESSAGE_TYPES.SHOW_TOAST, + state: "error", + }) + } + } catch (toastError) { + console.error("Failed to show error toast:", toastError) + } + + window.close() } finally { - setSaving(false); + setSaving(false) } - }; + } const handleSignOut = async () => { try { - await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]); - setUserSignedIn(false); - queryClient.clear(); + await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]) + await chrome.storage.local.remove([STORAGE_KEYS.USER_DATA]) + await chrome.storage.local.remove([STORAGE_KEYS.DEFAULT_PROJECT]) + setUserSignedIn(false) + queryClient.clear() } catch (error) { - console.error("Error signing out:", error); + console.error("Error signing out:", error) } - }; + } if (loading) { return ( @@ -131,7 +165,7 @@ function App() { <div>Loading...</div> </div> </div> - ); + ) } return ( @@ -267,11 +301,11 @@ function App() { <div className="flex flex-col gap-4"> <div className="flex flex-col gap-2"> <button - className="w-full py-3 px-3 bg-white text-black border border-gray-200 rounded-md text-sm font-medium cursor-pointer flex items-center justify-center transition-colors duration-200 hover:bg-gray-50" + className="w-full py-3 px-3 bg-white text-black border border-gray-200 rounded-md text-sm font-medium cursor-pointer flex items-center justify-start transition-colors duration-200 hover:bg-gray-50" onClick={() => { chrome.tabs.create({ url: "https://chatgpt.com/#settings/Personalization", - }); + }) }} type="button" > @@ -286,17 +320,33 @@ function App() { <title>OpenAI</title> <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" /> </svg> - Import ChatGPT Memories + <div className="text-left"> + <p>Import ChatGPT Memories</p> + <p className="m-0 text-[10px] text-gray-500 leading-tight"> + open 'manage', save your memories to supermemory + </p> + </div> </button> </div> <div className="flex flex-col gap-2"> <button className="w-full py-3 px-3 bg-white text-black border border-gray-200 rounded-md text-sm font-medium cursor-pointer flex items-center justify-center transition-colors duration-200 outline-none appearance-none hover:bg-gray-50 focus:outline-none" - onClick={() => { - chrome.tabs.create({ - url: "https://x.com/i/bookmarks", - }); + onClick={async () => { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }) + + const targetUrl = "https://x.com/i/bookmarks" + + if (activeTab?.url === targetUrl) { + return + } + + await chrome.tabs.create({ + url: targetUrl, + }) }} type="button" > @@ -310,11 +360,13 @@ function App() { <title>X Twitter Logo</title> <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /> </svg> - Import X Bookmarks + <div className="text-left"> + <p>Import X/Twitter Bookmarks</p> + <p className="m-0 text-[10px] text-gray-500 leading-tight"> + Click on supermemory on top right to import bookmarks + </p> + </div> </button> - <p className="m-0 text-xs text-gray-500 leading-tight pl-1"> - Click on supermemory on top right to import bookmarks - </p> </div> </div> </div> @@ -393,7 +445,7 @@ function App() { <button className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700" onClick={() => { - window.open("mailto:[email protected]", "_blank"); + window.open("mailto:[email protected]", "_blank") }} type="button" > @@ -408,7 +460,7 @@ function App() { url: import.meta.env.PROD ? "https://app.supermemory.ai/login" : "http://localhost:3000/login", - }); + }) }} type="button" > @@ -419,7 +471,7 @@ function App() { )} </div> </div> - ); + ) } -export default App; +export default App diff --git a/apps/browser-extension/entrypoints/popup/style.css b/apps/browser-extension/entrypoints/popup/style.css index 684e1ac6..ea67f153 100644 --- a/apps/browser-extension/entrypoints/popup/style.css +++ b/apps/browser-extension/entrypoints/popup/style.css @@ -15,7 +15,6 @@ -webkit-text-size-adjust: 100%; } - @media (prefers-color-scheme: light) { :root { color: #213547; diff --git a/apps/browser-extension/entrypoints/welcome/Welcome.tsx b/apps/browser-extension/entrypoints/welcome/Welcome.tsx index 00bd01cc..9463eba4 100644 --- a/apps/browser-extension/entrypoints/welcome/Welcome.tsx +++ b/apps/browser-extension/entrypoints/welcome/Welcome.tsx @@ -77,7 +77,7 @@ function Welcome() { url: import.meta.env.PROD ? "https://app.supermemory.ai/login" : "http://localhost:3000/login", - }); + }) }} type="button" > @@ -101,7 +101,7 @@ function Welcome() { </div> </div> </div> - ); + ) } -export default Welcome; +export default Welcome diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index 68f6c50a..0644ec24 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -17,6 +17,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.12", "@tanstack/react-query": "^5.85.5", + "posthog-js": "^1.261.7", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.1.12" diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index 2b95c6e2..7e4de310 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -1,27 +1,27 @@ /** * API service for supermemory browser extension */ -import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"; +import { API_ENDPOINTS, STORAGE_KEYS } from "./constants" import { AuthenticationError, type MemoryPayload, type Project, type ProjectsResponse, SupermemoryAPIError, -} from "./types"; +} from "./types" /** * Get bearer token from storage */ async function getBearerToken(): Promise<string> { - const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]); - const token = result[STORAGE_KEYS.BEARER_TOKEN]; + const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]) + const token = result[STORAGE_KEYS.BEARER_TOKEN] if (!token) { - throw new AuthenticationError("Bearer token not found"); + throw new AuthenticationError("Bearer token not found") } - return token; + return token } /** @@ -31,7 +31,7 @@ async function makeAuthenticatedRequest<T>( endpoint: string, options: RequestInit = {}, ): Promise<T> { - const token = await getBearerToken(); + const token = await getBearerToken() const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, { ...options, @@ -41,19 +41,19 @@ async function makeAuthenticatedRequest<T>( "Content-Type": "application/json", ...options.headers, }, - }); + }) if (!response.ok) { if (response.status === 401) { - throw new AuthenticationError("Invalid or expired token"); + throw new AuthenticationError("Invalid or expired token") } throw new SupermemoryAPIError( `API request failed: ${response.statusText}`, response.status, - ); + ) } - return response.json(); + return response.json() } /** @@ -62,11 +62,11 @@ async function makeAuthenticatedRequest<T>( export async function fetchProjects(): Promise<Project[]> { try { const response = - await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects"); - return response.projects; + await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects") + return response.projects } catch (error) { - console.error("Failed to fetch projects:", error); - throw error; + console.error("Failed to fetch projects:", error) + throw error } } @@ -77,11 +77,11 @@ export async function getDefaultProject(): Promise<Project | null> { try { const result = await chrome.storage.local.get([ STORAGE_KEYS.DEFAULT_PROJECT, - ]); - return result[STORAGE_KEYS.DEFAULT_PROJECT] || null; + ]) + return result[STORAGE_KEYS.DEFAULT_PROJECT] || null } catch (error) { - console.error("Failed to get default project:", error); - return null; + console.error("Failed to get default project:", error) + return null } } @@ -92,10 +92,10 @@ export async function setDefaultProject(project: Project): Promise<void> { try { await chrome.storage.local.set({ [STORAGE_KEYS.DEFAULT_PROJECT]: project, - }); + }) } catch (error) { - console.error("Failed to set default project:", error); - throw error; + console.error("Failed to set default project:", error) + throw error } } @@ -107,11 +107,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> { const response = await makeAuthenticatedRequest<unknown>("/v3/memories", { method: "POST", body: JSON.stringify(payload), - }); - return response; + }) + return response } catch (error) { - console.error("Failed to save memory:", error); - throw error; + console.error("Failed to save memory:", error) + throw error } } @@ -123,34 +123,40 @@ export async function searchMemories(query: string): Promise<unknown> { const response = await makeAuthenticatedRequest<unknown>("/v4/search", { method: "POST", body: JSON.stringify({ q: query, include: { relatedMemories: true } }), - }); - return response; + }) + return response } catch (error) { - console.error("Failed to search memories:", error); - throw error; + console.error("Failed to search memories:", error) + throw error } } /** * Save tweet to Supermemory API (specific for Twitter imports) */ -export async function saveTweet( - content: string, - metadata: { sm_source: string; [key: string]: unknown }, - containerTag = "sm_project_twitter_bookmarks", -): Promise<void> { +export async function saveAllTweets( + documents: MemoryPayload[], +): Promise<unknown> { try { - const payload: MemoryPayload = { - containerTags: [containerTag], - content, - metadata, - }; - await saveMemory(payload); + const response = await makeAuthenticatedRequest<unknown>( + "/v3/memories/batch", + { + method: "POST", + body: JSON.stringify({ + documents, + metadata: { + sm_source: "consumer", + sm_internal_group_id: "twitter_bookmarks", + }, + }), + }, + ) + return response } catch (error) { if (error instanceof SupermemoryAPIError && error.statusCode === 409) { // Skip if already exists (409 Conflict) - return; + return } - throw error; + throw error } } diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index 7634759b..5ebd76d1 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -15,6 +15,7 @@ export const API_ENDPOINTS = { */ export const STORAGE_KEYS = { BEARER_TOKEN: "bearer-token", + USER_DATA: "user-data", TOKENS_LOGGED: "tokens-logged", TWITTER_COOKIE: "twitter-cookie", TWITTER_CSRF: "twitter-csrf", @@ -27,10 +28,6 @@ export const STORAGE_KEYS = { */ export const ELEMENT_IDS = { TWITTER_IMPORT_BUTTON: "sm-twitter-import-button", - TWITTER_IMPORT_STATUS: "sm-twitter-import-status", - TWITTER_CLOSE_BTN: "sm-twitter-close-btn", - TWITTER_IMPORT_BTN: "sm-twitter-import-btn", - TWITTER_SIGNIN_BTN: "sm-twitter-signin-btn", SUPERMEMORY_TOAST: "sm-toast", SUPERMEMORY_SAVE_BUTTON: "sm-save-button", SAVE_TWEET_ELEMENT: "sm-save-tweet-element", @@ -83,3 +80,13 @@ export const MESSAGE_TYPES = { export const CONTEXT_MENU_IDS = { SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory", } as const + +export const POSTHOG_EVENT_KEY = { + TWITTER_IMPORT_STARTED: "twitter_import_started", + SAVE_MEMORY_ATTEMPTED: "save_memory_attempted", + SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed", + SOURCE: "extension", + T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched", + CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched", + CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched", +} as const diff --git a/apps/browser-extension/utils/posthog.ts b/apps/browser-extension/utils/posthog.ts new file mode 100644 index 00000000..cdcdbc4e --- /dev/null +++ b/apps/browser-extension/utils/posthog.ts @@ -0,0 +1,74 @@ +import { PostHog } from "posthog-js/dist/module.no-external" +import { STORAGE_KEYS } from "./constants" + +export async function identifyUser(posthog: PostHog): Promise<void> { + const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]) + const userData = stored[STORAGE_KEYS.USER_DATA] + + if (userData?.userId) { + posthog.identify(userData.userId, { + email: userData.email, + name: userData.name, + userId: userData.userId, + }) + } +} + +let posthogInstance: PostHog | null = null +let initializationPromise: Promise<PostHog> | null = null + +export const POSTHOG_CONFIG = { + api_host: "https://api.supermemory.ai/orange", + person_profiles: "identified_only", + disable_external_dependency_loading: true, + persistence: "localStorage", + capture_pageview: false, + autocapture: false, +} as const + +export async function getPostHogInstance(): Promise<PostHog> { + if (posthogInstance) { + return posthogInstance + } + + if (initializationPromise) { + return initializationPromise + } + + initializationPromise = initializePostHog() + return initializationPromise +} + +async function initializePostHog(): Promise<PostHog> { + try { + const posthog = new PostHog() + + if (!import.meta.env.WXT_POSTHOG_API_KEY) { + console.error("PostHog API key not configured") + throw new Error("PostHog API key not configured") + } + + posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG) + + await identifyUser(posthog) + + posthogInstance = posthog + return posthog + } catch (error) { + console.error("Failed to initialize PostHog:", error) + initializationPromise = null + throw error + } +} + +export async function trackEvent( + eventName: string, + properties?: Record<string, unknown>, +): Promise<void> { + try { + const posthog = await getPostHogInstance() + posthog.capture(eventName, properties) + } catch (error) { + console.error(`Failed to track event ${eventName}:`, error) + } +} diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts index c516e094..e68d6dbf 100644 --- a/apps/browser-extension/utils/twitter-import.ts +++ b/apps/browser-extension/utils/twitter-import.ts @@ -3,16 +3,14 @@ * Handles the import process for Twitter bookmarks */ -import { saveTweet } from "./api" +import { saveAllTweets } from "./api" import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth" import { BOOKMARKS_URL, buildRequestVariables, extractNextCursor, getAllTweets, - type Tweet, type TwitterAPIResponse, - tweetToMarkdown, } from "./twitter-utils" export type ImportProgressCallback = (message: string) => Promise<void> @@ -48,31 +46,6 @@ class RateLimiter { } /** - * Imports a single tweet to Supermemory - * @param tweetMd - Tweet content in markdown format - * @param tweet - Original tweet object with metadata - * @returns Promise that resolves when tweet is imported - */ -async function importTweet(tweetMd: string, tweet: Tweet): Promise<void> { - const metadata = { - sm_source: "consumer", - tweet_id: tweet.id_str, - author: tweet.user.screen_name, - created_at: tweet.created_at, - likes: tweet.favorite_count, - retweets: tweet.retweet_count || 0, - } - - try { - await saveTweet(tweetMd, metadata) - } catch (error) { - throw new Error( - `Failed to save tweet: ${error instanceof Error ? error.message : "Unknown error"}`, - ) - } -} - -/** * Main class for handling Twitter bookmarks import */ export class TwitterImporter { @@ -91,9 +64,10 @@ export class TwitterImporter { } this.importInProgress = true + const uniqueGroupId = crypto.randomUUID() try { - await this.batchImportAll("", 0) + await this.batchImportAll("", 0, uniqueGroupId) this.rateLimiter.reset() } catch (error) { await this.config.onError(error as Error) @@ -107,7 +81,7 @@ export class TwitterImporter { * @param cursor - Pagination cursor for Twitter API * @param totalImported - Number of tweets imported so far */ - private async batchImportAll(cursor = "", totalImported = 0): Promise<void> { + private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> { try { // Use a local variable to track imported count let importedCount = totalImported @@ -130,9 +104,6 @@ export class TwitterImporter { ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` : BOOKMARKS_URL - console.log("Making Twitter API request to:", urlWithCursor) - console.log("Request headers:", Object.fromEntries(headers.entries())) - const response = await fetch(urlWithCursor, { method: "GET", headers, @@ -145,7 +116,7 @@ export class TwitterImporter { if (response.status === 429) { await this.rateLimiter.handleRateLimit(this.config.onProgress) - return this.batchImportAll(cursor, totalImported) + return this.batchImportAll(cursor, totalImported, uniqueGroupId) } throw new Error( `Failed to fetch data: ${response.status} - ${errorText}`, @@ -155,21 +126,45 @@ export class TwitterImporter { const data: TwitterAPIResponse = await response.json() const tweets = getAllTweets(data) - console.log("Tweets:", tweets) + const documents: MemoryPayload[] = [] - // Process each tweet + // Convert tweets to MemoryPayload for (const tweet of tweets) { try { - const tweetMd = tweetToMarkdown(tweet) - await importTweet(tweetMd, tweet) + const metadata = { + sm_source: "consumer", + tweet_id: tweet.id_str, + author: tweet.user.screen_name, + created_at: tweet.created_at, + likes: tweet.favorite_count, + retweets: tweet.retweet_count || 0, + sm_internal_group_id: uniqueGroupId, + } + documents.push({ + containerTags: ["sm_project_twitter_bookmarks"], + content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`, + metadata, + customId: tweet.id_str, + }) importedCount++ - await this.config.onProgress(`Imported ${importedCount} tweets`) + await this.config.onProgress(`Imported ${importedCount} tweets, so far...`) } catch (error) { console.error("Error importing tweet:", error) - // Continue with next tweet } } + try { + if (documents.length > 0) { + await saveAllTweets(documents) + } + console.log("Tweets saved") + console.log("Documents:", documents) + } catch (error) { + console.error("Error saving tweets batch:", error) + await this.config.onError(error as Error) + return + } + // Handle pagination const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions @@ -180,7 +175,7 @@ export class TwitterImporter { if (nextCursor && tweets.length > 0) { await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting - await this.batchImportAll(nextCursor, importedCount) + await this.batchImportAll(nextCursor, importedCount, uniqueGroupId) } else { await this.config.onComplete(importedCount) } diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts index 2d0981c8..d20f899e 100644 --- a/apps/browser-extension/utils/types.ts +++ b/apps/browser-extension/utils/types.ts @@ -17,6 +17,7 @@ export interface ExtensionMessage { state?: ToastState importedMessage?: string totalImported?: number + actionSource?: string } /** @@ -32,12 +33,13 @@ export interface MemoryData { * Supermemory API payload for storing memories */ export interface MemoryPayload { - containerTags: string[] + containerTags?: string[] content: string metadata: { sm_source: string [key: string]: unknown } + customId?: string } /** diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 9c060017..29388656 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -3,7 +3,7 @@ * Reusable UI components for the browser extension */ -import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants" +import { ELEMENT_IDS, UI_CONFIG } from "./constants" import type { ToastState } from "./types" /** @@ -158,7 +158,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { color: black; border: none; border-radius: 50px; - padding: 12px 16px; + padding: 10px 12px; cursor: pointer; display: flex; align-items: center; @@ -169,15 +169,16 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { const iconUrl = browser.runtime.getURL("/icon-16.png") button.innerHTML = ` <img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" /> + <span style="font-weight: 500; font-size: 12px;">Import Bookmarks</span> ` button.addEventListener("mouseenter", () => { - button.style.transform = "scale(1.05)" + button.style.opacity = "0.8" button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)" }) button.addEventListener("mouseleave", () => { - button.style.transform = "scale(1)" + button.style.opacity = "1" button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)" }) @@ -187,103 +188,6 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { } /** - * Creates the Twitter import UI dialog - * @param onClose - Close handler - * @param onImport - Import handler - * @param isAuthenticated - Whether user is authenticated - * @returns HTMLElement - The dialog element - */ -export function createTwitterImportUI( - onClose: () => void, - onImport: () => void, - isAuthenticated: boolean, -): HTMLElement { - const container = document.createElement("div") - container.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 2147483647; - background: #ffffff; - border-radius: 12px; - padding: 16px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - min-width: 280px; - max-width: 400px; - border: 1px solid #e1e5e9; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - ` - - container.innerHTML = ` - <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;"> - <div style="display: flex; align-items: center; gap: 8px;"> - <svg width="20" height="20" viewBox="0 0 24 24" fill="#1d9bf0"> - <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/> - </svg> - <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #0f1419;"> - Import Twitter Bookmarks - </h3> - </div> - <button id="${ELEMENT_IDS.TWITTER_CLOSE_BTN}" style="background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; color: #536471;"> - ✕ - </button> - </div> - - ${ - isAuthenticated - ? ` - <div> - <p style="color: #536471; font-size: 14px; margin: 0 0 12px 0; line-height: 1.4;"> - This will import all your Twitter bookmarks to Supermemory - </p> - - <button id="${ELEMENT_IDS.TWITTER_IMPORT_BTN}" style="width: 100%; background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 12px 16px; cursor: pointer; font-size: 14px; font-weight: 500; margin-bottom: 12px;"> - Import All Bookmarks - </button> - - <div id="${ELEMENT_IDS.TWITTER_IMPORT_STATUS}"></div> - </div> - ` - : ` - <div style="text-align: center;"> - <p style="color: #536471; font-size: 14px; margin: 0 0 12px 0;"> - Please sign in to supermemory first - </p> - <button id="${ELEMENT_IDS.TWITTER_SIGNIN_BTN}" style="background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 8px 16px; cursor: pointer; font-size: 14px; font-weight: 500;"> - Sign In - </button> - </div> - ` - } - - <style> - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - </style> - ` - - // Add event listeners - const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`) - closeBtn?.addEventListener("click", onClose) - - const importBtn = container.querySelector( - `#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`, - ) - importBtn?.addEventListener("click", onImport) - - const signinBtn = container.querySelector( - `#${ELEMENT_IDS.TWITTER_SIGNIN_BTN}`, - ) - signinBtn?.addEventListener("click", () => { - browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` }) - }) - - return container -} - -/** * Creates a save tweet element button for Twitter/X * @param onClick - Click handler for the button * @returns HTMLElement - The save button element @@ -510,7 +414,48 @@ export const DOMUtils = { state: ToastState, duration: number = UI_CONFIG.TOAST_DURATION, ): HTMLElement { - // Remove all existing toasts more aggressively + const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST) + + if ((state === "success" || state === "error") && existingToast) { + const icon = existingToast.querySelector("div") + 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;" />` + icon.style.animation = "" + text.textContent = "Added to Memory" + } else if (state === "error") { + icon.innerHTML = ` + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="12" cy="12" r="10" fill="#ef4444"/> + <path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> + <path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> + </svg> + ` + icon.style.animation = "" + text.textContent = + "Failed to save memory / Make sure you are logged in" + } + + // Auto-dismiss + setTimeout(() => { + if (document.body.contains(existingToast)) { + existingToast.style.animation = "fadeOut 0.3s ease-out" + setTimeout(() => { + if (document.body.contains(existingToast)) { + existingToast.remove() + } + }, 300) + } + }, duration) + + return existingToast + } + } + const existingToasts = document.querySelectorAll( `#${ELEMENT_IDS.SUPERMEMORY_TOAST}`, ) diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 0ef00bd2..d655949a 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -1,5 +1,5 @@ -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig, type WxtViteConfig } from "wxt"; +import tailwindcss from "@tailwindcss/vite" +import { defineConfig, type WxtViteConfig } from "wxt" // See https://wxt.dev/api/config.html export default defineConfig({ @@ -12,13 +12,7 @@ export default defineConfig({ name: "supermemory", homepage_url: "https://supermemory.ai", version: "6.0.000", - permissions: [ - "contextMenus", - "storage", - "activeTab", - "webRequest", - "tabs", - ], + permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"], host_permissions: [ "*://x.com/*", "*://twitter.com/*", @@ -26,6 +20,7 @@ export default defineConfig({ "*://api.supermemory.ai/*", "*://chatgpt.com/*", "*://chat.openai.com/*", + "https://*.posthog.com/*", ], web_accessible_resources: [ { @@ -37,4 +32,4 @@ export default defineConfig({ webExt: { disabled: true, }, -}); +}) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 6bf8cf6f..f66327e1 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -19,7 +19,6 @@ import { } from "lucide-react" import { AnimatePresence, motion } from "motion/react" import Link from "next/link" -import { useRouter } from "next/navigation" import { useCallback, useEffect, useMemo, useState } from "react" import type { z } from "zod" import { ConnectAIModal } from "@/components/connect-ai-modal" @@ -741,21 +740,30 @@ const MemoryGraphPage = () => { // Wrapper component to handle auth and waitlist checks export default function Page() { - const router = useRouter() - const { user } = useAuth() + const { user, session } = useAuth() useEffect(() => { - // save the token for chrome extension const url = new URL(window.location.href) - const rawToken = url.searchParams.get("token") + const authenticateChromeExtension = url.searchParams.get( + "extension-auth-success", + ) + + if (authenticateChromeExtension) { + const sessionToken = session?.token + const userData = { + email: user?.email, + name: user?.name, + userId: user?.id, + } - if (rawToken) { - const encodedToken = encodeURIComponent(rawToken) - window.postMessage({ token: encodedToken }, "*") - url.searchParams.delete("token") - window.history.replaceState({}, "", url.toString()) + if (sessionToken && userData?.email) { + const encodedToken = encodeURIComponent(sessionToken) + window.postMessage({ token: encodedToken, userData }, "*") + url.searchParams.delete("extension-auth-success") + window.history.replaceState({}, "", url.toString()) + } } - }, []) + }, [user, session]) // Show loading state while checking authentication and waitlist status if (!user) { @@ -54,6 +54,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.12", "@tanstack/react-query": "^5.85.5", + "posthog-js": "^1.261.7", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.1.12", @@ -4810,6 +4811,8 @@ "strip-literal/js-tokens": ["[email protected]", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "supermemory-browser-extension/posthog-js": ["[email protected]", "", { "dependencies": { "@posthog/core": "1.0.2", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-Fjpbz6VfIMsEbKIN/UyTWhU1DGgVIngqoRjPGRolemIMOVzTfI77OZq8WwiBhMug+rU+wNhGCQhC41qRlR5CxA=="], + "supermemory-browser-extension/typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "tempy/is-stream": ["[email protected]", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], |