feat (extension) : Auto Search Toggle for Chat Applications (#418)

Adds user-controlled auto search functionality for memories across ChatGPT, Claude, and T3.chat with a settings toggle in the extension popup.

  Changes

  - Settings UI: Added new "Settings" tab in popup with toggle to enable/disable auto search
  - Auto Search: Automatically searches user memories while typing in chat apps (disabled by default)
  - Chat Integration: Supports ChatGPT, Claude, and T3.chat with consistent behavior
  - User Control: Users can enable/disable auto search via Settings tab
  - Storage: Added AUTO_SEARCH_ENABLED storage key with default value false

  Features

  -  Auto search memories while typing (when enabled)
  -  Manual search always available via supermemory icons
  -  Works across all supported chat platforms
  -  Real-time toggle without requiring page refresh
This commit is contained in:
MaheshtheDev 2025-09-10 03:55:53 +00:00
parent 96f1a97fd6
commit d7cc7d847d
15 changed files with 2641 additions and 696 deletions

View file

@ -23,7 +23,7 @@ export default defineBackground(() => {
browser.runtime.onInstalled.addListener(async (details) => {
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
title: "Save to supermemory",
title: "sync to supermemory",
contexts: ["selection", "page", "link"],
})
@ -114,7 +114,9 @@ export default defineBackground(() => {
const payload: MemoryPayload = {
containerTags: [containerTag],
content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
content:
data.content ||
`${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
metadata: { sm_source: "consumer" },
}
@ -144,9 +146,9 @@ export default defineBackground(() => {
const response = responseData as {
results?: Array<{ memory?: string }>
}
let memories = ""
const memories: string[] = []
response.results?.forEach((result, index) => {
memories += `[${index + 1}] ${result.memory} `
memories.push(`${index + 1}. ${result.memory} \n`)
})
console.log("Memories:", memories)
await trackEvent(eventSource)
@ -216,6 +218,37 @@ export default defineBackground(() => {
})()
return true
}
if (message.action === MESSAGE_TYPES.CAPTURE_PROMPT) {
;(async () => {
try {
const messageData = message.data as {
prompt: string
platform: string
source: string
}
console.log("=== PROMPT CAPTURED ===")
console.log(messageData)
console.log("========================")
const memoryData: MemoryData = {
content: messageData.prompt,
}
const result = await saveMemoryToSupermemory(
memoryData,
`prompt_capture_${messageData.platform}`,
)
sendResponse(result)
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
})
}
})()
return true
}
},
)
})

View file

@ -1,674 +0,0 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
STORAGE_KEYS,
} from "../utils/constants"
import { trackEvent } from "../utils/posthog"
import {
createChatGPTInputBarElement,
createClaudeInputBarElement,
createT3InputBarElement,
createTwitterImportButton,
DOMUtils,
} from "../utils/ui-components"
export default defineContentScript({
matches: ["<all_urls>"],
main() {
browser.runtime.onMessage.addListener(async (message) => {
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
await saveMemory()
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
updateTwitterImportUI(message)
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
updateTwitterImportUI(message)
}
})
const observeForMemoriesDialog = () => {
const observer = new MutationObserver(() => {
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
}
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
addSupermemoryIconToClaudeInput()
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
addSupermemoryIconToT3Input()
}
if (
DOMUtils.isOnDomain(DOMAINS.TWITTER) &&
window.location.pathname === "/i/bookmarks"
) {
addTwitterImportButton()
} else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
}
if (
DOMUtils.isOnDomain(DOMAINS.TWITTER) &&
window.location.pathname === "/i/bookmarks"
) {
setTimeout(() => {
addTwitterImportButton() // Wait 2 seconds for page to load
//addSaveTweetElement();
}, 2000)
} else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
}
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
setTimeout(() => {
addSupermemoryIconToClaudeInput() // Wait 2 seconds for Claude page to load
}, 2000)
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
setTimeout(() => {
addSupermemoryIconToT3Input() // Wait 2 seconds for T3 page to load
}, 2000)
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog)
} else {
observeForMemoriesDialog()
}
async function saveMemory() {
try {
DOMUtils.showToast("loading")
const highlightedText = window.getSelection()?.toString() || ""
const url = window.location.href
const html = document.documentElement.outerHTML
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html,
highlightedText,
url,
},
actionSource: "context_menu",
})
console.log("Response from enxtension:", response)
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memory:", error)
DOMUtils.showToast("error")
}
}
async function getRelatedMemories(actionSource: string) {
try {
const userQuery =
document.getElementById("prompt-textarea")?.textContent || ""
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
})
if (response.success && response.data) {
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
const currentContent = promptElement.innerHTML
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
}
}
} catch (error) {
console.error("Error getting related memories:", error)
}
}
function addSupermemoryButtonToMemoriesDialog() {
const dialogs = document.querySelectorAll('[role="dialog"]')
let memoriesDialog: HTMLElement | null = null
for (const dialog of dialogs) {
const headerText = dialog.querySelector("h2")
if (headerText?.textContent?.includes("Saved memories")) {
memoriesDialog = dialog as HTMLElement
break
}
}
if (!memoriesDialog) return
if (memoriesDialog.querySelector("#supermemory-save-button")) return
const deleteAllContainer = memoriesDialog.querySelector(
".mt-5.flex.justify-end",
)
if (!deleteAllContainer) return
const supermemoryButton = document.createElement("button")
supermemoryButton.id = "supermemory-save-button"
supermemoryButton.className = "btn relative btn-primary-outline mr-2"
const iconUrl = browser.runtime.getURL("/icon-16.png")
supermemoryButton.innerHTML = `
<div class="flex items-center justify-center gap-2">
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
Save to supermemory
</div>
`
supermemoryButton.style.cssText = `
background: #1C2026 !important;
color: white !important;
border: 1px solid #1C2026 !important;
border-radius: 9999px !important;
padding: 10px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
margin-right: 8px !important;
cursor: pointer !important;
`
supermemoryButton.addEventListener("mouseenter", () => {
supermemoryButton.style.backgroundColor = "#2B2E33"
})
supermemoryButton.addEventListener("mouseleave", () => {
supermemoryButton.style.backgroundColor = "#1C2026"
})
supermemoryButton.addEventListener("click", async () => {
await saveMemoriesToSupermemory()
})
deleteAllContainer.insertBefore(
supermemoryButton,
deleteAllContainer.firstChild,
)
}
async function saveMemoriesToSupermemory() {
try {
DOMUtils.showToast("loading")
const memoriesTable = document.querySelector(
'[role="dialog"] table tbody',
)
if (!memoriesTable) {
DOMUtils.showToast("error")
return
}
const memoryRows = memoriesTable.querySelectorAll("tr")
const memories: string[] = []
memoryRows.forEach((row) => {
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
if (memoryCell?.textContent) {
memories.push(memoryCell.textContent.trim())
}
})
console.log("Memories:", memories)
if (memories.length === 0) {
DOMUtils.showToast("error")
return
}
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html: combinedContent,
},
actionSource: "chatgpt_memories_dialog",
})
console.log({ response })
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memories to supermemory:", error)
DOMUtils.showToast("error")
}
}
function addTwitterImportButton() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
return
}
// Only show the import button on the bookmarks page
if (window.location.pathname !== "/i/bookmarks") {
return
}
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
return
}
const button = createTwitterImportButton(async () => {
try {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
})
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
})
} catch (error) {
console.error("Error starting import:", error)
}
})
document.body.appendChild(button)
}
function updateTwitterImportUI(message: {
type: string
importedMessage?: string
totalImported?: number
}) {
const importButton = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
if (!importButton) return
const iconUrl = browser.runtime.getURL("/icon-16.png")
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span>
`
importButton.style.cursor = "default"
}
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px; color: #059669;"> Imported ${message.totalImported} tweets!</span>
`
setTimeout(() => {
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span>
`
importButton.style.cursor = "pointer"
}, 3000)
}
}
function addSaveChatGPTElementBeforeComposerBtn() {
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
return
}
const composerButtons = document.querySelectorAll("button.composer-btn")
composerButtons.forEach((button) => {
if (button.hasAttribute("data-supermemory-icon-added-before")) {
return
}
const parent = button.parentElement
if (!parent) return
const parentSiblings = parent.parentElement?.children
if (!parentSiblings) return
let hasSpeechButtonSibling = false
for (const sibling of parentSiblings) {
if (
sibling.getAttribute("data-testid") ===
"composer-speech-button-container"
) {
hasSpeechButtonSibling = true
break
}
}
if (!hasSpeechButtonSibling) return
const grandParent = parent.parentElement
if (!grandParent) return
const existingIcon = grandParent.querySelector(
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
)
if (existingIcon) {
button.setAttribute("data-supermemory-icon-added-before", "true")
return
}
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
await getRelatedMemories(
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
)
})
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
button.setAttribute("data-supermemory-icon-added-before", "true")
grandParent.insertBefore(saveChatGPTElement, parent)
})
}
function addSupermemoryIconToClaudeInput() {
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
return
}
const targetContainers = document.querySelectorAll(
".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createClaudeInputBarElement(async () => {
await getRelatedMemoriesForClaude()
})
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForClaude() {
try {
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (supermemoryContainer?.parentElement?.previousElementSibling) {
const pTag =
supermemoryContainer.parentElement.previousElementSibling.querySelector(
"p",
)
userQuery = pTag?.innerText || pTag?.textContent || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const inputElements = document.querySelectorAll(
'div[contenteditable="true"], textarea, input[type="text"]',
)
for (const element of inputElements) {
const text =
(element as HTMLElement).innerText ||
(element as HTMLInputElement).value
if (text?.trim()) {
userQuery = text.trim()
break
}
}
}
console.log("Claude query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found")
DOMUtils.showToast("error")
return
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
})
console.log("Claude memories response:", response)
if (response.success && response.data) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (textareaElement) {
const currentContent = textareaElement.innerHTML
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
textareaElement.dispatchEvent(new Event("input", { bubbles: true }))
} else {
console.log("Could not find Claude input area")
}
} else {
console.log(
"Failed to get memories:",
response.error || "Unknown error",
)
}
} catch (error) {
console.error("Error getting related memories for Claude:", error)
}
}
function addSupermemoryIconToT3Input() {
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
return
}
const targetContainers = document.querySelectorAll(
".flex.min-w-0.items-center.gap-2.overflow-hidden",
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createT3InputBarElement(async () => {
await getRelatedMemoriesForT3()
})
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForT3() {
try {
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (
supermemoryContainer?.parentElement?.parentElement
?.previousElementSibling
) {
const textareaElement =
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
"textarea",
)
userQuery = textareaElement?.value || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const textareas = document.querySelectorAll("textarea")
for (const textarea of textareas) {
const text = (textarea as HTMLTextAreaElement).value
if (text?.trim()) {
userQuery = text.trim()
break
}
}
}
console.log("T3 query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found")
return
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED,
})
console.log("T3 memories response:", response)
if (response.success && response.data) {
let textareaElement = null
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (
supermemoryContainer?.parentElement?.parentElement
?.previousElementSibling
) {
textareaElement =
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
"textarea",
)
}
if (!textareaElement) {
textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
}
if (textareaElement) {
if (textareaElement.tagName === "TEXTAREA") {
const currentContent = (textareaElement as HTMLTextAreaElement)
.value
;(textareaElement as HTMLTextAreaElement).value =
`${currentContent}\n\nSupermemories: ${response.data}`
} else {
const currentContent = textareaElement.innerHTML
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
}
textareaElement.dispatchEvent(new Event("input", { bubbles: true }))
} else {
console.log("Could not find T3 input area")
}
} else {
console.log(
"Failed to get memories:",
response.error || "Unknown error",
)
}
} catch (error) {
console.error("Error getting related memories for T3:", error)
}
}
document.addEventListener("keydown", async (event) => {
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "m"
) {
event.preventDefault()
await saveMemory()
}
})
window.addEventListener("message", (event) => {
if (event.source !== window) {
return
}
const bearerToken = event.data.token
const userData = event.data.userData
if (bearerToken && userData) {
if (
!(
window.location.hostname === "localhost" ||
window.location.hostname === "supermemory.ai" ||
window.location.hostname === "app.supermemory.ai"
)
) {
console.log(
"Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
)
return
}
chrome.storage.local.set(
{
[STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
[STORAGE_KEYS.USER_DATA]: userData,
},
() => {},
)
}
})
},
})

View file

@ -0,0 +1,724 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
STORAGE_KEYS,
UI_CONFIG,
} from "../../utils/constants"
import {
createChatGPTInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
let chatGPTDebounceTimeout: NodeJS.Timeout | null = null
let chatGPTRouteObserver: MutationObserver | null = null
let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null
let chatGPTObserverThrottle: NodeJS.Timeout | null = null
export function initializeChatGPT() {
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
return
}
if (document.body.hasAttribute("data-chatgpt-initialized")) {
return
}
setTimeout(() => {
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
}, 2000)
setupChatGPTPromptCapture()
setupChatGPTRouteChangeDetection()
document.body.setAttribute("data-chatgpt-initialized", "true")
}
function setupChatGPTRouteChangeDetection() {
if (chatGPTRouteObserver) {
chatGPTRouteObserver.disconnect()
}
if (chatGPTUrlCheckInterval) {
clearInterval(chatGPTUrlCheckInterval)
}
if (chatGPTObserverThrottle) {
clearTimeout(chatGPTObserverThrottle)
chatGPTObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("ChatGPT route changed, re-adding supermemory elements")
setTimeout(() => {
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
}, 1000)
}
}
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000)
chatGPTRouteObserver = new MutationObserver((mutations) => {
if (chatGPTObserverThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.querySelector?.("#prompt-textarea") ||
element.querySelector?.("button.composer-btn") ||
element.querySelector?.('[role="dialog"]') ||
element.matches?.("#prompt-textarea") ||
element.id === "prompt-textarea"
) {
shouldRecheck = true
}
}
})
}
})
if (shouldRecheck) {
chatGPTObserverThrottle = setTimeout(() => {
try {
chatGPTObserverThrottle = null
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
setupChatGPTAutoFetch()
} catch (error) {
console.error("Error in ChatGPT observer callback:", error)
}
}, 300)
}
})
try {
chatGPTRouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up ChatGPT route observer:", error)
if (chatGPTUrlCheckInterval) {
clearInterval(chatGPTUrlCheckInterval)
}
chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
async function getRelatedMemoriesForChatGPT(actionSource: string) {
try {
const userQuery =
document.getElementById("prompt-textarea")?.textContent || ""
const icon = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)[0]
const iconElement = icon as HTMLElement
if (!iconElement) {
console.warn("ChatGPT icon element not found, cannot update feedback")
return
}
updateChatGPTIconFeedback("Searching memories...", iconElement)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Memory search timeout")),
UI_CONFIG.API_REQUEST_TIMEOUT,
),
)
const response = await Promise.race([
browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
}),
timeoutPromise,
])
if (response?.success && response?.data) {
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`
console.log(
"Prompt element dataset:",
promptElement.dataset.supermemories,
)
iconElement.dataset.memoriesData = response.data
updateChatGPTIconFeedback("Included Memories", iconElement)
} else {
console.warn(
"ChatGPT prompt element not found after successful memory fetch",
)
updateChatGPTIconFeedback("Memories found", iconElement)
}
} else {
console.warn("No memories found or API response invalid")
updateChatGPTIconFeedback("No memories found", iconElement)
}
} catch (error) {
console.error("Error getting related memories:", error)
try {
const icon = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)[0] as HTMLElement
if (icon) {
updateChatGPTIconFeedback("Error fetching memories", icon)
}
} catch (feedbackError) {
console.error("Failed to update error feedback:", feedbackError)
}
}
}
function addSupermemoryButtonToMemoriesDialog() {
const dialogs = document.querySelectorAll('[role="dialog"]')
let memoriesDialog: HTMLElement | null = null
for (const dialog of dialogs) {
const headerText = dialog.querySelector("h2")
if (headerText?.textContent?.includes("Saved memories")) {
memoriesDialog = dialog as HTMLElement
break
}
}
if (!memoriesDialog) return
if (memoriesDialog.querySelector("#supermemory-save-button")) return
const deleteAllContainer = memoriesDialog.querySelector(
".mt-5.flex.justify-end",
)
if (!deleteAllContainer) return
const supermemoryButton = document.createElement("button")
supermemoryButton.id = "supermemory-save-button"
supermemoryButton.className = "btn relative btn-primary-outline mr-2"
const iconUrl = browser.runtime.getURL("/icon-16.png")
supermemoryButton.innerHTML = `
<div class="flex items-center justify-center gap-2">
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
Save to supermemory
</div>
`
supermemoryButton.style.cssText = `
background: #1C2026 !important;
color: white !important;
border: 1px solid #1C2026 !important;
border-radius: 9999px !important;
padding: 10px 16px !important;
font-weight: 500 !important;
font-size: 14px !important;
margin-right: 8px !important;
cursor: pointer !important;
`
supermemoryButton.addEventListener("mouseenter", () => {
supermemoryButton.style.backgroundColor = "#2B2E33"
})
supermemoryButton.addEventListener("mouseleave", () => {
supermemoryButton.style.backgroundColor = "#1C2026"
})
supermemoryButton.addEventListener("click", async () => {
await saveMemoriesToSupermemory()
})
deleteAllContainer.insertBefore(
supermemoryButton,
deleteAllContainer.firstChild,
)
}
async function saveMemoriesToSupermemory() {
try {
DOMUtils.showToast("loading")
const memoriesTable = document.querySelector('[role="dialog"] table tbody')
if (!memoriesTable) {
DOMUtils.showToast("error")
return
}
const memoryRows = memoriesTable.querySelectorAll("tr")
const memories: string[] = []
memoryRows.forEach((row) => {
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
if (memoryCell?.textContent) {
memories.push(memoryCell.textContent.trim())
}
})
console.log("Memories:", memories)
if (memories.length === 0) {
DOMUtils.showToast("error")
return
}
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html: combinedContent,
},
actionSource: "chatgpt_memories_dialog",
})
console.log({ response })
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memories to supermemory:", error)
DOMUtils.showToast("error")
}
}
function updateChatGPTIconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #513EA9;
border-radius: 12px;
color: white;
font-size: 12px;
font-weight: 500;
cursor: ${message === "Included Memories" ? "pointer" : "default"};
position: relative;
`
feedbackDiv.innerHTML = `
<span></span>
<span>${message}</span>
`
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
border: 1px solid #333;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-weight: 600; color: #fff;">Included Memories</span>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 0;
max-height: 300px;
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
console.log("Memories text:", memoriesText)
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
memoryItem.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 10px;
font-size: 13px;
line-height: 1.4;
`
const memoryText = document.createElement("div")
memoryText.style.cssText = `
flex: 1;
color: #e5e5e5;
`
memoryText.textContent = memory.trim()
const removeBtn = document.createElement("button")
removeBtn.style.cssText = `
background: transparent;
color: #9ca3af;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
removeBtn.dataset.memoryIndex = index.toString()
removeBtn.addEventListener("mouseenter", () => {
removeBtn.style.color = "#ef4444"
})
removeBtn.addEventListener("mouseleave", () => {
removeBtn.style.color = "#9ca3af"
})
memoryItem.appendChild(memoryText)
memoryItem.appendChild(removeBtn)
content.appendChild(memoryItem)
})
popup.appendChild(header)
popup.appendChild(content)
document.body.appendChild(popup)
feedbackDiv.addEventListener("mouseenter", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Click to see memories"
}
})
feedbackDiv.addEventListener("mouseleave", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Included Memories"
}
})
feedbackDiv.addEventListener("click", (e) => {
e.stopPropagation()
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
const htmlButton = button as HTMLButtonElement
htmlButton.addEventListener("click", () => {
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
const memoryItem = htmlButton.parentElement
if (memoryItem) {
content.removeChild(memoryItem)
}
const currentMemories = (iconElement.dataset.memoriesData || "")
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
currentMemories.splice(index, 1)
const updatedMemories = currentMemories.join(" ,")
iconElement.dataset.memoriesData = updatedMemories
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
promptElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
})
if (currentMemories.length <= 1) {
if (promptElement?.dataset.supermemories) {
delete promptElement.dataset.supermemories
delete iconElement.dataset.memoriesData
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}
popup.style.display = "none"
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}
})
})
setTimeout(() => {
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}, 300000)
}
iconElement.innerHTML = ""
iconElement.appendChild(feedbackDiv)
if (resetAfter > 0) {
setTimeout(() => {
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}, resetAfter)
}
}
function addSaveChatGPTElementBeforeComposerBtn() {
const composerButtons = document.querySelectorAll("button.composer-btn")
composerButtons.forEach((button) => {
if (button.hasAttribute("data-supermemory-icon-added-before")) {
return
}
const parent = button.parentElement
if (!parent) return
const parentSiblings = parent.parentElement?.children
if (!parentSiblings) return
let hasSpeechButtonSibling = false
for (const sibling of parentSiblings) {
if (
sibling.getAttribute("data-testid") ===
"composer-speech-button-container"
) {
hasSpeechButtonSibling = true
break
}
}
if (!hasSpeechButtonSibling) return
const grandParent = parent.parentElement
if (!grandParent) return
const existingIcon = grandParent.querySelector(
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
)
if (existingIcon) {
button.setAttribute("data-supermemory-icon-added-before", "true")
return
}
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
await getRelatedMemoriesForChatGPT(
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
)
})
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
button.setAttribute("data-supermemory-icon-added-before", "true")
grandParent.insertBefore(saveChatGPTElement, parent)
setupChatGPTAutoFetch()
})
}
async function setupChatGPTAutoFetch() {
const result = await chrome.storage.local.get([
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
])
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
if (!autoSearchEnabled) {
return
}
const promptTextarea = document.getElementById("prompt-textarea")
if (
!promptTextarea ||
promptTextarea.hasAttribute("data-supermemory-auto-fetch")
) {
return
}
promptTextarea.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
if (chatGPTDebounceTimeout) {
clearTimeout(chatGPTDebounceTimeout)
}
chatGPTDebounceTimeout = setTimeout(async () => {
const content = promptTextarea.textContent?.trim() || ""
if (content.length > 2) {
await getRelatedMemoriesForChatGPT(
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (promptTextarea.dataset.supermemories) {
delete promptTextarea.dataset.supermemories
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
promptTextarea.addEventListener("input", handleInput)
}
function setupChatGPTPromptCapture() {
if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true")
const capturePromptContent = async (source: string) => {
const promptTextarea = document.getElementById("prompt-textarea")
let promptContent = ""
if (promptTextarea) {
promptContent = promptTextarea.textContent || ""
}
const storedMemories = promptTextarea?.dataset.supermemories
if (
storedMemories &&
promptTextarea &&
!promptContent.includes("Supermemories of user")
) {
promptTextarea.innerHTML = `${promptTextarea.innerHTML} ${storedMemories}`
promptContent = promptTextarea.textContent || ""
}
if (promptTextarea && promptContent.trim()) {
console.log(`ChatGPT prompt submitted via ${source}:`, promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "chatgpt",
source: source,
},
})
} catch (error) {
console.error("Error sending ChatGPT prompt to background:", error)
}
}
const icons = document.querySelectorAll(
'[id*="sm-chatgpt-input-bar-element-before-composer"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (promptTextarea?.dataset.supermemories) {
delete promptTextarea.dataset.supermemories
}
}
document.addEventListener(
"click",
async (event) => {
const target = event.target as HTMLElement
if (
target.id === "composer-submit-button" ||
target.closest("#composer-submit-button")
) {
await capturePromptContent("button click")
}
},
true,
)
document.addEventListener(
"keydown",
async (event) => {
const target = event.target as HTMLElement
if (
target.id === "prompt-textarea" &&
event.key === "Enter" &&
!event.shiftKey
) {
await capturePromptContent("Enter key")
}
},
true,
)
}

View file

@ -0,0 +1,648 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
STORAGE_KEYS,
UI_CONFIG,
} from "../../utils/constants"
import {
createClaudeInputBarElement,
DOMUtils,
} from "../../utils/ui-components"
let claudeDebounceTimeout: NodeJS.Timeout | null = null
let claudeRouteObserver: MutationObserver | null = null
let claudeUrlCheckInterval: NodeJS.Timeout | null = null
let claudeObserverThrottle: NodeJS.Timeout | null = null
export function initializeClaude() {
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
return
}
if (document.body.hasAttribute("data-claude-initialized")) {
return
}
setTimeout(() => {
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
}, 2000)
setupClaudePromptCapture()
setupClaudeRouteChangeDetection()
document.body.setAttribute("data-claude-initialized", "true")
}
function setupClaudeRouteChangeDetection() {
if (claudeRouteObserver) {
claudeRouteObserver.disconnect()
}
if (claudeUrlCheckInterval) {
clearInterval(claudeUrlCheckInterval)
}
if (claudeObserverThrottle) {
clearTimeout(claudeObserverThrottle)
claudeObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("Claude route changed, re-adding supermemory icon")
setTimeout(() => {
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
}, 1000)
}
}
claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000)
claudeRouteObserver = new MutationObserver((mutations) => {
if (claudeObserverThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.querySelector?.('div[contenteditable="true"]') ||
element.querySelector?.("textarea") ||
element.matches?.('div[contenteditable="true"]') ||
element.matches?.("textarea")
) {
shouldRecheck = true
}
}
})
}
})
if (shouldRecheck) {
claudeObserverThrottle = setTimeout(() => {
try {
claudeObserverThrottle = null
addSupermemoryIconToClaudeInput()
setupClaudeAutoFetch()
} catch (error) {
console.error("Error in Claude observer callback:", error)
}
}, 300)
}
})
try {
claudeRouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up Claude route observer:", error)
if (claudeUrlCheckInterval) {
clearInterval(claudeUrlCheckInterval)
}
claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
function addSupermemoryIconToClaudeInput() {
const targetContainers = document.querySelectorAll(
".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createClaudeInputBarElement(async () => {
await getRelatedMemoriesForClaude(
POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
)
})
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForClaude(actionSource: string) {
try {
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (supermemoryContainer?.parentElement?.previousElementSibling) {
const pTag =
supermemoryContainer.parentElement.previousElementSibling.querySelector(
"p",
)
userQuery = pTag?.innerText || pTag?.textContent || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const inputElements = document.querySelectorAll(
'div[contenteditable="true"], textarea, input[type="text"]',
)
for (const element of inputElements) {
const text =
(element as HTMLElement).innerText ||
(element as HTMLInputElement).value
if (text?.trim()) {
userQuery = text.trim()
break
}
}
}
console.log("Claude query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found for Claude")
return
}
const icon = document.querySelector('[id*="sm-claude-input-bar-element"]')
const iconElement = icon as HTMLElement
if (!iconElement) {
console.warn("Claude icon element not found, cannot update feedback")
return
}
updateClaudeIconFeedback("Searching memories...", iconElement)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Memory search timeout")),
UI_CONFIG.API_REQUEST_TIMEOUT,
),
)
const response = await Promise.race([
browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
}),
timeoutPromise,
])
console.log("Claude memories response:", response)
if (response?.success && response?.data) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (textareaElement) {
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${response.data}</div>`
console.log(
"Text element dataset:",
textareaElement.dataset.supermemories,
)
iconElement.dataset.memoriesData = response.data
updateClaudeIconFeedback("Included Memories", iconElement)
} else {
console.warn(
"Claude input area not found after successful memory fetch",
)
updateClaudeIconFeedback("Memories found", iconElement)
}
} else {
console.warn("No memories found or API response invalid for Claude")
updateClaudeIconFeedback("No memories found", iconElement)
}
} catch (error) {
console.error("Error getting related memories for Claude:", error)
try {
const icon = document.querySelector(
'[id*="sm-claude-input-bar-element"]',
) as HTMLElement
if (icon) {
updateClaudeIconFeedback("Error fetching memories", icon)
}
} catch (feedbackError) {
console.error("Failed to update Claude error feedback:", feedbackError)
}
}
}
function updateClaudeIconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: #513EA9;
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 500;
cursor: ${message === "Included Memories" ? "pointer" : "default"};
position: relative;
`
feedbackDiv.innerHTML = `
<span></span>
<span>${message}</span>
`
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
border: 1px solid #333;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-weight: 600; color: #fff;">Included Memories</span>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 0;
max-height: 300px;
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
console.log("Memories text:", memoriesText)
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
memoryItem.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 10px;
font-size: 13px;
line-height: 1.4;
`
const memoryText = document.createElement("div")
memoryText.style.cssText = `
flex: 1;
color: #e5e5e5;
`
memoryText.textContent = memory.trim()
const removeBtn = document.createElement("button")
removeBtn.style.cssText = `
background: transparent;
color: #9ca3af;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
removeBtn.dataset.memoryIndex = index.toString()
removeBtn.addEventListener("mouseenter", () => {
removeBtn.style.color = "#ef4444"
})
removeBtn.addEventListener("mouseleave", () => {
removeBtn.style.color = "#9ca3af"
})
memoryItem.appendChild(memoryText)
memoryItem.appendChild(removeBtn)
content.appendChild(memoryItem)
})
popup.appendChild(header)
popup.appendChild(content)
document.body.appendChild(popup)
feedbackDiv.addEventListener("mouseenter", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Click to see memories"
}
})
feedbackDiv.addEventListener("mouseleave", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Included Memories"
}
})
feedbackDiv.addEventListener("click", (e) => {
e.stopPropagation()
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
const htmlButton = button as HTMLButtonElement
htmlButton.addEventListener("click", () => {
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
const memoryItem = htmlButton.parentElement
if (memoryItem) {
content.removeChild(memoryItem)
}
const currentMemories = (iconElement.dataset.memoriesData || "")
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
currentMemories.splice(index, 1)
const updatedMemories = currentMemories.join(" ,")
iconElement.dataset.memoriesData = updatedMemories
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (textareaElement) {
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
})
if (currentMemories.length <= 1) {
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
delete iconElement.dataset.memoriesData
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}
popup.style.display = "none"
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}
})
})
setTimeout(() => {
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}, 300000)
}
iconElement.innerHTML = ""
iconElement.appendChild(feedbackDiv)
if (resetAfter > 0) {
setTimeout(() => {
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}, resetAfter)
}
}
function setupClaudePromptCapture() {
if (document.body.hasAttribute("data-claude-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-claude-prompt-capture-setup", "true")
const captureClaudePromptContent = async (source: string) => {
let promptContent = ""
const contentEditableDiv = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (contentEditableDiv) {
promptContent =
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
}
if (!promptContent) {
const textarea = document.querySelector("textarea") as HTMLTextAreaElement
if (textarea) {
promptContent = textarea.value || ""
}
}
const storedMemories = contentEditableDiv?.dataset.supermemories
if (
storedMemories &&
contentEditableDiv &&
!promptContent.includes("Supermemories of user")
) {
contentEditableDiv.innerHTML = `${contentEditableDiv.innerHTML} ${storedMemories}`
promptContent =
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
}
if (promptContent.trim()) {
console.log(`Claude prompt submitted via ${source}:`, promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "claude",
source: source,
},
})
} catch (error) {
console.error("Error sending Claude prompt to background:", error)
}
}
const icons = document.querySelectorAll(
'[id*="sm-claude-input-bar-element"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (contentEditableDiv?.dataset.supermemories) {
delete contentEditableDiv.dataset.supermemories
}
}
document.addEventListener(
"click",
async (event) => {
const target = event.target as HTMLElement
const sendButton =
target.closest(
"button.inline-flex.items-center.justify-center.relative.shrink-0.can-focus.select-none",
) ||
target.closest('button[class*="bg-accent-main-000"]') ||
target.closest('button[class*="rounded-lg"]')
if (sendButton) {
await captureClaudePromptContent("button click")
}
},
true,
)
document.addEventListener(
"keydown",
async (event) => {
const target = event.target as HTMLElement
if (
(target.matches('div[contenteditable="true"]') ||
target.matches(".ProseMirror") ||
target.matches("textarea") ||
target.closest(".ProseMirror")) &&
event.key === "Enter" &&
!event.shiftKey
) {
await captureClaudePromptContent("Enter key")
}
},
true,
)
}
async function setupClaudeAutoFetch() {
const result = await chrome.storage.local.get([
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
])
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
if (!autoSearchEnabled) {
return
}
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (
!textareaElement ||
textareaElement.hasAttribute("data-supermemory-auto-fetch")
) {
return
}
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
if (claudeDebounceTimeout) {
clearTimeout(claudeDebounceTimeout)
}
claudeDebounceTimeout = setTimeout(async () => {
const content = textareaElement.textContent?.trim() || ""
if (content.length > 2) {
await getRelatedMemoriesForClaude(
POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
'[id*="sm-claude-input-bar-element"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
textareaElement.addEventListener("input", handleInput)
}

View file

@ -0,0 +1,67 @@
import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
import { DOMUtils } from "../../utils/ui-components"
import { initializeChatGPT } from "./chatgpt"
import { initializeClaude } from "./claude"
import { saveMemory, setupGlobalKeyboardShortcut, setupStorageListener } from "./shared"
import { initializeT3 } from "./t3"
import { handleTwitterNavigation, initializeTwitter, updateTwitterImportUI } from "./twitter"
export default defineContentScript({
matches: ["<all_urls>"],
main() {
// Setup global event listeners
browser.runtime.onMessage.addListener(async (message) => {
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
await saveMemory()
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
updateTwitterImportUI(message)
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
updateTwitterImportUI(message)
}
})
// Setup global keyboard shortcuts
setupGlobalKeyboardShortcut()
// Setup storage listener
setupStorageListener()
// Observer for dynamic content changes
const observeForDynamicChanges = () => {
const observer = new MutationObserver(() => {
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
initializeChatGPT()
}
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
initializeClaude()
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
initializeT3()
}
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
handleTwitterNavigation()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
}
// Initialize platform-specific functionality
initializeChatGPT()
initializeClaude()
initializeT3()
initializeTwitter()
// Start observing for dynamic changes
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeForDynamicChanges)
} else {
observeForDynamicChanges()
}
},
})

View file

@ -0,0 +1,77 @@
import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants"
import { DOMUtils } from "../../utils/ui-components"
export async function saveMemory() {
try {
DOMUtils.showToast("loading")
const highlightedText = window.getSelection()?.toString() || ""
const url = window.location.href
const html = document.documentElement.outerHTML
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html,
highlightedText,
url,
},
actionSource: "context_menu",
})
console.log("Response from enxtension:", response)
if (response.success) {
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memory:", error)
DOMUtils.showToast("error")
}
}
export function setupGlobalKeyboardShortcut() {
document.addEventListener("keydown", async (event) => {
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === "m"
) {
event.preventDefault()
await saveMemory()
}
})
}
export function setupStorageListener() {
window.addEventListener("message", (event) => {
if (event.source !== window) {
return
}
const bearerToken = event.data.token
const userData = event.data.userData
if (bearerToken && userData) {
if (
!(
window.location.hostname === "localhost" ||
window.location.hostname === "supermemory.ai" ||
window.location.hostname === "app.supermemory.ai"
)
) {
console.log(
"Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
)
return
}
chrome.storage.local.set(
{
[STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
[STORAGE_KEYS.USER_DATA]: userData,
},
() => {},
)
}
})
}

View file

@ -0,0 +1,692 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
STORAGE_KEYS,
UI_CONFIG,
} from "../../utils/constants"
import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components"
let t3DebounceTimeout: NodeJS.Timeout | null = null
let t3RouteObserver: MutationObserver | null = null
let t3UrlCheckInterval: NodeJS.Timeout | null = null
let t3ObserverThrottle: NodeJS.Timeout | null = null
export function initializeT3() {
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
return
}
if (document.body.hasAttribute("data-t3-initialized")) {
return
}
setTimeout(() => {
console.log("Adding supermemory icon to T3 input")
addSupermemoryIconToT3Input()
setupT3AutoFetch()
}, 2000)
setupT3PromptCapture()
setupT3RouteChangeDetection()
document.body.setAttribute("data-t3-initialized", "true")
}
function setupT3RouteChangeDetection() {
if (t3RouteObserver) {
t3RouteObserver.disconnect()
}
if (t3UrlCheckInterval) {
clearInterval(t3UrlCheckInterval)
}
if (t3ObserverThrottle) {
clearTimeout(t3ObserverThrottle)
t3ObserverThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log("T3 route changed, re-adding supermemory icon")
setTimeout(() => {
addSupermemoryIconToT3Input()
setupT3AutoFetch()
}, 1000)
}
}
t3UrlCheckInterval = setInterval(checkForRouteChange, 2000)
t3RouteObserver = new MutationObserver((mutations) => {
if (t3ObserverThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.querySelector?.("textarea") ||
element.querySelector?.('div[contenteditable="true"]') ||
element.matches?.("textarea") ||
element.matches?.('div[contenteditable="true"]')
) {
shouldRecheck = true
}
}
})
}
})
if (shouldRecheck) {
t3ObserverThrottle = setTimeout(() => {
try {
t3ObserverThrottle = null
addSupermemoryIconToT3Input()
setupT3AutoFetch()
} catch (error) {
console.error("Error in T3 observer callback:", error)
}
}, 300)
}
})
try {
t3RouteObserver.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error("Failed to set up T3 route observer:", error)
if (t3UrlCheckInterval) {
clearInterval(t3UrlCheckInterval)
}
t3UrlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
function addSupermemoryIconToT3Input() {
const targetContainers = document.querySelectorAll(
".flex.min-w-0.items-center.gap-2.overflow-hidden",
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createT3InputBarElement(async () => {
await getRelatedMemoriesForT3(POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED)
})
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForT3(actionSource: string) {
try {
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (
supermemoryContainer?.parentElement?.parentElement?.previousElementSibling
) {
const textareaElement =
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
"textarea",
)
userQuery = textareaElement?.value || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const textareas = document.querySelectorAll("textarea")
for (const textarea of textareas) {
const text = (textarea as HTMLTextAreaElement).value
if (text?.trim()) {
userQuery = text.trim()
break
}
}
}
console.log("T3 query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found for T3")
return
}
const icon = document.querySelector('[id*="sm-t3-input-bar-element"]')
const iconElement = icon as HTMLElement
if (!iconElement) {
console.warn("T3 icon element not found, cannot update feedback")
return
}
updateT3IconFeedback("Searching memories...", iconElement)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Memory search timeout")),
UI_CONFIG.API_REQUEST_TIMEOUT,
),
)
const response = await Promise.race([
browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
actionSource: actionSource,
}),
timeoutPromise,
])
console.log("T3 memories response:", response)
if (response?.success && response?.data) {
let textareaElement = null
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
)
if (
supermemoryContainer?.parentElement?.parentElement
?.previousElementSibling
) {
textareaElement =
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
"textarea",
)
}
if (!textareaElement) {
textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
}
if (textareaElement) {
if (textareaElement.tagName === "TEXTAREA") {
;(textareaElement as HTMLTextAreaElement).dataset.supermemories =
`<br>Supermemories of user (only for the reference): ${response.data}</br>`
} else {
;(textareaElement as HTMLElement).dataset.supermemories =
`<br>Supermemories of user (only for the reference): ${response.data}</br>`
}
iconElement.dataset.memoriesData = response.data
updateT3IconFeedback("Included Memories", iconElement)
} else {
console.warn("T3 input area not found after successful memory fetch")
updateT3IconFeedback("Memories found", iconElement)
}
} else {
console.warn("No memories found or API response invalid for T3")
updateT3IconFeedback("No memories found", iconElement)
}
} catch (error) {
console.error("Error getting related memories for T3:", error)
try {
const icon = document.querySelector(
'[id*="sm-t3-input-bar-element"]',
) as HTMLElement
if (icon) {
updateT3IconFeedback("Error fetching memories", icon)
}
} catch (feedbackError) {
console.error("Failed to update T3 error feedback:", feedbackError)
}
}
}
function updateT3IconFeedback(
message: string,
iconElement: HTMLElement,
resetAfter = 0,
) {
if (!iconElement.dataset.originalHtml) {
iconElement.dataset.originalHtml = iconElement.innerHTML
}
const feedbackDiv = document.createElement("div")
feedbackDiv.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: #513EA9;
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 500;
cursor: ${message === "Included Memories" ? "pointer" : "default"};
position: relative;
`
feedbackDiv.innerHTML = `
<span></span>
<span>${message}</span>
`
if (message === "Included Memories" && iconElement.dataset.memoriesData) {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
border: 1px solid #333;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-weight: 600; color: #fff;">Included Memories</span>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 0;
max-height: 300px;
overflow-y: auto;
`
const memoriesText = iconElement.dataset.memoriesData || ""
console.log("Memories text:", memoriesText)
const individualMemories = memoriesText
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
console.log("Individual memories:", individualMemories)
individualMemories.forEach((memory, index) => {
const memoryItem = document.createElement("div")
memoryItem.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
padding: 10px;
font-size: 13px;
line-height: 1.4;
`
const memoryText = document.createElement("div")
memoryText.style.cssText = `
flex: 1;
color: #e5e5e5;
`
memoryText.textContent = memory.trim()
const removeBtn = document.createElement("button")
removeBtn.style.cssText = `
background: transparent;
color: #9ca3af;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`
removeBtn.dataset.memoryIndex = index.toString()
removeBtn.addEventListener("mouseenter", () => {
removeBtn.style.color = "#ef4444"
})
removeBtn.addEventListener("mouseleave", () => {
removeBtn.style.color = "#9ca3af"
})
memoryItem.appendChild(memoryText)
memoryItem.appendChild(removeBtn)
content.appendChild(memoryItem)
})
popup.appendChild(header)
popup.appendChild(content)
document.body.appendChild(popup)
feedbackDiv.addEventListener("mouseenter", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Click to see memories"
}
})
feedbackDiv.addEventListener("mouseleave", () => {
const textSpan = feedbackDiv.querySelector("span:last-child")
if (textSpan) {
textSpan.textContent = "Included Memories"
}
})
feedbackDiv.addEventListener("click", (e) => {
e.stopPropagation()
popup.style.display = "block"
})
document.addEventListener("click", (e) => {
if (!popup.contains(e.target as Node)) {
popup.style.display = "none"
}
})
content.querySelectorAll("button[data-memory-index]").forEach((button) => {
const htmlButton = button as HTMLButtonElement
htmlButton.addEventListener("click", () => {
const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10)
const memoryItem = htmlButton.parentElement
if (memoryItem) {
content.removeChild(memoryItem)
}
const currentMemories = (iconElement.dataset.memoriesData || "")
.split(/[,\n]/)
.map((memory) => memory.trim())
.filter((memory) => memory.length > 0 && memory !== ",")
currentMemories.splice(index, 1)
const updatedMemories = currentMemories.join(" ,")
iconElement.dataset.memoriesData = updatedMemories
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
if (textareaElement) {
textareaElement.dataset.supermemories = `<div>Supermemories of user (only for the reference): ${updatedMemories}</div>`
}
content
.querySelectorAll("button[data-memory-index]")
.forEach((btn, newIndex) => {
const htmlBtn = btn as HTMLButtonElement
htmlBtn.dataset.memoryIndex = newIndex.toString()
})
if (currentMemories.length <= 1) {
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
delete iconElement.dataset.memoriesData
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}
popup.style.display = "none"
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}
})
})
setTimeout(() => {
if (document.body.contains(popup)) {
document.body.removeChild(popup)
}
}, 300000)
}
iconElement.innerHTML = ""
iconElement.appendChild(feedbackDiv)
if (resetAfter > 0) {
setTimeout(() => {
iconElement.innerHTML = iconElement.dataset.originalHtml || ""
delete iconElement.dataset.originalHtml
}, resetAfter)
}
}
function setupT3PromptCapture() {
if (document.body.hasAttribute("data-t3-prompt-capture-setup")) {
return
}
document.body.setAttribute("data-t3-prompt-capture-setup", "true")
const captureT3PromptContent = async (source: string) => {
let promptContent = ""
const textarea = document.querySelector("textarea") as HTMLTextAreaElement
if (textarea) {
promptContent = textarea.value || ""
}
if (!promptContent) {
const contentEditableDiv = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement
if (contentEditableDiv) {
promptContent =
contentEditableDiv.textContent || contentEditableDiv.innerText || ""
}
}
const textareaElement =
textarea ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
const storedMemories = textareaElement?.dataset.supermemories
if (
storedMemories &&
textareaElement &&
!promptContent.includes("Supermemories of user")
) {
if (textareaElement.tagName === "TEXTAREA") {
;(textareaElement as HTMLTextAreaElement).value =
`${promptContent} ${storedMemories}`
promptContent = (textareaElement as HTMLTextAreaElement).value
} else {
textareaElement.innerHTML = `${textareaElement.innerHTML} ${storedMemories}`
promptContent =
textareaElement.textContent || textareaElement.innerText || ""
}
}
if (promptContent.trim()) {
console.log(`T3 prompt submitted via ${source}:`, promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "t3",
source: source,
},
})
} catch (error) {
console.error("Error sending T3 prompt to background:", error)
}
}
const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]')
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (textareaElement?.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
}
document.addEventListener(
"click",
async (event) => {
const target = event.target as HTMLElement
const sendButton =
target.closest("button.focus-visible\\:ring-ring") ||
target.closest('button[class*="bg-[rgb(162,59,103)]"]') ||
target.closest('button[class*="rounded-lg"]')
if (sendButton) {
await captureT3PromptContent("button click")
}
},
true,
)
document.addEventListener(
"keydown",
async (event) => {
const target = event.target as HTMLElement
if (
(target.matches("textarea") ||
target.matches('div[contenteditable="true"]')) &&
event.key === "Enter" &&
!event.shiftKey
) {
if (target.matches("textarea")) {
const promptContent = (target as HTMLTextAreaElement).value || ""
if (promptContent.trim()) {
console.log("T3 prompt submitted via Enter key:", promptContent)
try {
await browser.runtime.sendMessage({
action: MESSAGE_TYPES.CAPTURE_PROMPT,
data: {
prompt: promptContent,
platform: "t3",
source: "Enter key",
},
actionSource: "t3",
})
} catch (error) {
console.error(
"Error sending T3 textarea prompt to background:",
error,
)
}
}
} else {
await captureT3PromptContent("Enter key")
}
}
},
true,
)
}
async function setupT3AutoFetch() {
const result = await chrome.storage.local.get([
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
])
const autoSearchEnabled = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
if (!autoSearchEnabled) {
return
}
const textareaElement =
(document.querySelector("textarea") as HTMLTextAreaElement) ||
(document.querySelector('div[contenteditable="true"]') as HTMLElement)
if (
!textareaElement ||
textareaElement.hasAttribute("data-supermemory-auto-fetch")
) {
return
}
textareaElement.setAttribute("data-supermemory-auto-fetch", "true")
const handleInput = () => {
if (t3DebounceTimeout) {
clearTimeout(t3DebounceTimeout)
}
t3DebounceTimeout = setTimeout(async () => {
let content = ""
if (textareaElement.tagName === "TEXTAREA") {
content = (textareaElement as HTMLTextAreaElement).value?.trim() || ""
} else {
content = textareaElement.textContent?.trim() || ""
}
if (content.length > 2) {
await getRelatedMemoriesForT3(
POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_AUTO_SEARCHED,
)
} else if (content.length === 0) {
const icons = document.querySelectorAll(
'[id*="sm-t3-input-bar-element"]',
)
icons.forEach((icon) => {
const iconElement = icon as HTMLElement
if (iconElement.dataset.originalHtml) {
iconElement.innerHTML = iconElement.dataset.originalHtml
delete iconElement.dataset.originalHtml
delete iconElement.dataset.memoriesData
}
})
if (textareaElement.dataset.supermemories) {
delete textareaElement.dataset.supermemories
}
}
}, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY)
}
textareaElement.addEventListener("input", handleInput)
}

View file

@ -0,0 +1,102 @@
import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
} from "../../utils/constants"
import { trackEvent } from "../../utils/posthog"
import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components"
export function initializeTwitter() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
return
}
// Initial setup
if (window.location.pathname === "/i/bookmarks") {
setTimeout(() => {
addTwitterImportButton()
}, 2000)
} else {
// Remove button if not on bookmarks page
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
}
}
function addTwitterImportButton() {
// Only show the import button on the bookmarks page
if (window.location.pathname !== "/i/bookmarks") {
return
}
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
return
}
const button = createTwitterImportButton(async () => {
try {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
})
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
})
} catch (error) {
console.error("Error starting import:", error)
}
})
document.body.appendChild(button)
}
export function updateTwitterImportUI(message: {
type: string
importedMessage?: string
totalImported?: number
}) {
const importButton = document.getElementById(
ELEMENT_IDS.TWITTER_IMPORT_BUTTON,
)
if (!importButton) return
const iconUrl = browser.runtime.getURL("/icon-16.png")
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span>
`
importButton.style.cursor = "default"
}
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px; color: #059669;"> Imported ${message.totalImported} tweets!</span>
`
setTimeout(() => {
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span>
`
importButton.style.cursor = "pointer"
}, 3000)
}
}
export function handleTwitterNavigation() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
return
}
if (window.location.pathname === "/i/bookmarks") {
addTwitterImportButton()
} else {
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
}
}

View file

@ -16,7 +16,10 @@ function App() {
const [currentUrl, setCurrentUrl] = useState<string>("")
const [currentTitle, setCurrentTitle] = useState<string>("")
const [saving, setSaving] = useState<boolean>(false)
const [activeTab, setActiveTab] = useState<"save" | "imports">("save")
const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">(
"save",
)
const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false)
const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({
@ -32,9 +35,14 @@ function App() {
try {
const result = await chrome.storage.local.get([
STORAGE_KEYS.BEARER_TOKEN,
STORAGE_KEYS.AUTO_SEARCH_ENABLED,
])
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN]
setUserSignedIn(isSignedIn)
const autoSearchSetting =
result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false
setAutoSearchEnabled(autoSearchSetting)
} catch (error) {
console.error("Error checking auth status:", error)
setUserSignedIn(false)
@ -103,11 +111,6 @@ function App() {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "success",
})
} else {
await chrome.tabs.sendMessage(tabs[0].id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "error",
})
}
window.close()
@ -136,6 +139,17 @@ function App() {
}
}
const handleAutoSearchToggle = async (enabled: boolean) => {
try {
await chrome.storage.local.set({
[STORAGE_KEYS.AUTO_SEARCH_ENABLED]: enabled,
})
setAutoSearchEnabled(enabled)
} catch (error) {
console.error("Error updating auto search setting:", error)
}
}
const handleSignOut = async () => {
try {
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN])
@ -208,7 +222,7 @@ function App() {
{/* Tab Navigation */}
<div className="flex bg-gray-100 rounded-lg p-1 mb-4">
<button
className={`flex-1 py-2 px-4 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
activeTab === "save"
? "bg-white text-black shadow-sm"
: "text-gray-500 hover:text-gray-700"
@ -219,7 +233,7 @@ function App() {
Save
</button>
<button
className={`flex-1 py-2 px-4 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
activeTab === "imports"
? "bg-white text-black shadow-sm"
: "text-gray-500 hover:text-gray-700"
@ -229,6 +243,17 @@ function App() {
>
Imports
</button>
<button
className={`flex-1 py-2 px-3 bg-transparent border-none rounded-md text-sm font-medium cursor-pointer transition-all duration-200 outline-none appearance-none ${
activeTab === "settings"
? "bg-white text-black shadow-sm"
: "text-gray-500 hover:text-gray-700"
}`}
onClick={() => setActiveTab("settings")}
type="button"
>
Settings
</button>
</div>
{/* Tab Content */}
@ -295,7 +320,7 @@ function App() {
</button>
</div>
</div>
) : (
) : activeTab === "imports" ? (
<div className="flex flex-col gap-4 min-h-[200px]">
{/* Import Actions */}
<div className="flex flex-col gap-4">
@ -370,6 +395,40 @@ function App() {
</div>
</div>
</div>
) : (
<div className="flex flex-col gap-4 min-h-[200px]">
<div className="mb-4">
<h3 className="text-base font-semibold text-black mb-3">
Chat Integration
</h3>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex flex-col">
<span className="text-sm font-medium text-black">
Auto Search Memories
</span>
<span className="text-xs text-gray-500">
Automatically search your memories while typing in chat
apps
</span>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
checked={autoSearchEnabled}
className="sr-only peer"
onChange={(e) =>
handleAutoSearchToggle(e.target.checked)
}
type="checkbox"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gray-700" />
</label>
</div>
<p className="text-xs text-gray-500 mt-2">
When enabled, supermemory will search your memories as you
type in ChatGPT, Claude, and T3.chat
</p>
</div>
</div>
)}
{showProjectSelector && (

View file

@ -21,6 +21,7 @@ export const STORAGE_KEYS = {
TWITTER_CSRF: "twitter-csrf",
TWITTER_AUTH_TOKEN: "twitter-auth-token",
DEFAULT_PROJECT: "sm-default-project",
AUTO_SEARCH_ENABLED: "sm-auto-search-enabled",
} as const
/**
@ -44,6 +45,10 @@ export const UI_CONFIG = {
TOAST_DURATION: 3000, // milliseconds
RATE_LIMIT_BASE_WAIT: 60000, // 1 minute
PAGINATION_DELAY: 1000, // 1 second between requests
AUTO_SEARCH_DEBOUNCE_DELAY: 1500, // milliseconds to wait after user stops typing
OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions
ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks
API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout
} as const
/**
@ -75,6 +80,7 @@ export const MESSAGE_TYPES = {
IMPORT_UPDATE: "sm-import-update",
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
CAPTURE_PROMPT: "sm-capture-prompt",
} as const
export const CONTEXT_MENU_IDS = {
@ -87,6 +93,9 @@ export const POSTHOG_EVENT_KEY = {
SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed",
SOURCE: "extension",
T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched",
T3_CHAT_MEMORIES_AUTO_SEARCHED: "t3_chat_memories_auto_searched",
CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched",
CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
} as const

View file

@ -0,0 +1,93 @@
/**
* Memory Popup Utilities
* Standardized popup positioning and styling for memory display across platforms
*/
export interface MemoryPopupConfig {
memoriesData: string
onClose: () => void
onRemove?: () => void
}
export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
const popup = document.createElement("div")
popup.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: white;
padding: 0;
border-radius: 12px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
max-width: 500px;
max-height: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 999999;
display: none;
overflow: hidden;
`
const header = document.createElement("div")
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
`
header.innerHTML = `
<span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span>
<div style="display: flex; gap: 4px;">
${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""}
<button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;"></button>
</div>
`
const content = document.createElement("div")
content.style.cssText = `
padding: 8px;
max-height: 300px;
overflow-y: auto;
line-height: 1.4;
`
content.textContent = config.memoriesData
const closeBtn = header.querySelector("#close-popup-btn")
closeBtn?.addEventListener("click", config.onClose)
const removeBtn = header.querySelector("#remove-memories-btn")
if (removeBtn && config.onRemove) {
removeBtn.addEventListener("click", config.onRemove)
}
popup.appendChild(header)
popup.appendChild(content)
return popup
}
export function showMemoryPopup(popup: HTMLElement): void {
popup.style.display = "block"
setTimeout(() => {
if (popup.style.display === "block") {
hideMemoryPopup(popup)
}
}, 10000)
}
export function hideMemoryPopup(popup: HTMLElement): void {
popup.style.display = "none"
}
export function toggleMemoryPopup(popup: HTMLElement): void {
if (popup.style.display === "none" || popup.style.display === "") {
showMemoryPopup(popup)
} else {
hideMemoryPopup(popup)
}
}

View file

@ -0,0 +1,117 @@
/**
* Route Detection Utilities
* Shared logic for detecting route changes across different AI chat platforms
*/
import { UI_CONFIG } from "./constants"
export interface RouteDetectionConfig {
platform: string
selectors: string[]
reinitCallback: () => void
checkInterval?: number
observerThrottleDelay?: number
}
export interface RouteDetectionCleanup {
observer: MutationObserver | null
urlCheckInterval: NodeJS.Timeout | null
observerThrottle: NodeJS.Timeout | null
}
export function createRouteDetection(
config: RouteDetectionConfig,
cleanup: RouteDetectionCleanup,
): void {
if (cleanup.observer) {
cleanup.observer.disconnect()
}
if (cleanup.urlCheckInterval) {
clearInterval(cleanup.urlCheckInterval)
}
if (cleanup.observerThrottle) {
clearTimeout(cleanup.observerThrottle)
cleanup.observerThrottle = null
}
let currentUrl = window.location.href
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
console.log(`${config.platform} route changed, re-initializing`)
setTimeout(config.reinitCallback, 1000)
}
}
cleanup.urlCheckInterval = setInterval(
checkForRouteChange,
config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
)
cleanup.observer = new MutationObserver((mutations) => {
if (cleanup.observerThrottle) {
return
}
let shouldRecheck = false
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
for (const selector of config.selectors) {
if (
element.querySelector?.(selector) ||
element.matches?.(selector)
) {
shouldRecheck = true
break
}
}
}
})
}
})
if (shouldRecheck) {
cleanup.observerThrottle = setTimeout(() => {
try {
cleanup.observerThrottle = null
config.reinitCallback()
} catch (error) {
console.error(`Error in ${config.platform} observer callback:`, error)
}
}, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
}
})
try {
cleanup.observer.observe(document.body, {
childList: true,
subtree: true,
})
} catch (error) {
console.error(`Failed to set up ${config.platform} route observer:`, error)
if (cleanup.urlCheckInterval) {
clearInterval(cleanup.urlCheckInterval)
}
cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
}
}
export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
if (cleanup.observer) {
cleanup.observer.disconnect()
cleanup.observer = null
}
if (cleanup.urlCheckInterval) {
clearInterval(cleanup.urlCheckInterval)
cleanup.urlCheckInterval = null
}
if (cleanup.observerThrottle) {
clearTimeout(cleanup.observerThrottle)
cleanup.observerThrottle = null
}
}

View file

@ -24,7 +24,8 @@ export interface ExtensionMessage {
* Memory data structure for saving content
*/
export interface MemoryData {
html: string
html?: string
content?: string
highlightedText?: string
url?: string
}

View file

@ -242,7 +242,7 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
width: auto;
height: 24px;
cursor: pointer;
transition: opacity 0.2s ease;
@ -284,13 +284,12 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
border: 1px solid rgba(0, 0, 0, 0.1);
`
const iconFileName = "/icon-16.png"
@ -329,13 +328,12 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
width: auto;
height: 32px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
border: 1px solid rgba(0, 0, 0, 0.1);
`
const iconFileName = "/icon-16.png"
@ -421,7 +419,6 @@ export const DOMUtils = {
const text = existingToast.querySelector("span")
if (icon && text) {
// Update based on new state
if (state === "success") {
const iconUrl = browser.runtime.getURL("/icon-16.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`

View file

@ -11,7 +11,7 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.0.001",
version: "6.0.002",
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
@ -30,6 +30,6 @@ export default defineConfig({
],
},
webExt: {
disabled: true,
chromiumArgs: ["--user-data-dir=./.wxt/chrome-data"],
},
})