mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-13 02:24:34 +00:00
extension: updated telemetry and batch upload (#415)
This commit is contained in:
parent
99fd62d5f1
commit
3e5ea2fd9e
16 changed files with 706 additions and 652 deletions
2
apps/browser-extension/.env.example
Normal file
2
apps/browser-extension/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# PostHog Configuration
|
||||
WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here
|
||||
|
|
@ -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);
|
||||
return { success: true, data: responseData };
|
||||
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,
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
return
|
||||
}
|
||||
|
||||
const button = createTwitterImportButton(() => {
|
||||
showTwitterImportUI();
|
||||
});
|
||||
const button = createTwitterImportButton(async () => {
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
})
|
||||
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
|
||||
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error starting import:", error)
|
||||
}
|
||||
})
|
||||
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(button)
|
||||
}
|
||||
|
||||
function showTwitterImportUI() {
|
||||
if (twitterImportUI) {
|
||||
twitterImportUI.remove();
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
document.body.appendChild(twitterImportUI);
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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:dhravya@supermemory.com", "_blank");
|
||||
window.open("mailto:dhravya@supermemory.com", "_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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
74
apps/browser-extension/utils/posthog.ts
Normal file
74
apps/browser-extension/utils/posthog.ts
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -47,31 +45,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
|
||||
*/
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
})
|
||||
|
||||
|
|
@ -186,103 +187,6 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
|||
return button
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -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}`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 (rawToken) {
|
||||
const encodedToken = encodeURIComponent(rawToken)
|
||||
window.postMessage({ token: encodedToken }, "*")
|
||||
url.searchParams.delete("token")
|
||||
window.history.replaceState({}, "", url.toString())
|
||||
if (authenticateChromeExtension) {
|
||||
const sessionToken = session?.token
|
||||
const userData = {
|
||||
email: user?.email,
|
||||
name: user?.name,
|
||||
userId: user?.id,
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
3
bun.lock
3
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": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"supermemory-browser-extension/posthog-js": ["posthog-js@1.261.7", "", { "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": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"tempy/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue