mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
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:
parent
96f1a97fd6
commit
d7cc7d847d
15 changed files with 2641 additions and 696 deletions
|
|
@ -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
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
724
apps/browser-extension/entrypoints/content/chatgpt.ts
Normal file
724
apps/browser-extension/entrypoints/content/chatgpt.ts
Normal 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,
|
||||
)
|
||||
}
|
||||
648
apps/browser-extension/entrypoints/content/claude.ts
Normal file
648
apps/browser-extension/entrypoints/content/claude.ts
Normal 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)
|
||||
}
|
||||
67
apps/browser-extension/entrypoints/content/index.ts
Normal file
67
apps/browser-extension/entrypoints/content/index.ts
Normal 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()
|
||||
}
|
||||
},
|
||||
})
|
||||
77
apps/browser-extension/entrypoints/content/shared.ts
Normal file
77
apps/browser-extension/entrypoints/content/shared.ts
Normal 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,
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
692
apps/browser-extension/entrypoints/content/t3.ts
Normal file
692
apps/browser-extension/entrypoints/content/t3.ts
Normal 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)
|
||||
}
|
||||
102
apps/browser-extension/entrypoints/content/twitter.ts
Normal file
102
apps/browser-extension/entrypoints/content/twitter.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
93
apps/browser-extension/utils/memory-popup.ts
Normal file
93
apps/browser-extension/utils/memory-popup.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
117
apps/browser-extension/utils/route-detection.ts
Normal file
117
apps/browser-extension/utils/route-detection.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;" />`
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue