import { useQueryClient } from "@tanstack/react-query"
import { useEffect, useState } from "react"
import "./App.css"
import { validateAuthToken } from "../../utils/api"
import {
getSupermemoryLoginUrl,
MESSAGE_TYPES,
STORAGE_KEYS,
UI_CONFIG,
} from "../../utils/constants"
import {
useDefaultProject,
useProjects,
useSetDefaultProject,
useUserData,
} from "../../utils/query-hooks"
import {
autoSearchEnabled as autoSearchEnabledStorage,
autoCapturePromptsEnabled as autoCapturePromptsEnabledStorage,
bearerToken,
defaultProject as defaultProjectStorage,
userData as userDataStorage,
} from "../../utils/storage"
import type { Project } from "../../utils/types"
import { RightArrow } from "@/components/icons"
const Tooltip = ({
children,
content,
}: {
children: React.ReactNode
content: string
}) => {
const [isVisible, setIsVisible] = useState(false)
return (
setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
className="cursor-help bg-transparent border-none p-0 text-left"
>
{children}
setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
className="cursor-help bg-transparent border-none p-0 text-[#737373] transition-colors"
>
More information
{isVisible && (
)}
)
}
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 }) => (
)
const GeminiLogo = ({ className }: { className?: string }) => (
)
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
}) => (
{icon}
{title}
{description && (
{description}
)}
)
function App() {
const [userSignedIn, setUserSignedIn] = useState(false)
const [loading, setLoading] = useState(true)
const [showProjectSelector, setShowProjectSelector] = useState(false)
const [currentUrl, setCurrentUrl] = useState("")
const [currentTitle, setCurrentTitle] = useState("")
const [saving, setSaving] = useState(false)
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)
const [authInvalidated, setAuthInvalidated] = useState(false)
const [saveError, setSaveError] = useState(null)
const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({
enabled: userSignedIn,
})
const { data: defaultProject } = useDefaultProject({
enabled: userSignedIn,
})
const { data: userData, isLoading: loadingUserData } = useUserData({
enabled: userSignedIn,
})
const setDefaultProjectMutation = useSetDefaultProject()
// biome-ignore lint/correctness/useExhaustiveDependencies: suppress dependency analysis
useEffect(() => {
const checkAuthStatus = async () => {
try {
const [token, autoSearch, autoCapturePrompts] = await Promise.all([
bearerToken.getValue(),
autoSearchEnabledStorage.getValue(),
autoCapturePromptsEnabledStorage.getValue(),
])
const hasToken = !!token
if (hasToken) {
const isTokenValid = await validateAuthToken()
if (isTokenValid) {
setUserSignedIn(true)
setAuthInvalidated(false)
} else {
await Promise.all([
bearerToken.removeValue(),
userDataStorage.removeValue(),
defaultProjectStorage.removeValue(),
])
queryClient.clear()
setUserSignedIn(false)
setAuthInvalidated(true)
}
} else {
setUserSignedIn(false)
setAuthInvalidated(false)
}
setAutoSearchEnabled(autoSearch ?? false)
setAutoCapturePromptsEnabled(autoCapturePrompts ?? false)
} catch (error) {
console.error("Error checking auth status:", error)
setUserSignedIn(false)
setAuthInvalidated(false)
} finally {
setLoading(false)
}
}
const getCurrentTab = async () => {
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
})
if (tabs.length > 0 && tabs[0].url && tabs[0].title) {
setCurrentUrl(tabs[0].url)
setCurrentTitle(tabs[0].title)
}
} catch (error) {
console.error("Error getting current tab:", error)
}
}
checkAuthStatus()
getCurrentTab()
}, [])
const handleProjectSelect = (project: Project) => {
setDefaultProjectMutation.mutate(project, {
onSuccess: () => {
setShowProjectSelector(false)
},
onError: (error) => {
console.error("Error setting default project:", error)
},
})
}
const handleShowProjectSelector = () => {
setShowProjectSelector(true)
}
// Reconcile stored default against live list: reset if deleted, refresh if renamed.
useEffect(() => {
if (projects.length === 0) return
if (!defaultProject) {
setDefaultProjectMutation.mutate(projects[0])
return
}
const live = projects.find((p) => p.id === defaultProject.id)
if (!live) {
setDefaultProjectMutation.mutate(projects[0])
} else if (
live.name !== defaultProject.name ||
live.containerTag !== defaultProject.containerTag
) {
setDefaultProjectMutation.mutate(live)
}
}, [defaultProject, projects, setDefaultProjectMutation])
// 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 () => {
setSaving(true)
setSaveError(null)
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
})
const tab = tabs[0]
let response: { success?: boolean; error?: string } | undefined
if (tab?.id) {
try {
response = await chrome.tabs.sendMessage(tab.id, {
action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: "popup",
})
} catch (contentScriptError) {
console.warn("Content script save failed:", contentScriptError)
}
}
if (response && !response.success) {
throw new Error(response.error || "Failed to save current page")
}
if (!response) {
const fallbackUrl = tab?.url || currentUrl
const fallbackTitle = tab?.title || currentTitle || "Current Page"
if (!fallbackUrl) {
throw new Error("No active page URL found")
}
response = await chrome.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: "popup_fallback",
data: {
url: fallbackUrl,
title: fallbackTitle,
content: `${fallbackTitle}\n\n${fallbackUrl}`,
},
})
}
if (response?.success) {
if (tab?.id) {
await chrome.tabs
.sendMessage(tab.id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "success",
})
.catch(() => undefined)
}
window.close()
return
}
throw new Error(response?.error || "Failed to save current page")
} catch (error) {
console.error("Failed to save current page:", error)
setSaveError(
error instanceof Error ? error.message : "Could not save page",
)
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
})
if (tabs.length > 0 && tabs[0].id) {
await chrome.tabs.sendMessage(tabs[0].id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "error",
})
}
} catch (toastError) {
console.error("Failed to show error toast:", toastError)
}
} finally {
setSaving(false)
}
}
const handleAutoSearchToggle = async (enabled: boolean) => {
try {
await autoSearchEnabledStorage.setValue(enabled)
setAutoSearchEnabled(enabled)
} catch (error) {
console.error("Error updating auto search setting:", error)
}
}
const handleAutoCapturePromptsToggle = async (enabled: boolean) => {
try {
await autoCapturePromptsEnabledStorage.setValue(enabled)
setAutoCapturePromptsEnabled(enabled)
} catch (error) {
console.error("Error updating auto capture prompts setting:", error)
}
}
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([
bearerToken.removeValue(),
userDataStorage.removeValue(),
defaultProjectStorage.removeValue(),
])
setUserSignedIn(false)
queryClient.clear()
} catch (error) {
console.error("Error signing out:", error)
}
}
if (loading) {
return (
)
}
return (
{userSignedIn ? (
{/* Tab Navigation */}
setActiveTab("save")}
type="button"
>
Save
setActiveTab("imports")}
type="button"
>
Imports
setActiveTab("settings")}
type="button"
>
Settings
{/* Tab Content */}
{activeTab === "save" ? (
{/* Current Page Info */}
{currentTitle || "Current Page"}
{currentUrl}
{/* Space Selection */}
Save to Space
{showProjectSelector && (
setShowProjectSelector(false)}
type="button"
>
Close
)}
{showProjectSelector ? (
{loadingProjects ? (
Loading spaces...
) : (
projects.map((project) => (
handleProjectSelect(project)}
type="button"
>
{project.name}
{defaultProject?.id === project.id && (
Selected
)}
))
)}
) : (
{defaultProject
? defaultProject.name
: "Select a space"}
Expand
)}
{/* Save Button at Bottom */}
Add to Supermemory
{saving ? "Saving..." : "Add to supermemory"}
{saveError && (
{saveError}
)}
) : activeTab === "imports" ? (
{manualImportProvider ? (
Import{" "}
{
manualImportProviderConfig[manualImportProvider]
.label
}{" "}
memories
Copy the prompt, paste the response here, then add it
to supermemory.
1
Copy this prompt into chat
{manualMemoryImportPrompt}
{manualImportCopied ? "Copied" : "Copy"}
{manualImportError && (
{manualImportError}
)}
Cancel
{manualImportSaved ? (
"Done"
) : (
<>
{manualImportSaving
? "Saving..."
: "Save to supermemory"}
>
)}
{manualImportSaved && (
)}
) : showChatAppImports ? (
}
title="Import Claude Memories"
description="Open 'view and manage' > save your memories to supermemory"
onClick={() => {
chrome.tabs.create({
url: "https://claude.ai/settings/capabilities",
})
}}
/>
}
title="Import ChatGPT Memories"
description="Open 'manage' > save your memories to supermemory"
onClick={() => {
chrome.tabs.create({
url: "https://chatgpt.com/#settings/Personalization",
})
}}
/>
}
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",
})
}}
/>
}
title="Import Gemini Memories"
description="Paste memories exported from Gemini chat"
onClick={() => handleOpenManualMemoryImport("gemini")}
/>
) : (
}
title="Import Chat Memories"
description="Import your ChatGPT, Claude, Grok, and Gemini memories"
onClick={() => setShowChatAppImports(true)}
/>
}
title="Import X/Twitter Bookmarks"
description="Opens import dialog automatically"
onClick={handleTwitterBookmarksImport}
/>
)}
) : (
{/* Account Section */}
{loadingUserData ? (
Loading account data...
) : userData?.email ? (
<>
Email
{userData.email}
>
) : (
No email found
)}
{/* Chat Integration Section */}
)}
) : (
{authInvalidated ? (
Session Expired
Logged out since authentication was invalidated. Please
login again.
) : (
Login to unlock all Chrome extension features
Save any page to your supermemory
Import all your Twitter / X Bookmarks
Import your ChatGPT Memories
)}
Having trouble logging in?{" "}
{
window.open("mailto:support@supermemory.ai", "_blank")
}}
type="button"
>
Reach Out to Us
{
chrome.tabs.create({
url: getSupermemoryLoginUrl(),
})
}}
type="button"
>
Sign in or create account
)}
)
}
export default App