diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index d124c84a..4fe41cbd 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -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('[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("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( + "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 = ` +
+ supermemory + Save to supermemory +
+ ` + + 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("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, diff --git a/apps/browser-extension/entrypoints/content/grok.ts b/apps/browser-extension/entrypoints/content/grok.ts new file mode 100644 index 00000000..378ad750 --- /dev/null +++ b/apps/browser-extension/entrypoints/content/grok.ts @@ -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( + "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( + "button, a, [role='button'], [role='tab']", + ) || matchingElement + + clickableElement.click() + return true + } + + return false +} + +function getGrokSettingsDialog() { + return Array.from( + document.querySelectorAll('[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('[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("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( + "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 = ` +
+ supermemory + Save to supermemory +
+ ` + + 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("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") + } +} diff --git a/apps/browser-extension/entrypoints/content/index.ts b/apps/browser-extension/entrypoints/content/index.ts index cba33ce2..1c863530 100644 --- a/apps/browser-extension/entrypoints/content/index.ts +++ b/apps/browser-extension/entrypoints/content/index.ts @@ -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() diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index ac646ff7..bcc2a910 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -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 }) => ( + + OpenAI + + +) + +const ClaudeLogo = ({ className }: { className?: string }) => ( + Claude +) + +const GeminiLogo = ({ className }: { className?: string }) => ( + Gemini +) + +const XLogo = ({ className }: { className?: string }) => ( + + X Twitter Logo + + +) + +const GrokLogo = ({ className }: { className?: string }) => ( + + Grok + + + + +) + +const ChatAppsLogo = ({ className }: { className?: string }) => ( +
+
+ +
+
+ +
+
+ +
+
+) + +const ImportCard = ({ + icon, + title, + description, + onClick, +}: { + icon: React.ReactNode + title: string + description?: string + onClick: () => void +}) => ( + +) + function App() { const [userSignedIn, setUserSignedIn] = useState(false) const [loading, setLoading] = useState(true) @@ -80,6 +241,14 @@ function App() { const [activeTab, setActiveTab] = useState<"save" | "imports" | "settings">( "save", ) + const [showChatAppImports, setShowChatAppImports] = useState(false) + const [manualImportProvider, setManualImportProvider] = + useState(null) + const [manualImportText, setManualImportText] = useState("") + const [manualImportSaving, setManualImportSaving] = useState(false) + const [manualImportSaved, setManualImportSaved] = useState(false) + const [manualImportCopied, setManualImportCopied] = useState(false) + const [manualImportError, setManualImportError] = useState("") const [autoSearchEnabled, setAutoSearchEnabled] = useState(false) const [autoCapturePromptsEnabled, setAutoCapturePromptsEnabled] = useState(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() { ) : activeTab === "imports" ? (
- {/* Import Actions */} -
-
- +
+ +
+
+ + 1 + + Copy this prompt into chat +
+
+
+													{manualMemoryImportPrompt}
+												
+
+ +
+
+ +
+
+ + 2 + + Paste results below +
+