aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2025-09-08 05:19:10 +0000
committerMaheshtheDev <[email protected]>2025-09-08 05:19:10 +0000
commit3e5ea2fd9ed210644ae29b013b579703e30986de (patch)
tree7de64cf7bddca72cf56e89ee4320005dad9f2ac5
parentfix: billing page (#416) (diff)
downloadsupermemory-3e5ea2fd9ed210644ae29b013b579703e30986de.tar.xz
supermemory-3e5ea2fd9ed210644ae29b013b579703e30986de.zip
extension: updated telemetry and batch upload (#415)
-rw-r--r--apps/browser-extension/.env.example2
-rw-r--r--apps/browser-extension/entrypoints/background.ts130
-rw-r--r--apps/browser-extension/entrypoints/content.ts565
-rw-r--r--apps/browser-extension/entrypoints/popup/App.tsx194
-rw-r--r--apps/browser-extension/entrypoints/popup/style.css1
-rw-r--r--apps/browser-extension/entrypoints/welcome/Welcome.tsx6
-rw-r--r--apps/browser-extension/package.json1
-rw-r--r--apps/browser-extension/utils/api.ts92
-rw-r--r--apps/browser-extension/utils/constants.ts15
-rw-r--r--apps/browser-extension/utils/posthog.ts74
-rw-r--r--apps/browser-extension/utils/twitter-import.ts77
-rw-r--r--apps/browser-extension/utils/types.ts4
-rw-r--r--apps/browser-extension/utils/ui-components.ts149
-rw-r--r--apps/browser-extension/wxt.config.ts15
-rw-r--r--apps/web/app/page.tsx30
-rw-r--r--bun.lock3
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) {
diff --git a/bun.lock b/bun.lock
index 00818471..7ece9abc 100644
--- a/bun.lock
+++ b/bun.lock
@@ -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=="],