mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-07-09 17:18:51 +00:00
feat: add chat memory imports to extension (#1115)
demo shared on work-done
This commit is contained in:
parent
9c5a5d4991
commit
da70ccc5f1
8 changed files with 1164 additions and 123 deletions
|
|
@ -29,6 +29,7 @@ export function initializeClaude() {
|
|||
}
|
||||
|
||||
setTimeout(() => {
|
||||
addSupermemoryButtonToClaudeMemoryDialog()
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
}, 2000)
|
||||
|
|
@ -59,6 +60,7 @@ function setupClaudeRouteChangeDetection() {
|
|||
currentUrl = window.location.href
|
||||
console.log("Claude route changed, re-adding supermemory icon")
|
||||
setTimeout(() => {
|
||||
addSupermemoryButtonToClaudeMemoryDialog()
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
}, 1000)
|
||||
|
|
@ -79,10 +81,13 @@ function setupClaudeRouteChangeDetection() {
|
|||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
if (
|
||||
element.querySelector?.('[role="dialog"]') ||
|
||||
element.querySelector?.('div[contenteditable="true"]') ||
|
||||
element.querySelector?.("textarea") ||
|
||||
element.matches?.('[role="dialog"]') ||
|
||||
element.matches?.('div[contenteditable="true"]') ||
|
||||
element.matches?.("textarea")
|
||||
element.matches?.("textarea") ||
|
||||
element.textContent?.includes("Manage memory")
|
||||
) {
|
||||
shouldRecheck = true
|
||||
}
|
||||
|
|
@ -95,6 +100,7 @@ function setupClaudeRouteChangeDetection() {
|
|||
claudeObserverThrottle = setTimeout(() => {
|
||||
try {
|
||||
claudeObserverThrottle = null
|
||||
addSupermemoryButtonToClaudeMemoryDialog()
|
||||
addSupermemoryIconToClaudeInput()
|
||||
setupClaudeAutoFetch()
|
||||
} catch (error) {
|
||||
|
|
@ -264,6 +270,190 @@ async function getRelatedMemoriesForClaude(actionSource: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function getClaudeMemoryDialog(): HTMLElement | null {
|
||||
const dialogs = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[role="dialog"]'),
|
||||
)
|
||||
|
||||
for (const dialog of dialogs) {
|
||||
const heading = Array.from(dialog.querySelectorAll("h1, h2, h3")).find(
|
||||
(element) => element.textContent?.trim() === "Manage memory",
|
||||
)
|
||||
if (heading) return dialog
|
||||
}
|
||||
|
||||
const candidates = Array.from(document.querySelectorAll<HTMLElement>("div"))
|
||||
.filter((element) => {
|
||||
const text = element.textContent || ""
|
||||
if (
|
||||
!text.includes("Manage memory") ||
|
||||
!text.includes("Here's what Claude remembers")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
return rect.width > 400 && rect.height > 250
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const rectA = a.getBoundingClientRect()
|
||||
const rectB = b.getBoundingClientRect()
|
||||
return rectA.width * rectA.height - rectB.width * rectB.height
|
||||
})
|
||||
|
||||
return candidates[0] || null
|
||||
}
|
||||
|
||||
function getClaudeMemoryText(dialog: HTMLElement): string {
|
||||
const clonedDialog = dialog.cloneNode(true) as HTMLElement
|
||||
clonedDialog.querySelector("#supermemory-save-button")?.remove()
|
||||
|
||||
const sanitizeClaudeMemoryText = (text: string) =>
|
||||
text
|
||||
.replace(/^Memories from Claude:\s*/i, "")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(
|
||||
(line) =>
|
||||
line &&
|
||||
line !== "Tell Claude what to remember or forget..." &&
|
||||
line !== "Save to supermemory",
|
||||
)
|
||||
.join("\n")
|
||||
.trim()
|
||||
|
||||
const memorySections = Array.from(
|
||||
clonedDialog.querySelectorAll<HTMLElement>(
|
||||
"article, section, [class*='border'], [class*='rounded']",
|
||||
),
|
||||
)
|
||||
.map((element) => element.innerText || element.textContent || "")
|
||||
.map(sanitizeClaudeMemoryText)
|
||||
.filter((text) => {
|
||||
return (
|
||||
text.length > 80 &&
|
||||
!text.includes("Manage edits") &&
|
||||
!text.includes("Save to supermemory") &&
|
||||
!text.includes("Tell Claude what to remember or forget")
|
||||
)
|
||||
})
|
||||
.sort((a, b) => b.length - a.length)
|
||||
|
||||
if (memorySections[0]) return memorySections[0]
|
||||
|
||||
return sanitizeClaudeMemoryText(
|
||||
clonedDialog.innerText || clonedDialog.textContent || "",
|
||||
)
|
||||
}
|
||||
|
||||
function addSupermemoryButtonToClaudeMemoryDialog() {
|
||||
const memoryDialog = getClaudeMemoryDialog()
|
||||
if (!memoryDialog) return
|
||||
|
||||
if (memoryDialog.querySelector("#supermemory-save-button")) return
|
||||
|
||||
const supermemoryButton = document.createElement("button")
|
||||
supermemoryButton.id = "supermemory-save-button"
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
|
||||
supermemoryButton.innerHTML = `
|
||||
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap;">
|
||||
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
|
||||
<span style="white-space: nowrap;">Save to supermemory</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
supermemoryButton.style.cssText = `
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: auto !important;
|
||||
min-width: 190px !important;
|
||||
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;
|
||||
line-height: 20px !important;
|
||||
white-space: nowrap !important;
|
||||
margin: 8px 0 8px 0 !important;
|
||||
transform: translateX(-16px) !important;
|
||||
cursor: pointer !important;
|
||||
font-family: inherit !important;
|
||||
`
|
||||
|
||||
supermemoryButton.addEventListener("mouseenter", () => {
|
||||
supermemoryButton.style.backgroundColor = "#2B2E33"
|
||||
})
|
||||
|
||||
supermemoryButton.addEventListener("mouseleave", () => {
|
||||
supermemoryButton.style.backgroundColor = "#1C2026"
|
||||
})
|
||||
|
||||
supermemoryButton.addEventListener("click", async () => {
|
||||
await saveClaudeMemoriesToSupermemory(memoryDialog)
|
||||
})
|
||||
|
||||
const introText = Array.from(
|
||||
memoryDialog.querySelectorAll<HTMLElement>("p, div"),
|
||||
).find((element) =>
|
||||
element.textContent?.includes("Here's what Claude remembers"),
|
||||
)
|
||||
|
||||
if (introText?.parentElement) {
|
||||
introText.parentElement.insertBefore(
|
||||
supermemoryButton,
|
||||
introText.nextSibling,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const heading = Array.from(memoryDialog.querySelectorAll("h1, h2, h3")).find(
|
||||
(element) => element.textContent?.trim() === "Manage memory",
|
||||
)
|
||||
|
||||
if (heading?.parentElement) {
|
||||
heading.parentElement.insertBefore(supermemoryButton, heading.nextSibling)
|
||||
return
|
||||
}
|
||||
|
||||
memoryDialog.insertBefore(supermemoryButton, memoryDialog.firstChild)
|
||||
}
|
||||
|
||||
async function saveClaudeMemoriesToSupermemory(memoryDialog: HTMLElement) {
|
||||
try {
|
||||
DOMUtils.showToast("loading")
|
||||
|
||||
const memoryText = getClaudeMemoryText(memoryDialog)
|
||||
if (!memoryText) {
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
}
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
data: {
|
||||
html: memoryText,
|
||||
},
|
||||
actionSource: "claude_memories_dialog",
|
||||
})
|
||||
|
||||
console.log({ response })
|
||||
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success")
|
||||
} else {
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving Claude memories to supermemory:", error)
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
}
|
||||
|
||||
function updateClaudeIconFeedback(
|
||||
message: string,
|
||||
iconElement: HTMLElement,
|
||||
|
|
|
|||
445
apps/browser-extension/entrypoints/content/grok.ts
Normal file
445
apps/browser-extension/entrypoints/content/grok.ts
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
|
||||
import { DOMUtils } from "../../utils/ui-components"
|
||||
|
||||
let grokRouteObserver: MutationObserver | null = null
|
||||
let grokUrlCheckInterval: NodeJS.Timeout | null = null
|
||||
let grokObserverThrottle: NodeJS.Timeout | null = null
|
||||
const GROK_IMPORT_INTENT_PARAM = "sm_grok_import"
|
||||
const GROK_IMPORT_INTENT_VALUE = "memories"
|
||||
|
||||
export function initializeGrok() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.GROK)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (document.body.hasAttribute("data-grok-initialized")) {
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
addSupermemoryButtonToGrokMemoryDialog()
|
||||
handleGrokImportIntent()
|
||||
}, 1000)
|
||||
|
||||
setupGrokRouteChangeDetection()
|
||||
|
||||
document.body.setAttribute("data-grok-initialized", "true")
|
||||
}
|
||||
|
||||
function setupGrokRouteChangeDetection() {
|
||||
if (grokRouteObserver) {
|
||||
grokRouteObserver.disconnect()
|
||||
}
|
||||
if (grokUrlCheckInterval) {
|
||||
clearInterval(grokUrlCheckInterval)
|
||||
}
|
||||
if (grokObserverThrottle) {
|
||||
clearTimeout(grokObserverThrottle)
|
||||
grokObserverThrottle = null
|
||||
}
|
||||
|
||||
let currentUrl = window.location.href
|
||||
|
||||
const checkForRouteChange = () => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href
|
||||
setTimeout(() => {
|
||||
addSupermemoryButtonToGrokMemoryDialog()
|
||||
handleGrokImportIntent()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
grokUrlCheckInterval = setInterval(checkForRouteChange, 2000)
|
||||
|
||||
grokRouteObserver = new MutationObserver((mutations) => {
|
||||
if (grokObserverThrottle) {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldRecheck = false
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList" || mutation.addedNodes.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
continue
|
||||
}
|
||||
|
||||
const element = node as Element
|
||||
const text = element.textContent || ""
|
||||
if (
|
||||
element.querySelector?.('[role="dialog"]') ||
|
||||
element.matches?.('[role="dialog"]') ||
|
||||
text.includes("Data Controls") ||
|
||||
text.includes("Settings") ||
|
||||
text.includes("Memory from your chats")
|
||||
) {
|
||||
shouldRecheck = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRecheck) {
|
||||
grokObserverThrottle = setTimeout(() => {
|
||||
grokObserverThrottle = null
|
||||
addSupermemoryButtonToGrokMemoryDialog()
|
||||
handleGrokImportIntent()
|
||||
}, 250)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
grokRouteObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to set up Grok route observer:", error)
|
||||
if (grokUrlCheckInterval) {
|
||||
clearInterval(grokUrlCheckInterval)
|
||||
}
|
||||
grokUrlCheckInterval = setInterval(checkForRouteChange, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function hasGrokImportIntent() {
|
||||
return (
|
||||
new URLSearchParams(window.location.search).get(
|
||||
GROK_IMPORT_INTENT_PARAM,
|
||||
) === GROK_IMPORT_INTENT_VALUE
|
||||
)
|
||||
}
|
||||
|
||||
function clearGrokImportIntent() {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete(GROK_IMPORT_INTENT_PARAM)
|
||||
window.history.replaceState(window.history.state, "", url.toString())
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function isVisible(element: HTMLElement) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const style = window.getComputedStyle(element)
|
||||
|
||||
return (
|
||||
rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
style.display !== "none" &&
|
||||
style.visibility !== "hidden" &&
|
||||
Number.parseFloat(style.opacity || "1") > 0
|
||||
)
|
||||
}
|
||||
|
||||
function getNormalizedText(element: Element) {
|
||||
return (element.textContent || "").replace(/\s+/g, " ").trim()
|
||||
}
|
||||
|
||||
function clickVisibleElementByText(
|
||||
labels: string[],
|
||||
root: ParentNode = document,
|
||||
) {
|
||||
const elements = Array.from(
|
||||
root.querySelectorAll<HTMLElement>(
|
||||
"button, a, [role='button'], [role='tab'], [data-testid], div, span",
|
||||
),
|
||||
)
|
||||
|
||||
for (const label of labels) {
|
||||
const matchingElement = elements.find((element) => {
|
||||
const text = getNormalizedText(element)
|
||||
return text === label && isVisible(element)
|
||||
})
|
||||
|
||||
if (!matchingElement) {
|
||||
continue
|
||||
}
|
||||
|
||||
const clickableElement =
|
||||
matchingElement.closest<HTMLElement>(
|
||||
"button, a, [role='button'], [role='tab']",
|
||||
) || matchingElement
|
||||
|
||||
clickableElement.click()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function getGrokSettingsDialog() {
|
||||
return Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[role="dialog"]'),
|
||||
).find((dialog) => {
|
||||
const text = getNormalizedText(dialog)
|
||||
return (
|
||||
isVisible(dialog) &&
|
||||
text.includes("Data Controls") &&
|
||||
text.includes("Appearance") &&
|
||||
text.includes("Behavior")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function isGrokDataControlsVisible() {
|
||||
const text = getNormalizedText(document.body)
|
||||
return (
|
||||
text.includes("Data Controls") && text.includes("Memory from your chats")
|
||||
)
|
||||
}
|
||||
|
||||
async function handleGrokImportIntent() {
|
||||
if (!hasGrokImportIntent()) return
|
||||
|
||||
if (document.body.hasAttribute("data-grok-import-intent-running")) {
|
||||
return
|
||||
}
|
||||
|
||||
document.body.setAttribute("data-grok-import-intent-running", "true")
|
||||
|
||||
for (let attempt = 0; attempt < 24; attempt++) {
|
||||
addSupermemoryButtonToGrokMemoryDialog()
|
||||
|
||||
if (getGrokMemoryDialog()) {
|
||||
clearGrokImportIntent()
|
||||
document.body.removeAttribute("data-grok-import-intent-running")
|
||||
return
|
||||
}
|
||||
|
||||
const settingsDialog = getGrokSettingsDialog()
|
||||
if (settingsDialog) {
|
||||
if (isGrokDataControlsVisible()) {
|
||||
clearGrokImportIntent()
|
||||
document.body.removeAttribute("data-grok-import-intent-running")
|
||||
return
|
||||
}
|
||||
|
||||
clickVisibleElementByText(["Data Controls"], settingsDialog)
|
||||
} else {
|
||||
clickVisibleElementByText(["Settings"], document)
|
||||
}
|
||||
|
||||
await sleep(350)
|
||||
}
|
||||
|
||||
document.body.removeAttribute("data-grok-import-intent-running")
|
||||
}
|
||||
|
||||
function getGrokMemoryDialog(): HTMLElement | null {
|
||||
const dialogs = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[role="dialog"]'),
|
||||
)
|
||||
|
||||
for (const dialog of dialogs) {
|
||||
const heading = Array.from(dialog.querySelectorAll("h1, h2, h3")).find(
|
||||
(element) => element.textContent?.trim() === "Memory from your chats",
|
||||
)
|
||||
if (heading) return dialog
|
||||
}
|
||||
|
||||
const candidates = Array.from(document.querySelectorAll<HTMLElement>("div"))
|
||||
.filter((element) => {
|
||||
const text = element.textContent || ""
|
||||
if (
|
||||
!text.includes("Memory from your chats") ||
|
||||
!text.includes("This summary is regenerated")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
return rect.width > 400 && rect.height > 250
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const rectA = a.getBoundingClientRect()
|
||||
const rectB = b.getBoundingClientRect()
|
||||
return rectA.width * rectA.height - rectB.width * rectB.height
|
||||
})
|
||||
|
||||
return candidates[0] || null
|
||||
}
|
||||
|
||||
const GROK_MEMORY_UI_TEXT = [
|
||||
"Memory from your chats",
|
||||
"This summary is regenerated periodically from your conversations.",
|
||||
"Save to supermemory",
|
||||
"Close",
|
||||
"Delete memory",
|
||||
"Edit",
|
||||
] as const
|
||||
|
||||
function escapeRegExp(text: string) {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
function sanitizeGrokMemoryText(text: string) {
|
||||
let sanitizedText = text
|
||||
|
||||
for (const uiText of GROK_MEMORY_UI_TEXT) {
|
||||
sanitizedText = sanitizedText.replace(
|
||||
new RegExp(escapeRegExp(uiText), "g"),
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
|
||||
return sanitizedText
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line)
|
||||
.join("\n")
|
||||
.trim()
|
||||
}
|
||||
|
||||
function getGrokMemoryText(dialog: HTMLElement): string {
|
||||
const clonedDialog = dialog.cloneNode(true) as HTMLElement
|
||||
clonedDialog.querySelector("#supermemory-save-button")?.remove()
|
||||
|
||||
const possibleMemoryContainers = Array.from(
|
||||
clonedDialog.querySelectorAll<HTMLElement>(
|
||||
"article, section, [class*='overflow'], [class*='prose'], [class*='whitespace']",
|
||||
),
|
||||
)
|
||||
.map((element) => element.innerText || element.textContent || "")
|
||||
.map(sanitizeGrokMemoryText)
|
||||
.filter((text) => text.length > 30)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
|
||||
if (possibleMemoryContainers[0]) {
|
||||
return possibleMemoryContainers[0]
|
||||
}
|
||||
|
||||
return sanitizeGrokMemoryText(
|
||||
clonedDialog.innerText || clonedDialog.textContent || "",
|
||||
)
|
||||
}
|
||||
|
||||
function createSupermemoryButton(memoryDialog: HTMLElement) {
|
||||
const supermemoryButton = document.createElement("button")
|
||||
supermemoryButton.id = "supermemory-save-button"
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
|
||||
supermemoryButton.innerHTML = `
|
||||
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap;">
|
||||
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
|
||||
<span style="white-space: nowrap;">Save to supermemory</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
supermemoryButton.style.cssText = `
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: auto !important;
|
||||
min-width: 190px !important;
|
||||
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;
|
||||
line-height: 20px !important;
|
||||
white-space: nowrap !important;
|
||||
cursor: pointer !important;
|
||||
font-family: inherit !important;
|
||||
z-index: 1 !important;
|
||||
`
|
||||
|
||||
supermemoryButton.addEventListener("mouseenter", () => {
|
||||
supermemoryButton.style.backgroundColor = "#2B2E33"
|
||||
})
|
||||
|
||||
supermemoryButton.addEventListener("mouseleave", () => {
|
||||
supermemoryButton.style.backgroundColor = "#1C2026"
|
||||
})
|
||||
|
||||
supermemoryButton.addEventListener("click", async () => {
|
||||
await saveGrokMemoriesToSupermemory(memoryDialog)
|
||||
})
|
||||
|
||||
return supermemoryButton
|
||||
}
|
||||
|
||||
function addSupermemoryButtonToGrokMemoryDialog() {
|
||||
const memoryDialog = getGrokMemoryDialog()
|
||||
if (!memoryDialog) return
|
||||
|
||||
if (memoryDialog.querySelector("#supermemory-save-button")) return
|
||||
|
||||
const supermemoryButton = createSupermemoryButton(memoryDialog)
|
||||
|
||||
const heading = Array.from(memoryDialog.querySelectorAll("h1, h2, h3")).find(
|
||||
(element) => element.textContent?.trim() === "Memory from your chats",
|
||||
)
|
||||
|
||||
const closeButton = Array.from(
|
||||
memoryDialog.querySelectorAll<HTMLButtonElement>("button"),
|
||||
).find((button) => {
|
||||
const label = button.getAttribute("aria-label")?.toLowerCase() || ""
|
||||
const text = button.textContent?.trim().toLowerCase() || ""
|
||||
return label.includes("close") || text === "×" || text === "x"
|
||||
})
|
||||
|
||||
if (heading?.parentElement) {
|
||||
const header = heading.parentElement
|
||||
header.style.display = "flex"
|
||||
header.style.alignItems = "center"
|
||||
header.style.gap = "12px"
|
||||
|
||||
const spacer = document.createElement("div")
|
||||
spacer.style.flex = "1"
|
||||
|
||||
if (closeButton?.parentElement === header) {
|
||||
header.insertBefore(spacer, closeButton)
|
||||
header.insertBefore(supermemoryButton, closeButton)
|
||||
} else {
|
||||
header.appendChild(spacer)
|
||||
header.appendChild(supermemoryButton)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (closeButton?.parentElement) {
|
||||
closeButton.parentElement.insertBefore(supermemoryButton, closeButton)
|
||||
return
|
||||
}
|
||||
|
||||
memoryDialog.insertBefore(supermemoryButton, memoryDialog.firstChild)
|
||||
}
|
||||
|
||||
async function saveGrokMemoriesToSupermemory(memoryDialog: HTMLElement) {
|
||||
try {
|
||||
DOMUtils.showToast("loading")
|
||||
|
||||
const memoryText = getGrokMemoryText(memoryDialog)
|
||||
if (!memoryText) {
|
||||
DOMUtils.showToast("error")
|
||||
return
|
||||
}
|
||||
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
data: {
|
||||
content: memoryText,
|
||||
title: "Grok memories import",
|
||||
},
|
||||
actionSource: "grok_memories_dialog",
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
DOMUtils.showToast("success")
|
||||
} else {
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving Grok memories to supermemory:", error)
|
||||
DOMUtils.showToast("error")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { DOMAINS, MESSAGE_TYPES } from "../../utils/constants"
|
|||
import { DOMUtils } from "../../utils/ui-components"
|
||||
import { initializeChatGPT } from "./chatgpt"
|
||||
import { initializeClaude } from "./claude"
|
||||
import { initializeGrok } from "./grok"
|
||||
import {
|
||||
saveMemory,
|
||||
setupGlobalKeyboardShortcut,
|
||||
|
|
@ -48,6 +49,9 @@ export default defineContentScript({
|
|||
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
|
||||
initializeClaude()
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.GROK)) {
|
||||
initializeGrok()
|
||||
}
|
||||
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
|
||||
initializeT3()
|
||||
}
|
||||
|
|
@ -65,6 +69,7 @@ export default defineContentScript({
|
|||
// Initialize platform-specific functionality
|
||||
initializeChatGPT()
|
||||
initializeClaude()
|
||||
initializeGrok()
|
||||
initializeT3()
|
||||
initializeTwitter()
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,167 @@ const Tooltip = ({
|
|||
)
|
||||
}
|
||||
|
||||
const cardShadow =
|
||||
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset"
|
||||
|
||||
type ManualImportProvider = "gemini"
|
||||
|
||||
const manualImportProviderConfig: Record<
|
||||
ManualImportProvider,
|
||||
{ label: string; actionSource: string }
|
||||
> = {
|
||||
gemini: {
|
||||
label: "Gemini",
|
||||
actionSource: "gemini_manual_memory_import",
|
||||
},
|
||||
}
|
||||
|
||||
const manualMemoryImportPrompt = `Export all of my stored memories and any context you've learned about me from past conversations. Preserve my words verbatim where possible, especially for instructions and preferences.
|
||||
|
||||
## Categories (output in this order):
|
||||
|
||||
1. **Instructions**: Rules I've explicitly asked you to follow going forward - tone, format, style, "always do X", "never do Y", and corrections to your behavior. Only include rules from stored memories, not from conversations.
|
||||
|
||||
2. **Identity**: Name, age, location, education, family, relationships, languages, and personal interests.
|
||||
|
||||
3. **Career**: Current and past roles, companies, and general skill areas.
|
||||
|
||||
4. **Projects**: Projects I meaningfully built or committed to. Ideally ONE entry per project. Include what it does, current status, and any key decisions. Use the project name or a short descriptor as the first words of the entry.
|
||||
|
||||
5. **Preferences**: Opinions, tastes, and working-style preferences that apply broadly.
|
||||
|
||||
## Format:
|
||||
|
||||
Use section headers for each category. Within each category, list one entry per line, sorted by oldest date first. Format each line as:
|
||||
|
||||
[YYYY-MM-DD] - Entry content here.
|
||||
|
||||
If no date is known, use [unknown] instead.
|
||||
|
||||
## Output:
|
||||
- Wrap the entire export in a single code block for easy copying.
|
||||
- After the code block, state whether this is the complete set or if more remain.`
|
||||
|
||||
const normalizeManualMemoryImport = (value: string) => {
|
||||
const trimmed = value.trim()
|
||||
const codeBlockMatch = trimmed.match(/```(?:[\w-]+)?\s*([\s\S]*?)```/)
|
||||
|
||||
return (codeBlockMatch?.[1] ?? trimmed).trim()
|
||||
}
|
||||
|
||||
const OpenAILogo = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
aria-label="ChatGPT Logo"
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>OpenAI</title>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ClaudeLogo = ({ className }: { className?: string }) => (
|
||||
<img alt="Claude" className={className} src="./claude.png" />
|
||||
)
|
||||
|
||||
const GeminiLogo = ({ className }: { className?: string }) => (
|
||||
<img alt="Gemini" className={className} src="./gemini.png" />
|
||||
)
|
||||
|
||||
const XLogo = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
aria-label="X Twitter Logo"
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>X Twitter Logo</title>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const GrokLogo = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
aria-label="Grok Logo"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Grok</title>
|
||||
<path
|
||||
d="M17.85 6.35A7.3 7.3 0 0 0 6.2 14.75"
|
||||
stroke="white"
|
||||
strokeLinecap="square"
|
||||
strokeWidth="2.7"
|
||||
/>
|
||||
<path
|
||||
d="M6.15 17.65A7.3 7.3 0 0 0 17.8 9.25"
|
||||
stroke="white"
|
||||
strokeLinecap="square"
|
||||
strokeWidth="2.7"
|
||||
/>
|
||||
<path
|
||||
d="M3.8 20.2L20.2 3.8"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2.4"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChatAppsLogo = ({ className }: { className?: string }) => (
|
||||
<div className={`relative h-5 w-[42px] shrink-0 ${className || ""}`}>
|
||||
<div className="absolute left-0 top-0 flex h-5 w-5 items-center justify-center rounded-[7px] border border-[#FFFFFF1A] bg-[#214E54] shadow-[0_0_0_1px_rgba(0,0,0,0.35),0_4px_12px_rgba(0,0,0,0.25)]">
|
||||
<OpenAILogo className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<div className="absolute left-[13px] top-0 flex h-5 w-5 items-center justify-center rounded-[7px] border border-[#FFFFFF1A] bg-[#2A1710] shadow-[0_0_0_1px_rgba(0,0,0,0.35),0_4px_12px_rgba(0,0,0,0.25)]">
|
||||
<ClaudeLogo className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="absolute left-[26px] top-0 flex h-5 w-5 items-center justify-center rounded-[7px] border border-[#FFFFFF1A] bg-[#111820] shadow-[0_0_0_1px_rgba(0,0,0,0.35),0_4px_12px_rgba(0,0,0,0.25)]">
|
||||
<GrokLogo className="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ImportCard = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<button
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-between gap-3 transition-colors duration-200 hover:bg-[#5B7EF520]"
|
||||
style={{
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<div className="text-left min-w-0">
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
{icon}
|
||||
{title}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="m-0 text-[14px] text-[#737373] leading-tight">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<RightArrow className="size-4 shrink-0 mt-1" />
|
||||
</button>
|
||||
)
|
||||
|
||||
function App() {
|
||||
const [userSignedIn, setUserSignedIn] = useState<boolean>(false)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
|
|
@ -80,6 +241,14 @@ function App() {
|
|||
const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">(
|
||||
"save",
|
||||
)
|
||||
const [showChatAppImports, setShowChatAppImports] = useState<boolean>(false)
|
||||
const [manualImportProvider, setManualImportProvider] =
|
||||
useState<ManualImportProvider | null>(null)
|
||||
const [manualImportText, setManualImportText] = useState<string>("")
|
||||
const [manualImportSaving, setManualImportSaving] = useState<boolean>(false)
|
||||
const [manualImportSaved, setManualImportSaved] = useState<boolean>(false)
|
||||
const [manualImportCopied, setManualImportCopied] = useState<boolean>(false)
|
||||
const [manualImportError, setManualImportError] = useState<string>("")
|
||||
const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false)
|
||||
const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] =
|
||||
useState<boolean>(false)
|
||||
|
|
@ -196,6 +365,12 @@ function App() {
|
|||
// biome-ignore lint/correctness/useExhaustiveDependencies: close space selector when tab changes
|
||||
useEffect(() => {
|
||||
setShowProjectSelector(false)
|
||||
setShowChatAppImports(false)
|
||||
setManualImportProvider(null)
|
||||
setManualImportText("")
|
||||
setManualImportSaved(false)
|
||||
setManualImportCopied(false)
|
||||
setManualImportError("")
|
||||
}, [activeTab])
|
||||
|
||||
const handleSaveCurrentPage = async () => {
|
||||
|
|
@ -263,6 +438,125 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleTwitterBookmarksImport = async () => {
|
||||
const targetUrl = "https://x.com/i/bookmarks"
|
||||
|
||||
try {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
|
||||
const isOnBookmarksPage =
|
||||
activeTab?.url?.includes("x.com/i/bookmarks") ||
|
||||
activeTab?.url?.includes("twitter.com/i/bookmarks")
|
||||
|
||||
if (isOnBookmarksPage && activeTab?.id) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTab.id, {
|
||||
action: MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to send message to content script:", error)
|
||||
const intentExpiry = Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]: intentExpiry,
|
||||
})
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const intentExpiry = Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]: intentExpiry,
|
||||
})
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error opening Twitter import:", error)
|
||||
try {
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
} catch (fallbackError) {
|
||||
console.error("Failed to open bookmarks page:", fallbackError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenManualMemoryImport = (provider: ManualImportProvider) => {
|
||||
setManualImportProvider(provider)
|
||||
setManualImportText("")
|
||||
setManualImportSaved(false)
|
||||
setManualImportCopied(false)
|
||||
setManualImportError("")
|
||||
}
|
||||
|
||||
const handleCloseManualMemoryImport = () => {
|
||||
setManualImportProvider(null)
|
||||
setManualImportText("")
|
||||
setManualImportSaved(false)
|
||||
setManualImportCopied(false)
|
||||
setManualImportError("")
|
||||
}
|
||||
|
||||
const handleCopyManualImportPrompt = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(manualMemoryImportPrompt)
|
||||
setManualImportCopied(true)
|
||||
window.setTimeout(() => setManualImportCopied(false), 1600)
|
||||
} catch (error) {
|
||||
console.error("Failed to copy memory import prompt:", error)
|
||||
setManualImportError(
|
||||
"Could not copy prompt. Select and copy it manually.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleManualMemoryImportSave = async () => {
|
||||
if (!manualImportProvider) return
|
||||
|
||||
const content = normalizeManualMemoryImport(manualImportText)
|
||||
if (!content) {
|
||||
setManualImportError("Paste the exported memories first.")
|
||||
return
|
||||
}
|
||||
|
||||
setManualImportSaving(true)
|
||||
setManualImportError("")
|
||||
|
||||
try {
|
||||
const providerConfig = manualImportProviderConfig[manualImportProvider]
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.SAVE_MEMORY,
|
||||
actionSource: providerConfig.actionSource,
|
||||
data: {
|
||||
content,
|
||||
title: `${providerConfig.label} memories import`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "Could not add memories")
|
||||
}
|
||||
|
||||
setManualImportSaved(true)
|
||||
window.setTimeout(() => {
|
||||
handleCloseManualMemoryImport()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error("Failed to add manual memory import:", error)
|
||||
setManualImportError(
|
||||
error instanceof Error ? error.message : "Could not add memories",
|
||||
)
|
||||
} finally {
|
||||
setManualImportSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
|
|
@ -641,136 +935,238 @@ function App() {
|
|||
</div>
|
||||
) : activeTab === "imports" ? (
|
||||
<div className="flex flex-col gap-4 min-h-[200px]">
|
||||
{/* Import Actions */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 hover:bg-[#5B7EF520]"
|
||||
style={{
|
||||
boxShadow:
|
||||
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
{manualImportProvider ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="m-0 text-base font-semibold text-white">
|
||||
Import{" "}
|
||||
{
|
||||
manualImportProviderConfig[manualImportProvider]
|
||||
.label
|
||||
}{" "}
|
||||
memories
|
||||
</h3>
|
||||
<p className="m-0 mt-1 text-xs leading-tight text-[#737373]">
|
||||
Copy the prompt, paste the response here, then add it
|
||||
to supermemory.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close manual import"
|
||||
className="shrink-0 bg-transparent border-none cursor-pointer p-1 text-[#737373] transition-colors hover:text-white"
|
||||
onClick={handleCloseManualMemoryImport}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="18"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-white">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-black text-xs">
|
||||
1
|
||||
</span>
|
||||
<span>Copy this prompt into chat</span>
|
||||
</div>
|
||||
<div
|
||||
className="relative overflow-hidden rounded-xl bg-black/70 p-3"
|
||||
style={{ boxShadow: cardShadow }}
|
||||
>
|
||||
<pre className="m-0 max-h-28 overflow-y-auto whitespace-pre-wrap pb-9 pr-1 text-xs leading-snug text-[#B7B7B7] font-[Space_Grotesk,-apple-system,BlinkMacSystemFont,Segoe_UI,Roboto,sans-serif]">
|
||||
{manualMemoryImportPrompt}
|
||||
</pre>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-linear-to-t from-black/80 to-transparent" />
|
||||
<button
|
||||
className="absolute bottom-3 right-3 flex items-center gap-1.5 rounded-lg border-none bg-[#FFFFFF1A] px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-[#FFFFFF26]"
|
||||
onClick={handleCopyManualImportPrompt}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="14"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
>
|
||||
<rect
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
width="14"
|
||||
x="8"
|
||||
y="8"
|
||||
/>
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
{manualImportCopied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-white">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-black text-xs">
|
||||
2
|
||||
</span>
|
||||
<span>Paste results below</span>
|
||||
</div>
|
||||
<textarea
|
||||
className="min-h-32 w-full resize-none rounded-xl border border-[#FFFFFF14] bg-[#FFFFFF08] p-3 text-sm leading-snug text-white outline-none placeholder:text-[#737373] focus:border-[#5B7EF566]"
|
||||
onChange={(event) => {
|
||||
setManualImportText(event.target.value)
|
||||
setManualImportError("")
|
||||
}}
|
||||
placeholder="Paste your memory details here"
|
||||
value={manualImportText}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{manualImportError && (
|
||||
<p className="m-0 text-xs leading-tight text-red-300">
|
||||
{manualImportError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<button
|
||||
className="rounded-lg border-none bg-transparent px-4 py-2 text-sm font-medium text-[#8A8C90] transition-colors hover:bg-[#FFFFFF0D] hover:text-white"
|
||||
onClick={handleCloseManualMemoryImport}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="rounded-xl border-none px-3.5 py-2 text-xs font-medium text-white transition-opacity disabled:cursor-not-allowed disabled:opacity-80"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
|
||||
boxShadow:
|
||||
"1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
|
||||
}}
|
||||
disabled={manualImportSaving || manualImportSaved}
|
||||
onClick={handleManualMemoryImportSave}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex items-center gap-2 whitespace-nowrap">
|
||||
{manualImportSaved ? (
|
||||
"Done"
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-4 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 20 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.4295 6.3108H12.1691V0H9.82324V6.84734C9.82324 7.57459 10.1103 8.27304 10.6206 8.78766L16.549 14.7664L18.2077 13.0936L13.8291 8.6779H19.4309V6.31219L19.4295 6.3108Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.08945 2.90808L5.46808 7.32387H-0.133789V9.68958H7.12669V16.0003H9.4725V9.15304C9.4725 8.42574 9.18541 7.72728 8.67512 7.21272L2.74809 1.23535L1.08945 2.90808Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
{manualImportSaving
|
||||
? "Saving..."
|
||||
: "Save to supermemory"}
|
||||
</>
|
||||
)}
|
||||
{manualImportSaved && (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="14"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2.4"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : showChatAppImports ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<ImportCard
|
||||
icon={<ClaudeLogo className="w-4 h-4 shrink-0" />}
|
||||
title="Import Claude Memories"
|
||||
description="Open 'view and manage' > save your memories to supermemory"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://claude.ai/settings/capabilities",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ImportCard
|
||||
icon={<OpenAILogo className="w-3 h-3.5 shrink-0" />}
|
||||
title="Import ChatGPT Memories"
|
||||
description="Open 'manage' > save your memories to supermemory"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://chatgpt.com/#settings/Personalization",
|
||||
})
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="text-left">
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
<svg
|
||||
aria-label="ChatGPT Logo"
|
||||
className="w-3 h-3.5 shrink-0"
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>OpenAI</title>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
Import ChatGPT Memories
|
||||
</p>
|
||||
<p className="m-0 text-[14px] text-[#737373] leading-tight">
|
||||
open 'manage' > save your memories to supermemory
|
||||
</p>
|
||||
</div>
|
||||
<RightArrow className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
className="w-full p-4 bg-[#5B7EF50A] text-white border-none rounded-xl text-sm cursor-pointer flex items-start justify-start transition-colors duration-200 outline-none appearance-none hover:bg-[#5B7EF520] focus:outline-none"
|
||||
style={{
|
||||
boxShadow:
|
||||
"2px 2px 2px 0 rgba(0, 0, 0, 0.50) inset, -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset",
|
||||
/>
|
||||
<ImportCard
|
||||
icon={<GrokLogo className="w-4 h-4 shrink-0" />}
|
||||
title="Import Grok Memories"
|
||||
description="Open 'Memory from your chats' > save your memories to supermemory"
|
||||
onClick={() => {
|
||||
chrome.tabs.create({
|
||||
url: "https://grok.com/?_s=data&sm_grok_import=memories",
|
||||
})
|
||||
}}
|
||||
onClick={async () => {
|
||||
const targetUrl = "https://x.com/i/bookmarks"
|
||||
|
||||
try {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
|
||||
const isOnBookmarksPage =
|
||||
activeTab?.url?.includes("x.com/i/bookmarks") ||
|
||||
activeTab?.url?.includes("twitter.com/i/bookmarks")
|
||||
|
||||
if (isOnBookmarksPage && activeTab?.id) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTab.id, {
|
||||
action: MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL,
|
||||
})
|
||||
} catch (error) {
|
||||
// Content script may not be loaded yet, fall back to intent-based approach
|
||||
console.error(
|
||||
"Failed to send message to content script:",
|
||||
error,
|
||||
)
|
||||
const intentExpiry =
|
||||
Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]:
|
||||
intentExpiry,
|
||||
})
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const intentExpiry =
|
||||
Date.now() + UI_CONFIG.IMPORT_INTENT_TTL
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL]:
|
||||
intentExpiry,
|
||||
})
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error opening Twitter import:", error)
|
||||
// Fallback: try to open the bookmarks page anyway
|
||||
try {
|
||||
await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
})
|
||||
} catch (fallbackError) {
|
||||
console.error(
|
||||
"Failed to open bookmarks page:",
|
||||
fallbackError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="text-left">
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
<svg
|
||||
aria-label="X Twitter Logo"
|
||||
className="w-3 h-3.5 shrink-0"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>X Twitter Logo</title>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
Import X/Twitter Bookmarks
|
||||
</p>
|
||||
<p className="m-0 text-[14px] text-[#737373] leading-tight">
|
||||
Opens import dialog automatically
|
||||
</p>
|
||||
</div>
|
||||
<RightArrow className="size-4" />
|
||||
</button>
|
||||
/>
|
||||
<ImportCard
|
||||
icon={
|
||||
<GeminiLogo className="w-4 h-4 shrink-0 rounded-[4px]" />
|
||||
}
|
||||
title="Import Gemini Memories"
|
||||
description="Paste memories exported from Gemini chat"
|
||||
onClick={() => handleOpenManualMemoryImport("gemini")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ImportCard
|
||||
icon={<ChatAppsLogo />}
|
||||
title="Import Chat Memories"
|
||||
description="Import your ChatGPT, Claude, Grok, and Gemini memories"
|
||||
onClick={() => setShowChatAppImports(true)}
|
||||
/>
|
||||
<ImportCard
|
||||
icon={<XLogo className="w-3 h-3.5 shrink-0" />}
|
||||
title="Import X/Twitter Bookmarks"
|
||||
description="Opens import dialog automatically"
|
||||
onClick={handleTwitterBookmarksImport}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[200px] pl-1">
|
||||
|
|
|
|||
BIN
apps/browser-extension/public/claude.png
Normal file
BIN
apps/browser-extension/public/claude.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
apps/browser-extension/public/gemini.png
Normal file
BIN
apps/browser-extension/public/gemini.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -58,6 +58,7 @@ export const DOMAINS = {
|
|||
TWITTER: ["x.com", "twitter.com"],
|
||||
CHATGPT: ["chatgpt.com", "chat.openai.com"],
|
||||
CLAUDE: ["claude.ai"],
|
||||
GROK: ["grok.com", "x.ai"],
|
||||
T3: ["t3.chat"],
|
||||
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ export default defineConfig({
|
|||
"*://api.supermemory.ai/*",
|
||||
"*://chatgpt.com/*",
|
||||
"*://chat.openai.com/*",
|
||||
"*://grok.com/*",
|
||||
"*://*.grok.com/*",
|
||||
"*://x.ai/*",
|
||||
"*://*.x.ai/*",
|
||||
"https://*.posthog.com/*",
|
||||
],
|
||||
web_accessible_resources: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue