From 5258cb74c895c5297fbfff594935741aeb14a915 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:56:46 +0000 Subject: [PATCH] chore(web): reduce the app to a redirect shell, drop the browser extension (#1651) chore(web): reduce the app to a redirect shell, drop the browser extension app.supermemory.ai now forwards everything to the console: plugin, OAuth and invite paths get an immediate 308 with the query intact, everything else shows a short notice first. Removes the browser extension workspace. chore(web): give the moved notice a proper design Hostnames become the headline, one primary action, a draining line for the countdown, DM Sans and the dot-grid backdrop from the brand. chore(docs): point docs and README at the console, drop stale sections Docs and both READMEs now link to console.supermemory.ai for API keys. Removed the company-brain docs tab with a redirect, and trimmed the README app section. chore(web): give the redirect five seconds --- README.md | 24 +- README.zh-CN.md | 22 +- apps/browser-extension/.env.example | 2 - apps/browser-extension/.gitignore | 26 - apps/browser-extension/README.md | 1 - apps/browser-extension/components/icons.tsx | 18 - .../entrypoints/background.ts | 353 -- .../entrypoints/content/chatgpt.ts | 783 --- .../entrypoints/content/claude.ts | 909 ---- .../entrypoints/content/gemini.ts | 664 --- .../entrypoints/content/grok.ts | 445 -- .../entrypoints/content/index.ts | 88 - .../entrypoints/content/memory-suggestion.ts | 446 -- .../entrypoints/content/shared.ts | 136 - .../entrypoints/content/t3.ts | 744 --- .../entrypoints/content/twitter.ts | 739 --- .../entrypoints/popup/App.css | 42 - .../entrypoints/popup/App.tsx | 1364 ----- .../entrypoints/popup/index.html | 13 - .../entrypoints/popup/main.tsx | 17 - .../entrypoints/popup/style.css | 21 - .../entrypoints/welcome/Welcome.tsx | 116 - .../entrypoints/welcome/index.html | 13 - .../entrypoints/welcome/main.tsx | 17 - .../entrypoints/welcome/welcome.css | 49 - .../fonts/SpaceGrotesk-Bold.ttf | Bin 86516 -> 0 bytes .../fonts/SpaceGrotesk-Light.ttf | Bin 86612 -> 0 bytes .../fonts/SpaceGrotesk-Medium.ttf | Bin 86612 -> 0 bytes .../fonts/SpaceGrotesk-Regular.ttf | Bin 86588 -> 0 bytes .../fonts/SpaceGrotesk-SemiBold.ttf | Bin 86572 -> 0 bytes .../fonts/SpaceGrotesk-VariableFont_wght.ttf | Bin 134112 -> 0 bytes apps/browser-extension/package.json | 35 - apps/browser-extension/public/claude.png | Bin 54846 -> 0 bytes .../public/dark-transparent.svg | 15 - apps/browser-extension/public/gemini.png | Bin 21598 -> 0 bytes apps/browser-extension/public/icon-128.png | Bin 229436 -> 0 bytes .../browser-extension/public/icon-128x128.png | Bin 25439 -> 0 bytes .../browser-extension/public/icon-144x144.png | Bin 31339 -> 0 bytes .../browser-extension/public/icon-152x152.png | Bin 34547 -> 0 bytes apps/browser-extension/public/icon-16.png | Bin 33814 -> 0 bytes .../browser-extension/public/icon-192x192.png | Bin 52097 -> 0 bytes .../browser-extension/public/icon-256x256.png | Bin 86437 -> 0 bytes .../browser-extension/public/icon-384x384.png | Bin 181681 -> 0 bytes apps/browser-extension/public/icon-48.png | Bin 12541 -> 0 bytes apps/browser-extension/public/icon-48x48.png | Bin 4882 -> 0 bytes .../browser-extension/public/icon-512x512.png | Bin 298891 -> 0 bytes apps/browser-extension/public/icon-72x72.png | Bin 9558 -> 0 bytes apps/browser-extension/public/icon-96x96.png | Bin 15486 -> 0 bytes .../public/logo-fullmark.svg | 15 - apps/browser-extension/public/new_logo.png | Bin 33050 -> 0 bytes apps/browser-extension/tsconfig.json | 9 - apps/browser-extension/utils/api.ts | 193 - apps/browser-extension/utils/constants.ts | 115 - apps/browser-extension/utils/memory-popup.ts | 93 - apps/browser-extension/utils/posthog.ts | 76 - apps/browser-extension/utils/query-client.ts | 25 - apps/browser-extension/utils/query-hooks.ts | 76 - .../utils/route-detection.ts | 116 - .../utils/search-request.test.ts | 19 - .../browser-extension/utils/search-request.ts | 14 - apps/browser-extension/utils/storage.ts | 120 - apps/browser-extension/utils/twitter-auth.ts | 87 - .../utils/twitter-expand-text.test.ts | 58 - .../browser-extension/utils/twitter-import.ts | 208 - apps/browser-extension/utils/twitter-utils.ts | 504 -- .../utils/twitter-video-variant.test.ts | 75 - apps/browser-extension/utils/types.ts | 165 - apps/browser-extension/utils/ui-components.ts | 791 --- apps/browser-extension/wxt.config.ts | 60 - apps/docs/company-brain/automations.mdx | 99 - apps/docs/company-brain/connectors.mdx | 64 - apps/docs/company-brain/outside-slack.mdx | 52 - apps/docs/company-brain/overview.mdx | 99 - apps/docs/company-brain/permissions.mdx | 91 - apps/docs/company-brain/setup.mdx | 87 - .../use-cases/acting-in-tools.mdx | 58 - .../docs/company-brain/use-cases/greeting.mdx | 53 - .../company-brain/use-cases/incidents.mdx | 48 - .../use-cases/knowledge-recall.mdx | 45 - .../use-cases/long-horizon-research.mdx | 49 - .../use-cases/meeting-recall.mdx | 40 - .../use-cases/meeting-scheduling.mdx | 43 - .../docs/company-brain/use-cases/overview.mdx | 47 - .../use-cases/sandbox-debugging.mdx | 51 - .../use-cases/support-escalation.mdx | 101 - apps/docs/company-brain/use-cases/support.mdx | 39 - apps/docs/docs.json | 37 +- .../docs/images/company-brain/cursor-icon.png | Bin 27918 -> 0 bytes .../company-brain/dhravya-slack-icon.jpg | Bin 139539 -> 0 bytes apps/docs/images/company-brain/plain-icon.png | Bin 6816 -> 0 bytes .../company-brain/signup-company-domain.png | Bin 328396 -> 0 bytes .../signup-research-complete.png | Bin 3270360 -> 0 bytes .../company-brain/signup-research-connect.png | Bin 696640 -> 0 bytes .../company-brain/signup-team-toggle.png | Bin 1110954 -> 0 bytes .../company-brain/slack-create-workspace.png | Bin 398560 -> 0 bytes .../company-brain/supermemory-slack-icon.png | Bin 330626 -> 0 bytes apps/docs/images/intro-company-brain-card.jpg | Bin 71729 -> 0 bytes ...brain.jpg => intro-developer-platform.jpg} | Bin apps/docs/index.mdx | 21 +- apps/docs/integrations/claude-code.mdx | 2 +- apps/docs/integrations/hermes.mdx | 2 +- apps/docs/integrations/openclaw.mdx | 4 +- apps/docs/integrations/opencode.mdx | 2 +- apps/docs/overview/comparison.mdx | 2 +- apps/docs/overview/use-cases.mdx | 3 +- apps/docs/overview/what-is-supermemory.mdx | 13 +- apps/docs/quickstart.mdx | 2 +- apps/docs/supermemory-mcp/setup.mdx | 2 - apps/web/.env.example | 4 +- apps/web/README.md | 6 +- apps/web/app/(app)/brain-home/page.tsx | 992 ---- apps/web/app/(app)/brain/page.tsx | 285 -- .../app/(app)/configure/[section]/page.tsx | 27 - apps/web/app/(app)/configure/layout.tsx | 15 - apps/web/app/(app)/configure/page.tsx | 4 - apps/web/app/(app)/connect/page.tsx | 250 - .../app/(app)/integrations/[card]/page.tsx | 13 - apps/web/app/(app)/integrations/page.tsx | 5 - apps/web/app/(app)/layout.tsx | 16 - .../app/(app)/onboarding/[...slug]/page.tsx | 5 - apps/web/app/(app)/onboarding/page.tsx | 625 --- apps/web/app/(app)/page.tsx | 5 - .../app/(app)/settings/integrations/page.tsx | 5 - apps/web/app/(app)/settings/page.tsx | 23 - apps/web/app/(auth)/login/new/page.tsx | 26 - apps/web/app/(auth)/login/page.tsx | 753 --- apps/web/app/api/emails/welcome/route.tsx | 19 - apps/web/app/api/mcp-icon/route.ts | 58 - apps/web/app/api/og/route.ts | 454 -- apps/web/app/global-error.tsx | 27 - apps/web/app/globals.css | 200 + apps/web/app/layout.tsx | 99 +- apps/web/app/manifest.ts | 25 - apps/web/app/not-found.tsx | 27 - apps/web/app/oauth/consent/ConsentCard.tsx | 703 --- apps/web/app/oauth/consent/page.tsx | 253 - .../app/org/invite/[invitationId]/page.tsx | 388 -- apps/web/app/page.tsx | 60 + apps/web/app/ref/[code]/page.tsx | 205 - apps/web/app/ref/page.tsx | 56 - apps/web/app/slack/link/page.tsx | 536 -- apps/web/app/upgrade-mcp/page.tsx | 328 -- apps/web/components.json | 20 - .../components/add-document/connections.tsx | 993 ---- apps/web/components/add-document/file.tsx | 392 -- apps/web/components/add-document/index.tsx | 702 --- apps/web/components/add-document/link.tsx | 310 -- apps/web/components/add-document/note.tsx | 97 - apps/web/components/add-space-modal.tsx | 356 -- .../animated-gradient-background.tsx | 64 - apps/web/components/app-experience.tsx | 922 ---- apps/web/components/bottom-nav.tsx | 204 - apps/web/components/brain-connector-icons.tsx | 123 - .../components/brain-home/brain-home-view.tsx | 679 --- .../brain-home/connections-board.tsx | 444 -- apps/web/components/brain-home/data.ts | 466 -- apps/web/components/brain-home/widgets.tsx | 639 --- apps/web/components/brain-trial-pill.tsx | 45 - apps/web/components/chat/attachments.ts | 82 - apps/web/components/chat/chat-empty-state.tsx | 84 - .../chat/chat-graph-context-rail.tsx | 63 - .../components/chat/home-chat-composer.tsx | 170 - apps/web/components/chat/index.tsx | 2194 -------- apps/web/components/chat/input/actions.tsx | 66 - .../chat/input/chain-of-thought.tsx | 213 - apps/web/components/chat/input/index.tsx | 601 --- .../components/chat/message/agent-message.tsx | 1506 ------ .../message/memory-search-result-card.tsx | 103 - .../chat/message/message-actions.tsx | 80 - .../chat/message/related-memories.tsx | 70 - .../components/chat/message/user-message.tsx | 193 - apps/web/components/chat/model-selector.tsx | 193 - .../components/chat/reasoning-selector.tsx | 73 - apps/web/components/company-brain-header.tsx | 600 --- apps/web/components/configure-view.tsx | 232 - apps/web/components/connect-ai-modal.tsx | 927 ---- apps/web/components/dashboard-view.tsx | 1648 ------ apps/web/components/digests-view.tsx | 479 -- .../components/directory/connector-card.tsx | 82 - .../web/components/directory/section-rail.tsx | 117 - .../document-cards/file-preview.tsx | 135 - .../document-cards/google-docs-preview.tsx | 46 - .../components/document-cards/mcp-preview.tsx | 59 - .../document-cards/note-preview.tsx | 52 - .../document-cards/notion-preview.tsx | 219 - .../document-cards/plugin-preview.tsx | 48 - .../document-cards/tweet-preview.tsx | 245 - .../document-cards/website-preview.tsx | 43 - .../document-cards/youtube-preview.tsx | 53 - apps/web/components/document-icon.tsx | 401 -- .../document-modal/content/google-doc.tsx | 63 - .../document-modal/content/image-preview.tsx | 96 - .../document-modal/content/index.tsx | 153 - .../document-modal/content/notion-doc.tsx | 27 - .../components/document-modal/content/pdf.tsx | 170 - .../document-modal/content/plugin-content.tsx | 182 - .../content/text-editor-content.tsx | 91 - .../document-modal/content/tweet.tsx | 63 - .../document-modal/content/web-page.tsx | 27 - .../document-modal/content/yt-video.tsx | 89 - .../document-modal/document-icon.tsx | 10 - .../document-modal/graph-list-memories.tsx | 373 -- apps/web/components/document-modal/index.tsx | 481 -- .../web/components/document-modal/summary.tsx | 85 - apps/web/components/document-modal/title.tsx | 60 - .../components/documents-command-palette.tsx | 450 -- apps/web/components/edit-space-modal.tsx | 206 - apps/web/components/ensure-workspace.tsx | 74 - apps/web/components/error-boundary.tsx | 33 - apps/web/components/feedback-modal.tsx | 179 - apps/web/components/fullscreen-note-modal.tsx | 225 - apps/web/components/granola-connect-modal.tsx | 221 - apps/web/components/graph-layout-view.tsx | 85 - apps/web/components/header.tsx | 608 --- apps/web/components/highlights-card.tsx | 405 -- apps/web/components/initial-header.tsx | 79 - apps/web/components/integration-icons.tsx | 113 - apps/web/components/integrations-view.tsx | 4468 ----------------- .../components/integrations/chrome-detail.tsx | 95 - .../components/integrations/install-steps.tsx | 174 - .../integrations/integration-grid-card.tsx | 76 - .../integrations/plugins-detail.tsx | 882 ---- .../integrations/raycast-detail.tsx | 141 - .../integrations/raycast-setup-modal.tsx | 132 - .../integrations/shortcuts-detail.tsx | 382 -- apps/web/components/login-tools-panel.tsx | 57 - .../claude-desktop-manual-timeline.tsx | 238 - .../components/mcp-modal/mcp-detail-view.tsx | 686 --- apps/web/components/memories-grid.tsx | 1446 ------ .../components/memory-graph/graph-card.tsx | 206 - .../memory-graph/hooks/use-graph-api.ts | 225 - apps/web/components/memory-graph/index.ts | 18 - .../memory-graph/memory-graph-wrapper.tsx | 87 - apps/web/components/nova/auto-space-icon.tsx | 37 - apps/web/components/nova/bg-grad.tsx | 150 - apps/web/components/nova/nova-empty-state.tsx | 204 - apps/web/components/nova/nova-orb.tsx | 42 - .../company-brain-onboarding.tsx | 935 ---- .../onboarding-brain/onboarding-confetti.tsx | 53 - .../onboarding-brain/research-action-rail.tsx | 793 --- .../onboarding-brain/setup-call-button.tsx | 32 - .../web/components/onboarding-brain/shell.tsx | 157 - .../onboarding-brain/slack-handoff.tsx | 61 - .../onboarding-brain/step-about.tsx | 459 -- .../onboarding-brain/step-ingest.tsx | 576 --- .../onboarding-brain/step-sources.tsx | 1414 ------ .../components/onboarding-brain/step-team.tsx | 331 -- .../onboarding-brain/step-trial.tsx | 88 - apps/web/components/onboarding-brain/types.ts | 163 - .../onboarding/x-bookmarks-detail-view.tsx | 111 - apps/web/components/orbit-memory.tsx | 582 --- apps/web/components/org-plan-badge.tsx | 36 - apps/web/components/pwa-install-prompt.tsx | 429 -- apps/web/components/query-client.tsx | 23 - apps/web/components/quick-note-card.tsx | 369 -- .../components/remove-connection-dialog.tsx | 168 - apps/web/components/review-memories-card.tsx | 79 - apps/web/components/review-memories-modal.tsx | 477 -- apps/web/components/select-spaces-modal.tsx | 1867 ------- apps/web/components/settings/account.tsx | 1352 ----- apps/web/components/settings/api-keys.tsx | 669 --- apps/web/components/settings/billing.tsx | 1976 -------- .../web/components/settings/cancel-reasons.ts | 19 - .../settings/company-brain-automations.tsx | 1134 ----- .../settings/company-brain-connections.tsx | 1299 ----- .../settings/company-brain-models.tsx | 547 -- .../settings/company-brain-proactivity.tsx | 321 -- .../settings/company-brain-skills.tsx | 447 -- .../settings/company-brain-skills/domain.ts | 251 - .../company-brain-skills/skill-editor.tsx | 511 -- .../company-brain-skills/skill-row.tsx | 83 - .../components/settings/connections-mcp.tsx | 757 --- apps/web/components/settings/integrations.tsx | 581 --- .../settings/mcp-directory-browser.tsx | 329 -- apps/web/components/settings/org-context.tsx | 127 - .../settings/proactiveness-icon.tsx | 21 - .../web/components/settings/proactiveness.tsx | 26 - .../components/settings/settings-content.tsx | 791 --- .../components/settings/settings-modal.tsx | 147 - .../settings/settings-org-switcher.tsx | 156 - apps/web/components/settings/support.tsx | 217 - .../settings/sync-history-panel.tsx | 269 - .../components/settings/sync-status-badge.tsx | 95 - apps/web/components/settings/sync-utils.ts | 64 - .../components/settings/workspace-prompt.tsx | 186 - apps/web/components/setup-call-link.tsx | 45 - apps/web/components/share-modal.tsx | 552 -- apps/web/components/space-folder-icon.tsx | 34 - apps/web/components/space-glyph.tsx | 24 - apps/web/components/space-profile-modal.tsx | 39 - apps/web/components/space-profile-panel.tsx | 255 - apps/web/components/space-selector.tsx | 963 ---- apps/web/components/spinner.tsx | 6 - apps/web/components/superloader.tsx | 97 - .../web/components/text-editor/extensions.tsx | 100 - apps/web/components/text-editor/index.tsx | 193 - .../components/text-editor/slash-command.tsx | 300 -- .../components/text-editor/suggestions.tsx | 103 - apps/web/components/timeline-view.tsx | 665 --- apps/web/components/user-profile-menu.tsx | 263 - apps/web/components/utils.ts | 71 - apps/web/console-origin.ts | 12 + apps/web/globals.css | 606 --- apps/web/hooks/use-account-settings.ts | 106 - apps/web/hooks/use-brain-models.ts | 68 - apps/web/hooks/use-brain-settings.ts | 76 - apps/web/hooks/use-brain-skills.impl.ts | 187 - apps/web/hooks/use-brain-skills.ts | 3 - apps/web/hooks/use-brain-trial.ts | 83 - apps/web/hooks/use-company-brain.ts | 8 - apps/web/hooks/use-connection-health.ts | 29 - apps/web/hooks/use-connector-access.ts | 22 - apps/web/hooks/use-container-tags.ts | 24 - apps/web/hooks/use-delete-organization.ts | 26 - apps/web/hooks/use-digests.ts | 72 - apps/web/hooks/use-document-mutations.ts | 772 --- apps/web/hooks/use-full-document.ts | 29 - apps/web/hooks/use-inferred-memories.ts | 109 - apps/web/hooks/use-org-member-role.ts | 19 - apps/web/hooks/use-org-settings.ts | 74 - apps/web/hooks/use-org-summaries.ts | 38 - apps/web/hooks/use-personalization.ts | 494 -- apps/web/hooks/use-plugin-space-meta.ts | 95 - apps/web/hooks/use-processing-documents.ts | 96 - apps/web/hooks/use-project-mutations.ts | 289 -- apps/web/hooks/use-promo-code.ts | 82 - apps/web/hooks/use-research-status.ts | 66 - apps/web/hooks/use-reset-organization.ts | 43 - apps/web/hooks/use-space-context.ts | 115 - apps/web/hooks/use-space-profile.ts | 57 - apps/web/hooks/use-sync-runs.ts | 45 - apps/web/hooks/use-token-usage.ts | 115 - apps/web/hooks/use-trial-status.ts | 34 - apps/web/hooks/use-trigger-sync.ts | 97 - apps/web/instrumentation-client.ts | 53 - apps/web/lib/agent-space.test.ts | 175 - apps/web/lib/agent-space.ts | 335 -- apps/web/lib/analytics.ts | 287 -- apps/web/lib/billing-utils.ts | 243 - apps/web/lib/cal.ts | 8 - apps/web/lib/chat-auto-space.ts | 1 - apps/web/lib/chat-highlight-documents.ts | 234 - apps/web/lib/chat-memory-tools.test.ts | 177 - apps/web/lib/chat-memory-tools.ts | 375 -- apps/web/lib/chat-search-memory-results.ts | 96 - apps/web/lib/chat-space-label.ts | 16 - apps/web/lib/chat-stream-error.ts | 45 - apps/web/lib/chat-web-search-tools.ts | 9 - apps/web/lib/company-brain-entry.ts | 68 - apps/web/lib/configure-routes.ts | 38 - apps/web/lib/connector-availability.ts | 30 - apps/web/lib/connector-notify.ts | 44 - apps/web/lib/constants.ts | 28 - apps/web/lib/extract-urls.test.ts | 69 - apps/web/lib/file-cache.ts | 51 - apps/web/lib/fonts.ts | 55 - apps/web/lib/ingest-auto-space.ts | 52 - apps/web/lib/integration-routes.ts | 40 - apps/web/lib/mcp-client-setup.ts | 69 - apps/web/lib/mcp-directory.ts | 21 - apps/web/lib/mcp-icon-domains.json | 518 -- apps/web/lib/mcp-manual-instructions.ts | 125 - apps/web/lib/models.tsx | 93 - apps/web/lib/oauth-plugins.ts | 57 - apps/web/lib/plugin-catalog.ts | 203 - apps/web/lib/plugin-document.test.ts | 166 - apps/web/lib/plugin-document.ts | 826 --- apps/web/lib/plugin-space.ts | 249 - apps/web/lib/reviewer-password-login.ts | 15 - apps/web/lib/search-params.ts | 68 - apps/web/lib/source-annotations.test.ts | 92 - apps/web/lib/source-annotations.ts | 194 - apps/web/lib/theme-provider.tsx | 8 - apps/web/lib/url-helpers.test.ts | 73 - apps/web/lib/url-helpers.ts | 425 -- apps/web/lib/variants.ts | 95 - apps/web/lib/verify-session.ts | 47 - apps/web/lib/view-mode-context.tsx | 90 - apps/web/middleware.ts | 145 +- apps/web/next.config.ts | 84 +- apps/web/package.json | 100 +- apps/web/postcss.config.mjs | 5 - apps/web/public/add-connections.png | Bin 378476 -> 0 bytes apps/web/public/add-memory.png | Bin 337871 -> 0 bytes apps/web/public/bg-rectangle.png | Bin 6008962 -> 0 bytes apps/web/public/chat.png | Bin 188671 -> 0 bytes apps/web/public/dot-pattern.svg | 2126 -------- apps/web/public/icons/chrome-transparent.svg | 13 - apps/web/public/icons/share-graph.svg | 12 - apps/web/public/images/digest/feat-fs.svg | 135 - apps/web/public/images/digest/feat-memory.png | Bin 98355 -> 0 bytes .../public/images/digest/feat-profiles.png | Bin 117420 -> 0 bytes .../public/images/digest/feat-retrieval.png | Bin 125750 -> 0 bytes apps/web/public/images/digest/feat-router.png | Bin 108980 -> 0 bytes apps/web/public/images/digest/wordmark.png | Bin 42046 -> 0 bytes apps/web/public/images/extension-bg.png | Bin 39989 -> 0 bytes apps/web/public/images/extension-logo.png | Bin 33814 -> 0 bytes apps/web/public/images/gdrive.svg | 8 - apps/web/public/images/icon-16.png | Bin 33814 -> 0 bytes apps/web/public/images/ios-shortcuts.png | Bin 34394 -> 0 bytes apps/web/public/images/login-carousel-1.png | Bin 395589 -> 0 bytes apps/web/public/images/login-carousel-2.png | Bin 269962 -> 0 bytes apps/web/public/images/logo.png | Bin 102803 -> 0 bytes apps/web/public/images/notion.svg | 4 - apps/web/public/images/onedrive.svg | 1 - .../web/public/images/plugins/claude-code.svg | 11 - apps/web/public/images/plugins/clawdbot.svg | 86 - apps/web/public/images/plugins/codex.png | Bin 7578 -> 0 bytes apps/web/public/images/plugins/cursor.png | Bin 35690 -> 0 bytes apps/web/public/images/plugins/hermes.svg | 1 - apps/web/public/images/plugins/openclaw.svg | 86 - apps/web/public/images/plugins/opencode.svg | 1 - apps/web/public/images/poke.png | Bin 1258753 -> 0 bytes apps/web/public/images/sidebar.png | Bin 4777067 -> 0 bytes apps/web/public/landing-page.jpeg | Bin 694533 -> 0 bytes apps/web/public/mcp-icon.svg | 5 - .../mcp-supported-tools/antigravity.png | Bin 20266 -> 0 bytes .../public/mcp-supported-tools/chatgpt.png | Bin 11297 -> 0 bytes .../web/public/mcp-supported-tools/claude.png | Bin 54846 -> 0 bytes apps/web/public/mcp-supported-tools/cline.png | Bin 25126 -> 0 bytes apps/web/public/mcp-supported-tools/codex.png | Bin 7578 -> 0 bytes .../web/public/mcp-supported-tools/cursor.png | Bin 104215 -> 0 bytes .../public/mcp-supported-tools/gemini-cli.png | Bin 21598 -> 0 bytes .../guides/claude-desktop/step-1.png | Bin 652240 -> 0 bytes .../guides/claude-desktop/step-2.png | Bin 18883 -> 0 bytes .../guides/claude-desktop/step-3.png | Bin 65123 -> 0 bytes .../guides/claude-desktop/step-4.png | Bin 891222 -> 0 bytes .../web/public/mcp-supported-tools/vscode.png | Bin 70640 -> 0 bytes apps/web/public/mcp.png | Bin 470925 -> 0 bytes apps/web/public/onboarding/bg-gradient-0.png | Bin 3874273 -> 0 bytes apps/web/public/onboarding/bg-gradient-1.png | Bin 3881693 -> 0 bytes apps/web/public/onboarding/chrome-ext-1.png | Bin 581919 -> 0 bytes apps/web/public/onboarding/chrome-ext-2.png | Bin 376422 -> 0 bytes apps/web/public/onboarding/chrome-ext-3.png | Bin 733974 -> 0 bytes apps/web/public/onboarding/chrome.png | Bin 7284 -> 0 bytes apps/web/public/onboarding/connectors.png | Bin 13524 -> 0 bytes apps/web/public/onboarding/human-brain.png | Bin 6793 -> 0 bytes apps/web/public/onboarding/mcp.png | Bin 39215 -> 0 bytes apps/web/public/onboarding/plant.png | Bin 2921 -> 0 bytes apps/web/public/onboarding/search.png | Bin 3637 -> 0 bytes apps/web/public/onboarding/x.png | Bin 8972 -> 0 bytes apps/web/stores/chat.ts | 268 - apps/web/stores/highlights.ts | 35 - apps/web/stores/index.ts | 51 - apps/web/stores/indexeddb-storage.ts | 24 - apps/web/stores/quick-note-draft.ts | 52 - bun.lock | 2238 +-------- 447 files changed, 563 insertions(+), 89671 deletions(-) delete mode 100644 apps/browser-extension/.env.example delete mode 100644 apps/browser-extension/.gitignore delete mode 100644 apps/browser-extension/README.md delete mode 100644 apps/browser-extension/components/icons.tsx delete mode 100644 apps/browser-extension/entrypoints/background.ts delete mode 100644 apps/browser-extension/entrypoints/content/chatgpt.ts delete mode 100644 apps/browser-extension/entrypoints/content/claude.ts delete mode 100644 apps/browser-extension/entrypoints/content/gemini.ts delete mode 100644 apps/browser-extension/entrypoints/content/grok.ts delete mode 100644 apps/browser-extension/entrypoints/content/index.ts delete mode 100644 apps/browser-extension/entrypoints/content/memory-suggestion.ts delete mode 100644 apps/browser-extension/entrypoints/content/shared.ts delete mode 100644 apps/browser-extension/entrypoints/content/t3.ts delete mode 100644 apps/browser-extension/entrypoints/content/twitter.ts delete mode 100644 apps/browser-extension/entrypoints/popup/App.css delete mode 100644 apps/browser-extension/entrypoints/popup/App.tsx delete mode 100644 apps/browser-extension/entrypoints/popup/index.html delete mode 100644 apps/browser-extension/entrypoints/popup/main.tsx delete mode 100644 apps/browser-extension/entrypoints/popup/style.css delete mode 100644 apps/browser-extension/entrypoints/welcome/Welcome.tsx delete mode 100644 apps/browser-extension/entrypoints/welcome/index.html delete mode 100644 apps/browser-extension/entrypoints/welcome/main.tsx delete mode 100644 apps/browser-extension/entrypoints/welcome/welcome.css delete mode 100644 apps/browser-extension/fonts/SpaceGrotesk-Bold.ttf delete mode 100644 apps/browser-extension/fonts/SpaceGrotesk-Light.ttf delete mode 100644 apps/browser-extension/fonts/SpaceGrotesk-Medium.ttf delete mode 100644 apps/browser-extension/fonts/SpaceGrotesk-Regular.ttf delete mode 100644 apps/browser-extension/fonts/SpaceGrotesk-SemiBold.ttf delete mode 100644 apps/browser-extension/fonts/SpaceGrotesk-VariableFont_wght.ttf delete mode 100644 apps/browser-extension/package.json delete mode 100644 apps/browser-extension/public/claude.png delete mode 100644 apps/browser-extension/public/dark-transparent.svg delete mode 100644 apps/browser-extension/public/gemini.png delete mode 100644 apps/browser-extension/public/icon-128.png delete mode 100644 apps/browser-extension/public/icon-128x128.png delete mode 100644 apps/browser-extension/public/icon-144x144.png delete mode 100644 apps/browser-extension/public/icon-152x152.png delete mode 100644 apps/browser-extension/public/icon-16.png delete mode 100644 apps/browser-extension/public/icon-192x192.png delete mode 100644 apps/browser-extension/public/icon-256x256.png delete mode 100644 apps/browser-extension/public/icon-384x384.png delete mode 100644 apps/browser-extension/public/icon-48.png delete mode 100644 apps/browser-extension/public/icon-48x48.png delete mode 100644 apps/browser-extension/public/icon-512x512.png delete mode 100644 apps/browser-extension/public/icon-72x72.png delete mode 100644 apps/browser-extension/public/icon-96x96.png delete mode 100644 apps/browser-extension/public/logo-fullmark.svg delete mode 100644 apps/browser-extension/public/new_logo.png delete mode 100644 apps/browser-extension/tsconfig.json delete mode 100644 apps/browser-extension/utils/api.ts delete mode 100644 apps/browser-extension/utils/constants.ts delete mode 100644 apps/browser-extension/utils/memory-popup.ts delete mode 100644 apps/browser-extension/utils/posthog.ts delete mode 100644 apps/browser-extension/utils/query-client.ts delete mode 100644 apps/browser-extension/utils/query-hooks.ts delete mode 100644 apps/browser-extension/utils/route-detection.ts delete mode 100644 apps/browser-extension/utils/search-request.test.ts delete mode 100644 apps/browser-extension/utils/search-request.ts delete mode 100644 apps/browser-extension/utils/storage.ts delete mode 100644 apps/browser-extension/utils/twitter-auth.ts delete mode 100644 apps/browser-extension/utils/twitter-expand-text.test.ts delete mode 100644 apps/browser-extension/utils/twitter-import.ts delete mode 100644 apps/browser-extension/utils/twitter-utils.ts delete mode 100644 apps/browser-extension/utils/twitter-video-variant.test.ts delete mode 100644 apps/browser-extension/utils/types.ts delete mode 100644 apps/browser-extension/utils/ui-components.ts delete mode 100644 apps/browser-extension/wxt.config.ts delete mode 100644 apps/docs/company-brain/automations.mdx delete mode 100644 apps/docs/company-brain/connectors.mdx delete mode 100644 apps/docs/company-brain/outside-slack.mdx delete mode 100644 apps/docs/company-brain/overview.mdx delete mode 100644 apps/docs/company-brain/permissions.mdx delete mode 100644 apps/docs/company-brain/setup.mdx delete mode 100644 apps/docs/company-brain/use-cases/acting-in-tools.mdx delete mode 100644 apps/docs/company-brain/use-cases/greeting.mdx delete mode 100644 apps/docs/company-brain/use-cases/incidents.mdx delete mode 100644 apps/docs/company-brain/use-cases/knowledge-recall.mdx delete mode 100644 apps/docs/company-brain/use-cases/long-horizon-research.mdx delete mode 100644 apps/docs/company-brain/use-cases/meeting-recall.mdx delete mode 100644 apps/docs/company-brain/use-cases/meeting-scheduling.mdx delete mode 100644 apps/docs/company-brain/use-cases/overview.mdx delete mode 100644 apps/docs/company-brain/use-cases/sandbox-debugging.mdx delete mode 100644 apps/docs/company-brain/use-cases/support-escalation.mdx delete mode 100644 apps/docs/company-brain/use-cases/support.mdx delete mode 100644 apps/docs/images/company-brain/cursor-icon.png delete mode 100644 apps/docs/images/company-brain/dhravya-slack-icon.jpg delete mode 100644 apps/docs/images/company-brain/plain-icon.png delete mode 100644 apps/docs/images/company-brain/signup-company-domain.png delete mode 100644 apps/docs/images/company-brain/signup-research-complete.png delete mode 100644 apps/docs/images/company-brain/signup-research-connect.png delete mode 100644 apps/docs/images/company-brain/signup-team-toggle.png delete mode 100644 apps/docs/images/company-brain/slack-create-workspace.png delete mode 100644 apps/docs/images/company-brain/supermemory-slack-icon.png delete mode 100644 apps/docs/images/intro-company-brain-card.jpg rename apps/docs/images/{intro-company-brain.jpg => intro-developer-platform.jpg} (100%) delete mode 100644 apps/web/app/(app)/brain-home/page.tsx delete mode 100644 apps/web/app/(app)/brain/page.tsx delete mode 100644 apps/web/app/(app)/configure/[section]/page.tsx delete mode 100644 apps/web/app/(app)/configure/layout.tsx delete mode 100644 apps/web/app/(app)/configure/page.tsx delete mode 100644 apps/web/app/(app)/connect/page.tsx delete mode 100644 apps/web/app/(app)/integrations/[card]/page.tsx delete mode 100644 apps/web/app/(app)/integrations/page.tsx delete mode 100644 apps/web/app/(app)/layout.tsx delete mode 100644 apps/web/app/(app)/onboarding/[...slug]/page.tsx delete mode 100644 apps/web/app/(app)/onboarding/page.tsx delete mode 100644 apps/web/app/(app)/page.tsx delete mode 100644 apps/web/app/(app)/settings/integrations/page.tsx delete mode 100644 apps/web/app/(app)/settings/page.tsx delete mode 100644 apps/web/app/(auth)/login/new/page.tsx delete mode 100644 apps/web/app/(auth)/login/page.tsx delete mode 100644 apps/web/app/api/emails/welcome/route.tsx delete mode 100644 apps/web/app/api/mcp-icon/route.ts delete mode 100644 apps/web/app/api/og/route.ts delete mode 100644 apps/web/app/global-error.tsx create mode 100644 apps/web/app/globals.css delete mode 100644 apps/web/app/manifest.ts delete mode 100644 apps/web/app/not-found.tsx delete mode 100644 apps/web/app/oauth/consent/ConsentCard.tsx delete mode 100644 apps/web/app/oauth/consent/page.tsx delete mode 100644 apps/web/app/org/invite/[invitationId]/page.tsx create mode 100644 apps/web/app/page.tsx delete mode 100644 apps/web/app/ref/[code]/page.tsx delete mode 100644 apps/web/app/ref/page.tsx delete mode 100644 apps/web/app/slack/link/page.tsx delete mode 100644 apps/web/app/upgrade-mcp/page.tsx delete mode 100644 apps/web/components.json delete mode 100644 apps/web/components/add-document/connections.tsx delete mode 100644 apps/web/components/add-document/file.tsx delete mode 100644 apps/web/components/add-document/index.tsx delete mode 100644 apps/web/components/add-document/link.tsx delete mode 100644 apps/web/components/add-document/note.tsx delete mode 100644 apps/web/components/add-space-modal.tsx delete mode 100644 apps/web/components/animated-gradient-background.tsx delete mode 100644 apps/web/components/app-experience.tsx delete mode 100644 apps/web/components/bottom-nav.tsx delete mode 100644 apps/web/components/brain-connector-icons.tsx delete mode 100644 apps/web/components/brain-home/brain-home-view.tsx delete mode 100644 apps/web/components/brain-home/connections-board.tsx delete mode 100644 apps/web/components/brain-home/data.ts delete mode 100644 apps/web/components/brain-home/widgets.tsx delete mode 100644 apps/web/components/brain-trial-pill.tsx delete mode 100644 apps/web/components/chat/attachments.ts delete mode 100644 apps/web/components/chat/chat-empty-state.tsx delete mode 100644 apps/web/components/chat/chat-graph-context-rail.tsx delete mode 100644 apps/web/components/chat/home-chat-composer.tsx delete mode 100644 apps/web/components/chat/index.tsx delete mode 100644 apps/web/components/chat/input/actions.tsx delete mode 100644 apps/web/components/chat/input/chain-of-thought.tsx delete mode 100644 apps/web/components/chat/input/index.tsx delete mode 100644 apps/web/components/chat/message/agent-message.tsx delete mode 100644 apps/web/components/chat/message/memory-search-result-card.tsx delete mode 100644 apps/web/components/chat/message/message-actions.tsx delete mode 100644 apps/web/components/chat/message/related-memories.tsx delete mode 100644 apps/web/components/chat/message/user-message.tsx delete mode 100644 apps/web/components/chat/model-selector.tsx delete mode 100644 apps/web/components/chat/reasoning-selector.tsx delete mode 100644 apps/web/components/company-brain-header.tsx delete mode 100644 apps/web/components/configure-view.tsx delete mode 100644 apps/web/components/connect-ai-modal.tsx delete mode 100644 apps/web/components/dashboard-view.tsx delete mode 100644 apps/web/components/digests-view.tsx delete mode 100644 apps/web/components/directory/connector-card.tsx delete mode 100644 apps/web/components/directory/section-rail.tsx delete mode 100644 apps/web/components/document-cards/file-preview.tsx delete mode 100644 apps/web/components/document-cards/google-docs-preview.tsx delete mode 100644 apps/web/components/document-cards/mcp-preview.tsx delete mode 100644 apps/web/components/document-cards/note-preview.tsx delete mode 100644 apps/web/components/document-cards/notion-preview.tsx delete mode 100644 apps/web/components/document-cards/plugin-preview.tsx delete mode 100644 apps/web/components/document-cards/tweet-preview.tsx delete mode 100644 apps/web/components/document-cards/website-preview.tsx delete mode 100644 apps/web/components/document-cards/youtube-preview.tsx delete mode 100644 apps/web/components/document-icon.tsx delete mode 100644 apps/web/components/document-modal/content/google-doc.tsx delete mode 100644 apps/web/components/document-modal/content/image-preview.tsx delete mode 100644 apps/web/components/document-modal/content/index.tsx delete mode 100644 apps/web/components/document-modal/content/notion-doc.tsx delete mode 100644 apps/web/components/document-modal/content/pdf.tsx delete mode 100644 apps/web/components/document-modal/content/plugin-content.tsx delete mode 100644 apps/web/components/document-modal/content/text-editor-content.tsx delete mode 100644 apps/web/components/document-modal/content/tweet.tsx delete mode 100644 apps/web/components/document-modal/content/web-page.tsx delete mode 100644 apps/web/components/document-modal/content/yt-video.tsx delete mode 100644 apps/web/components/document-modal/document-icon.tsx delete mode 100644 apps/web/components/document-modal/graph-list-memories.tsx delete mode 100644 apps/web/components/document-modal/index.tsx delete mode 100644 apps/web/components/document-modal/summary.tsx delete mode 100644 apps/web/components/document-modal/title.tsx delete mode 100644 apps/web/components/documents-command-palette.tsx delete mode 100644 apps/web/components/edit-space-modal.tsx delete mode 100644 apps/web/components/ensure-workspace.tsx delete mode 100644 apps/web/components/error-boundary.tsx delete mode 100644 apps/web/components/feedback-modal.tsx delete mode 100644 apps/web/components/fullscreen-note-modal.tsx delete mode 100644 apps/web/components/granola-connect-modal.tsx delete mode 100644 apps/web/components/graph-layout-view.tsx delete mode 100644 apps/web/components/header.tsx delete mode 100644 apps/web/components/highlights-card.tsx delete mode 100644 apps/web/components/initial-header.tsx delete mode 100644 apps/web/components/integration-icons.tsx delete mode 100644 apps/web/components/integrations-view.tsx delete mode 100644 apps/web/components/integrations/chrome-detail.tsx delete mode 100644 apps/web/components/integrations/install-steps.tsx delete mode 100644 apps/web/components/integrations/integration-grid-card.tsx delete mode 100644 apps/web/components/integrations/plugins-detail.tsx delete mode 100644 apps/web/components/integrations/raycast-detail.tsx delete mode 100644 apps/web/components/integrations/raycast-setup-modal.tsx delete mode 100644 apps/web/components/integrations/shortcuts-detail.tsx delete mode 100644 apps/web/components/login-tools-panel.tsx delete mode 100644 apps/web/components/mcp-modal/claude-desktop-manual-timeline.tsx delete mode 100644 apps/web/components/mcp-modal/mcp-detail-view.tsx delete mode 100644 apps/web/components/memories-grid.tsx delete mode 100644 apps/web/components/memory-graph/graph-card.tsx delete mode 100644 apps/web/components/memory-graph/hooks/use-graph-api.ts delete mode 100644 apps/web/components/memory-graph/index.ts delete mode 100644 apps/web/components/memory-graph/memory-graph-wrapper.tsx delete mode 100644 apps/web/components/nova/auto-space-icon.tsx delete mode 100644 apps/web/components/nova/bg-grad.tsx delete mode 100644 apps/web/components/nova/nova-empty-state.tsx delete mode 100644 apps/web/components/nova/nova-orb.tsx delete mode 100644 apps/web/components/onboarding-brain/company-brain-onboarding.tsx delete mode 100644 apps/web/components/onboarding-brain/onboarding-confetti.tsx delete mode 100644 apps/web/components/onboarding-brain/research-action-rail.tsx delete mode 100644 apps/web/components/onboarding-brain/setup-call-button.tsx delete mode 100644 apps/web/components/onboarding-brain/shell.tsx delete mode 100644 apps/web/components/onboarding-brain/slack-handoff.tsx delete mode 100644 apps/web/components/onboarding-brain/step-about.tsx delete mode 100644 apps/web/components/onboarding-brain/step-ingest.tsx delete mode 100644 apps/web/components/onboarding-brain/step-sources.tsx delete mode 100644 apps/web/components/onboarding-brain/step-team.tsx delete mode 100644 apps/web/components/onboarding-brain/step-trial.tsx delete mode 100644 apps/web/components/onboarding-brain/types.ts delete mode 100644 apps/web/components/onboarding/x-bookmarks-detail-view.tsx delete mode 100644 apps/web/components/orbit-memory.tsx delete mode 100644 apps/web/components/org-plan-badge.tsx delete mode 100644 apps/web/components/pwa-install-prompt.tsx delete mode 100644 apps/web/components/query-client.tsx delete mode 100644 apps/web/components/quick-note-card.tsx delete mode 100644 apps/web/components/remove-connection-dialog.tsx delete mode 100644 apps/web/components/review-memories-card.tsx delete mode 100644 apps/web/components/review-memories-modal.tsx delete mode 100644 apps/web/components/select-spaces-modal.tsx delete mode 100644 apps/web/components/settings/account.tsx delete mode 100644 apps/web/components/settings/api-keys.tsx delete mode 100644 apps/web/components/settings/billing.tsx delete mode 100644 apps/web/components/settings/cancel-reasons.ts delete mode 100644 apps/web/components/settings/company-brain-automations.tsx delete mode 100644 apps/web/components/settings/company-brain-connections.tsx delete mode 100644 apps/web/components/settings/company-brain-models.tsx delete mode 100644 apps/web/components/settings/company-brain-proactivity.tsx delete mode 100644 apps/web/components/settings/company-brain-skills.tsx delete mode 100644 apps/web/components/settings/company-brain-skills/domain.ts delete mode 100644 apps/web/components/settings/company-brain-skills/skill-editor.tsx delete mode 100644 apps/web/components/settings/company-brain-skills/skill-row.tsx delete mode 100644 apps/web/components/settings/connections-mcp.tsx delete mode 100644 apps/web/components/settings/integrations.tsx delete mode 100644 apps/web/components/settings/mcp-directory-browser.tsx delete mode 100644 apps/web/components/settings/org-context.tsx delete mode 100644 apps/web/components/settings/proactiveness-icon.tsx delete mode 100644 apps/web/components/settings/proactiveness.tsx delete mode 100644 apps/web/components/settings/settings-content.tsx delete mode 100644 apps/web/components/settings/settings-modal.tsx delete mode 100644 apps/web/components/settings/settings-org-switcher.tsx delete mode 100644 apps/web/components/settings/support.tsx delete mode 100644 apps/web/components/settings/sync-history-panel.tsx delete mode 100644 apps/web/components/settings/sync-status-badge.tsx delete mode 100644 apps/web/components/settings/sync-utils.ts delete mode 100644 apps/web/components/settings/workspace-prompt.tsx delete mode 100644 apps/web/components/setup-call-link.tsx delete mode 100644 apps/web/components/share-modal.tsx delete mode 100644 apps/web/components/space-folder-icon.tsx delete mode 100644 apps/web/components/space-glyph.tsx delete mode 100644 apps/web/components/space-profile-modal.tsx delete mode 100644 apps/web/components/space-profile-panel.tsx delete mode 100644 apps/web/components/space-selector.tsx delete mode 100644 apps/web/components/spinner.tsx delete mode 100644 apps/web/components/superloader.tsx delete mode 100644 apps/web/components/text-editor/extensions.tsx delete mode 100644 apps/web/components/text-editor/index.tsx delete mode 100644 apps/web/components/text-editor/slash-command.tsx delete mode 100644 apps/web/components/text-editor/suggestions.tsx delete mode 100644 apps/web/components/timeline-view.tsx delete mode 100644 apps/web/components/user-profile-menu.tsx delete mode 100644 apps/web/components/utils.ts create mode 100644 apps/web/console-origin.ts delete mode 100644 apps/web/globals.css delete mode 100644 apps/web/hooks/use-account-settings.ts delete mode 100644 apps/web/hooks/use-brain-models.ts delete mode 100644 apps/web/hooks/use-brain-settings.ts delete mode 100644 apps/web/hooks/use-brain-skills.impl.ts delete mode 100644 apps/web/hooks/use-brain-skills.ts delete mode 100644 apps/web/hooks/use-brain-trial.ts delete mode 100644 apps/web/hooks/use-company-brain.ts delete mode 100644 apps/web/hooks/use-connection-health.ts delete mode 100644 apps/web/hooks/use-connector-access.ts delete mode 100644 apps/web/hooks/use-container-tags.ts delete mode 100644 apps/web/hooks/use-delete-organization.ts delete mode 100644 apps/web/hooks/use-digests.ts delete mode 100644 apps/web/hooks/use-document-mutations.ts delete mode 100644 apps/web/hooks/use-full-document.ts delete mode 100644 apps/web/hooks/use-inferred-memories.ts delete mode 100644 apps/web/hooks/use-org-member-role.ts delete mode 100644 apps/web/hooks/use-org-settings.ts delete mode 100644 apps/web/hooks/use-org-summaries.ts delete mode 100644 apps/web/hooks/use-personalization.ts delete mode 100644 apps/web/hooks/use-plugin-space-meta.ts delete mode 100644 apps/web/hooks/use-processing-documents.ts delete mode 100644 apps/web/hooks/use-project-mutations.ts delete mode 100644 apps/web/hooks/use-promo-code.ts delete mode 100644 apps/web/hooks/use-research-status.ts delete mode 100644 apps/web/hooks/use-reset-organization.ts delete mode 100644 apps/web/hooks/use-space-context.ts delete mode 100644 apps/web/hooks/use-space-profile.ts delete mode 100644 apps/web/hooks/use-sync-runs.ts delete mode 100644 apps/web/hooks/use-token-usage.ts delete mode 100644 apps/web/hooks/use-trial-status.ts delete mode 100644 apps/web/hooks/use-trigger-sync.ts delete mode 100644 apps/web/instrumentation-client.ts delete mode 100644 apps/web/lib/agent-space.test.ts delete mode 100644 apps/web/lib/agent-space.ts delete mode 100644 apps/web/lib/analytics.ts delete mode 100644 apps/web/lib/billing-utils.ts delete mode 100644 apps/web/lib/cal.ts delete mode 100644 apps/web/lib/chat-auto-space.ts delete mode 100644 apps/web/lib/chat-highlight-documents.ts delete mode 100644 apps/web/lib/chat-memory-tools.test.ts delete mode 100644 apps/web/lib/chat-memory-tools.ts delete mode 100644 apps/web/lib/chat-search-memory-results.ts delete mode 100644 apps/web/lib/chat-space-label.ts delete mode 100644 apps/web/lib/chat-stream-error.ts delete mode 100644 apps/web/lib/chat-web-search-tools.ts delete mode 100644 apps/web/lib/company-brain-entry.ts delete mode 100644 apps/web/lib/configure-routes.ts delete mode 100644 apps/web/lib/connector-availability.ts delete mode 100644 apps/web/lib/connector-notify.ts delete mode 100644 apps/web/lib/constants.ts delete mode 100644 apps/web/lib/extract-urls.test.ts delete mode 100644 apps/web/lib/file-cache.ts delete mode 100644 apps/web/lib/fonts.ts delete mode 100644 apps/web/lib/ingest-auto-space.ts delete mode 100644 apps/web/lib/integration-routes.ts delete mode 100644 apps/web/lib/mcp-client-setup.ts delete mode 100644 apps/web/lib/mcp-directory.ts delete mode 100644 apps/web/lib/mcp-icon-domains.json delete mode 100644 apps/web/lib/mcp-manual-instructions.ts delete mode 100644 apps/web/lib/models.tsx delete mode 100644 apps/web/lib/oauth-plugins.ts delete mode 100644 apps/web/lib/plugin-catalog.ts delete mode 100644 apps/web/lib/plugin-document.test.ts delete mode 100644 apps/web/lib/plugin-document.ts delete mode 100644 apps/web/lib/plugin-space.ts delete mode 100644 apps/web/lib/reviewer-password-login.ts delete mode 100644 apps/web/lib/search-params.ts delete mode 100644 apps/web/lib/source-annotations.test.ts delete mode 100644 apps/web/lib/source-annotations.ts delete mode 100644 apps/web/lib/theme-provider.tsx delete mode 100644 apps/web/lib/url-helpers.test.ts delete mode 100644 apps/web/lib/url-helpers.ts delete mode 100644 apps/web/lib/variants.ts delete mode 100644 apps/web/lib/verify-session.ts delete mode 100644 apps/web/lib/view-mode-context.tsx delete mode 100644 apps/web/postcss.config.mjs delete mode 100644 apps/web/public/add-connections.png delete mode 100644 apps/web/public/add-memory.png delete mode 100644 apps/web/public/bg-rectangle.png delete mode 100644 apps/web/public/chat.png delete mode 100644 apps/web/public/dot-pattern.svg delete mode 100644 apps/web/public/icons/chrome-transparent.svg delete mode 100644 apps/web/public/icons/share-graph.svg delete mode 100644 apps/web/public/images/digest/feat-fs.svg delete mode 100644 apps/web/public/images/digest/feat-memory.png delete mode 100644 apps/web/public/images/digest/feat-profiles.png delete mode 100644 apps/web/public/images/digest/feat-retrieval.png delete mode 100644 apps/web/public/images/digest/feat-router.png delete mode 100644 apps/web/public/images/digest/wordmark.png delete mode 100644 apps/web/public/images/extension-bg.png delete mode 100644 apps/web/public/images/extension-logo.png delete mode 100644 apps/web/public/images/gdrive.svg delete mode 100644 apps/web/public/images/icon-16.png delete mode 100644 apps/web/public/images/ios-shortcuts.png delete mode 100644 apps/web/public/images/login-carousel-1.png delete mode 100644 apps/web/public/images/login-carousel-2.png delete mode 100644 apps/web/public/images/logo.png delete mode 100644 apps/web/public/images/notion.svg delete mode 100644 apps/web/public/images/onedrive.svg delete mode 100644 apps/web/public/images/plugins/claude-code.svg delete mode 100644 apps/web/public/images/plugins/clawdbot.svg delete mode 100644 apps/web/public/images/plugins/codex.png delete mode 100644 apps/web/public/images/plugins/cursor.png delete mode 100644 apps/web/public/images/plugins/hermes.svg delete mode 100644 apps/web/public/images/plugins/openclaw.svg delete mode 100644 apps/web/public/images/plugins/opencode.svg delete mode 100644 apps/web/public/images/poke.png delete mode 100644 apps/web/public/images/sidebar.png delete mode 100644 apps/web/public/landing-page.jpeg delete mode 100644 apps/web/public/mcp-icon.svg delete mode 100644 apps/web/public/mcp-supported-tools/antigravity.png delete mode 100644 apps/web/public/mcp-supported-tools/chatgpt.png delete mode 100644 apps/web/public/mcp-supported-tools/claude.png delete mode 100644 apps/web/public/mcp-supported-tools/cline.png delete mode 100644 apps/web/public/mcp-supported-tools/codex.png delete mode 100644 apps/web/public/mcp-supported-tools/cursor.png delete mode 100644 apps/web/public/mcp-supported-tools/gemini-cli.png delete mode 100644 apps/web/public/mcp-supported-tools/guides/claude-desktop/step-1.png delete mode 100644 apps/web/public/mcp-supported-tools/guides/claude-desktop/step-2.png delete mode 100644 apps/web/public/mcp-supported-tools/guides/claude-desktop/step-3.png delete mode 100644 apps/web/public/mcp-supported-tools/guides/claude-desktop/step-4.png delete mode 100644 apps/web/public/mcp-supported-tools/vscode.png delete mode 100644 apps/web/public/mcp.png delete mode 100644 apps/web/public/onboarding/bg-gradient-0.png delete mode 100644 apps/web/public/onboarding/bg-gradient-1.png delete mode 100644 apps/web/public/onboarding/chrome-ext-1.png delete mode 100644 apps/web/public/onboarding/chrome-ext-2.png delete mode 100644 apps/web/public/onboarding/chrome-ext-3.png delete mode 100644 apps/web/public/onboarding/chrome.png delete mode 100644 apps/web/public/onboarding/connectors.png delete mode 100644 apps/web/public/onboarding/human-brain.png delete mode 100644 apps/web/public/onboarding/mcp.png delete mode 100644 apps/web/public/onboarding/plant.png delete mode 100644 apps/web/public/onboarding/search.png delete mode 100644 apps/web/public/onboarding/x.png delete mode 100644 apps/web/stores/chat.ts delete mode 100644 apps/web/stores/highlights.ts delete mode 100644 apps/web/stores/index.ts delete mode 100644 apps/web/stores/indexeddb-storage.ts delete mode 100644 apps/web/stores/quick-note-draft.ts diff --git a/README.md b/README.md index 9d8fa3e0..17de7dea 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

- State-of-the-art memory and context engine for AI. And yes - you can use it as a company/personal brain. + State-of-the-art memory and context engine for AI.

@@ -67,7 +67,7 @@ All of this is in our single memory structure and ontology.

🧑‍💻 I use AI tools

-Build your own personal supermemory by using our app. Builds **persistent memory graph across every conversation**. +Give Claude Code, Cursor, Codex and OpenCode **persistent memory across every conversation** with a plugin or the MCP server. Your AI remembers your preferences, projects, past discussions — and gets smarter over time. @@ -107,21 +107,11 @@ curl -fsSL https://supermemory.ai/install | bash ## Give your AI memory -The Supermemory App, browser extension, plugins and MCP server gives any compatible AI assistant persistent memory. One install, and your AI remembers you. - -### The app - -You can use supermemory without any code, by using our consumer-facing app for free. - -Start at https://app.supermemory.ai - -image - -It also comes with an agent embedded inside, which we call Nova. +Plugins and the MCP server give any compatible AI assistant persistent memory. One install, and your AI remembers you. ### Supermemory Plugins -Supermemory comes built with Plugins for Claude Code, OpenCode, OpenClaw, and Hermes. +Supermemory comes built with plugins for Claude Code, Cursor, Codex, OpenCode, OpenClaw, and Hermes. image @@ -129,8 +119,10 @@ These plugins are implementations of the supermemory API, and they are open sour You can find them here: -- Openclaw plugin: https://github.com/supermemoryai/openclaw-supermemory -- Claude code plugin: https://github.com/supermemoryai/claude-supermemory +- Claude Code plugin: https://github.com/supermemoryai/claude-supermemory +- Cursor plugin: https://github.com/supermemoryai/cursor-supermemory +- Codex plugin: https://github.com/supermemoryai/codex-supermemory +- OpenClaw plugin: https://github.com/supermemoryai/openclaw-supermemory - OpenCode plugin: https://github.com/supermemoryai/opencode-supermemory - Hermes agent (Supermemory memory provider): https://github.com/NousResearch/hermes-agent diff --git a/README.zh-CN.md b/README.zh-CN.md index 20121721..e0a53055 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,7 +7,7 @@

- 面向 AI 的记忆与上下文引擎,业界领先。也可以把它当作公司或个人的「大脑」来用。 + 面向 AI 的记忆与上下文引擎,业界领先。

@@ -60,7 +60,7 @@ Supermemory 是为 AI 设计的记忆与上下文层。在 **[LongMemEval](https

🧑‍💻 我只是 AI 工具的用户

-直接用我们的应用,给自己搭一份专属的 supermemory。它会**在每次对话之间维护一张持久的记忆图谱**。 +通过插件或 MCP 服务器,让 Claude Code、Cursor、Codex 和 OpenCode **在每次对话之间保持持久记忆**。 你的 AI 会记住你的偏好、项目、历史讨论——而且越用越聪明。 @@ -85,28 +85,20 @@ Supermemory 是为 AI 设计的记忆与上下文层。在 **[LongMemEval](https ## 给你的 AI 装上记忆 -Supermemory 的应用、浏览器扩展、插件和 MCP 服务器,可以为任何兼容的 AI 助手提供持久记忆。装一次,AI 从此记住你。 - -### 应用 - -不用写代码,直接用我们面向消费者的应用——免费。 - -入口:https://app.supermemory.ai - -image - -应用里内置了一个 agent,我们叫它 Nova。 +插件和 MCP 服务器可以为任何兼容的 AI 助手提供持久记忆。装一次,AI 从此记住你。 ### Supermemory 插件 -Supermemory 已经为 Claude Code、OpenCode、OpenClaw、Hermes 提供了开箱即用的插件。 +Supermemory 已经为 Claude Code、Cursor、Codex、OpenCode、OpenClaw、Hermes 提供了开箱即用的插件。 image 这些插件本质上是 supermemory API 的实现,全部开源: -- Openclaw 插件:https://github.com/supermemoryai/openclaw-supermemory - Claude Code 插件:https://github.com/supermemoryai/claude-supermemory +- Cursor 插件:https://github.com/supermemoryai/cursor-supermemory +- Codex 插件:https://github.com/supermemoryai/codex-supermemory +- OpenClaw 插件:https://github.com/supermemoryai/openclaw-supermemory - OpenCode 插件:https://github.com/supermemoryai/opencode-supermemory - Hermes agent(Supermemory 作为记忆 provider):https://github.com/NousResearch/hermes-agent diff --git a/apps/browser-extension/.env.example b/apps/browser-extension/.env.example deleted file mode 100644 index aca1ac3d..00000000 --- a/apps/browser-extension/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -# PostHog Configuration -WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here \ No newline at end of file diff --git a/apps/browser-extension/.gitignore b/apps/browser-extension/.gitignore deleted file mode 100644 index a2569538..00000000 --- a/apps/browser-extension/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -.output -stats.html -stats-*.json -.wxt -web-ext.config.ts - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/apps/browser-extension/README.md b/apps/browser-extension/README.md deleted file mode 100644 index 4fb6e6e8..00000000 --- a/apps/browser-extension/README.md +++ /dev/null @@ -1 +0,0 @@ -## supermemory Browser Extension \ No newline at end of file diff --git a/apps/browser-extension/components/icons.tsx b/apps/browser-extension/components/icons.tsx deleted file mode 100644 index 2cddd587..00000000 --- a/apps/browser-extension/components/icons.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export function RightArrow({ className }: { className?: string }) { - return ( - - Right arrow - - - ) -} diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts deleted file mode 100644 index ccf3dce0..00000000 --- a/apps/browser-extension/entrypoints/background.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { - getDefaultProject, - saveMemory, - searchMemories, - fetchProjects, -} from "../utils/api" -import { - CONTAINER_TAGS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, -} from "../utils/constants" -import { trackEvent } from "../utils/posthog" -import { captureTwitterTokens } from "../utils/twitter-auth" -import { - type TwitterImportConfig, - TwitterImporter, -} from "../utils/twitter-import" -import type { - ExtensionMessage, - MemoryData, - MemoryPayload, -} from "../utils/types" - -const PLATFORM_LABELS: Record = { - chatgpt: "ChatGPT", - claude: "Claude", - gemini: "Gemini", - t3: "T3 Chat", - twitter: "X / Twitter", -} - -function normalizePlatform(value?: string): string | undefined { - if (!value) return undefined - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") -} - -function inferPlatformFromActionSource( - actionSource: string, -): string | undefined { - const source = actionSource.toLowerCase() - if (source.includes("chatgpt")) return "chatgpt" - if (source.includes("claude")) return "claude" - if (source.includes("gemini")) return "gemini" - if (source.includes("t3")) return "t3" - if (source.includes("twitter") || source.includes("x_")) return "twitter" - return undefined -} - -function inferPlatformFromUrl(url?: string): string | undefined { - if (!url) return undefined - try { - const hostname = new URL(url).hostname - if (hostname === "chatgpt.com" || hostname === "chat.openai.com") { - return "chatgpt" - } - if (hostname === "claude.ai") return "claude" - if (hostname === "gemini.google.com") return "gemini" - if (hostname === "t3.chat") return "t3" - if (hostname === "x.com" || hostname === "twitter.com") return "twitter" - } catch { - return undefined - } -} - -export default defineBackground(() => { - let twitterImporter: TwitterImporter | null = null - - browser.runtime.onInstalled.addListener(async (details) => { - if (details.reason === "install" || details.reason === "update") { - await trackEvent("extension_installed", { - reason: details.reason, - version: browser.runtime.getManifest().version, - }) - browser.tabs.create({ - url: browser.runtime.getURL("/welcome.html"), - }) - } - }) - - // Intercept Twitter requests to capture authentication headers. - browser.webRequest.onBeforeSendHeaders.addListener( - (details) => { - captureTwitterTokens(details) - return {} - }, - { urls: ["*://x.com/*", "*://twitter.com/*"] }, - ["requestHeaders", "extraHeaders"], - ) - - // Send message to current active tab. - const sendMessageToCurrentTab = async (message: string) => { - const tabs = await browser.tabs.query({ - active: true, - currentWindow: true, - }) - if (tabs.length > 0 && tabs[0].id) { - await browser.tabs.sendMessage(tabs[0].id, { - type: MESSAGE_TYPES.IMPORT_UPDATE, - importedMessage: message, - }) - } - } - - /** - * Send import completion message - */ - const sendImportDoneMessage = async (totalImported: number) => { - const tabs = await browser.tabs.query({ - active: true, - currentWindow: true, - }) - if (tabs.length > 0 && tabs[0].id) { - await browser.tabs.sendMessage(tabs[0].id, { - type: MESSAGE_TYPES.IMPORT_DONE, - totalImported, - }) - } - } - - /** - * Save memory to supermemory API - */ - const saveMemoryToSupermemory = async ( - data: MemoryData, - actionSource: string, - ): Promise<{ success: boolean; data?: unknown; error?: string }> => { - try { - let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT - try { - const defaultProject = await getDefaultProject() - if (defaultProject?.containerTag) { - containerTag = defaultProject.containerTag - } - } catch (error) { - console.warn("Failed to get default project, using fallback:", error) - } - - let content: string - if (data.content) { - content = data.content - } else if (data.highlightedText) { - content = `${data.highlightedText}\n\n${data?.url || ""}` - } else if (data.markdown) { - content = `${data.markdown}\n\n${data?.url || ""}` - } else if (data.html) { - content = `${data.html}\n\n${data?.url || ""}` - } else { - content = data?.url || "" - } - - const platform = - normalizePlatform(data.sourcePlatform) || - inferPlatformFromUrl(data.url) || - inferPlatformFromActionSource(actionSource) - const platformLabel = platform - ? data.sourcePlatformLabel || PLATFORM_LABELS[platform] || platform - : undefined - - const metadata: MemoryPayload["metadata"] = { - sm_source: "consumer", - sm_origin: "browser_extension", - sm_origin_action: actionSource, - website_url: data.url, - } - - if (platform) { - metadata.sm_origin_platform = platform - } - - if (platformLabel) { - metadata.sm_origin_platform_label = platformLabel - } - - if (data.sourceSurface) { - metadata.sm_origin_surface = data.sourceSurface - } - - if (data.ogImage) { - metadata.website_og_image = data.ogImage - } - - if (data.title) { - metadata.website_title = data.title - } - - const payload: MemoryPayload = { - containerTags: [containerTag], - content, - metadata, - } - - const responseData = await saveMemory(payload) - - await trackEvent(POSTHOG_EVENT_KEY.SAVE_MEMORY_ATTEMPTED, { - source: `${POSTHOG_EVENT_KEY.SOURCE}_${actionSource}`, - has_highlight: !!data.highlightedText, - url_domain: data.url ? new URL(data.url).hostname : undefined, - }) - - return { success: true, data: responseData } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } - } - } - - const getRelatedMemories = async ( - data: string, - eventSource: string, - ): Promise<{ success: boolean; data?: unknown; error?: string }> => { - try { - let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT - try { - const defaultProject = await getDefaultProject() - if (defaultProject?.containerTag) { - containerTag = defaultProject.containerTag - } - } catch (error) { - console.warn("Failed to get default project, using fallback:", error) - } - - const responseData = await searchMemories(data, containerTag) - const response = responseData as { - results?: Array<{ memory?: string }> - } - const memories: string[] = [] - response.results?.forEach((result, index) => { - memories.push(`${index + 1}. ${result.memory} \n`) - }) - await trackEvent(eventSource) - return { success: true, data: memories } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } - } - } - - /** - * Handle extension messages - */ - browser.runtime.onMessage.addListener( - (message: ExtensionMessage, _sender, sendResponse) => { - // Handle Twitter import request - if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) { - const importConfig: TwitterImportConfig = { - isFolderImport: message.isFolderImport, - bookmarkCollectionId: message.bookmarkCollectionId, - selectedProject: message.selectedProject, - onProgress: sendMessageToCurrentTab, - onComplete: sendImportDoneMessage, - onError: async (error: Error) => { - await sendMessageToCurrentTab(`Error: ${error.message}`) - }, - } - - twitterImporter = new TwitterImporter(importConfig) - twitterImporter.startImport().catch(console.error) - sendResponse({ success: true }) - return true - } - - // Handle regular memory save request - if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - ;(async () => { - try { - const result = await saveMemoryToSupermemory( - message.data as MemoryData, - message.actionSource || "unknown", - ) - sendResponse(result) - } catch (error) { - sendResponse({ - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }) - } - })() - return true - } - - if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) { - ;(async () => { - try { - const result = await getRelatedMemories( - message.data as string, - message.actionSource || "unknown", - ) - sendResponse(result) - } catch (error) { - sendResponse({ - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }) - } - })() - return true - } - - if (message.action === MESSAGE_TYPES.CAPTURE_PROMPT) { - ;(async () => { - try { - const messageData = message.data as { - prompt: string - platform: string - source: string - } - - const memoryData: MemoryData = { - content: messageData.prompt, - url: messageData.source, - sourcePlatform: messageData.platform, - sourceSurface: "prompt_capture", - } - - const result = await saveMemoryToSupermemory( - memoryData, - `prompt_capture_${messageData.platform}`, - ) - sendResponse(result) - } catch (error) { - sendResponse({ - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }) - } - })() - return true - } - - if (message.action === MESSAGE_TYPES.FETCH_PROJECTS) { - ;(async () => { - try { - const projects = await fetchProjects() - sendResponse({ success: true, data: projects }) - } catch (error) { - sendResponse({ - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }) - } - })() - return true - } - }, - ) -}) diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts deleted file mode 100644 index cf7a3002..00000000 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ /dev/null @@ -1,783 +0,0 @@ -import { - DOMAINS, - ELEMENT_IDS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, - UI_CONFIG, -} from "../../utils/constants" -import { - autoSearchEnabled, - autoCapturePromptsEnabled, -} from "../../utils/storage" -import { - createChatGPTInputBarElement, - DOMUtils, -} from "../../utils/ui-components" -import { - acceptMemorySuggestion, - clearMemorySuggestion, - hasAcceptedSupermemoryContext, - serializeMemoriesForDataset, - setMemoryMarkerStatus, - showLoadingSuggestion, - showMarkerPopover, - showMemorySuggestion, - syncAcceptedSupermemoryState, -} from "./memory-suggestion" - -let chatGPTDebounceTimeout: NodeJS.Timeout | null = null -let chatGPTRouteObserver: MutationObserver | null = null -let chatGPTUrlCheckInterval: NodeJS.Timeout | null = null -let chatGPTObserverThrottle: NodeJS.Timeout | null = null -const CHATGPT_DEBUG = false -const CHATGPT_LOG_PREFIX = "[supermemory:chatgpt]" - -export function initializeChatGPT() { - debugChatGPT("initializeChatGPT called", { - host: window.location.hostname, - href: window.location.href, - }) - - if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - debugChatGPT("not on ChatGPT domain, skipping") - return - } - - if (document.body.hasAttribute("data-chatgpt-initialized")) { - debugChatGPT("already initialized") - return - } - - setTimeout(() => { - addSupermemoryButtonToMemoriesDialog() - addSaveChatGPTElementBeforeComposerBtn() - setupChatGPTAutoFetch() - }, 2000) - - setupChatGPTPromptCapture() - - setupChatGPTRouteChangeDetection() - - document.body.setAttribute("data-chatgpt-initialized", "true") - debugChatGPT("initialized listeners") -} - -function debugChatGPT(message: string, data?: unknown) { - if (!CHATGPT_DEBUG) return - - if (data === undefined) { - console.log(CHATGPT_LOG_PREFIX, message) - return - } - - console.log(CHATGPT_LOG_PREFIX, message, data) -} - -function setupChatGPTRouteChangeDetection() { - if (chatGPTRouteObserver) { - chatGPTRouteObserver.disconnect() - } - if (chatGPTUrlCheckInterval) { - clearInterval(chatGPTUrlCheckInterval) - } - if (chatGPTObserverThrottle) { - clearTimeout(chatGPTObserverThrottle) - chatGPTObserverThrottle = null - } - - let currentUrl = window.location.href - - const checkForRouteChange = () => { - if (window.location.href !== currentUrl) { - currentUrl = window.location.href - debugChatGPT("route changed, re-adding supermemory elements", currentUrl) - setTimeout(() => { - addSupermemoryButtonToMemoriesDialog() - addSaveChatGPTElementBeforeComposerBtn() - setupChatGPTAutoFetch() - }, 1000) - } - } - - chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 2000) - - chatGPTRouteObserver = new MutationObserver((mutations) => { - if (chatGPTObserverThrottle) { - return - } - - let shouldRecheck = false - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element - if ( - element.querySelector?.("#prompt-textarea") || - element.querySelector?.("button.composer-btn") || - element.querySelector?.("button") || - element.querySelector?.('[role="dialog"]') || - element.matches?.("#prompt-textarea") || - element.matches?.("button") || - element.id === "prompt-textarea" - ) { - shouldRecheck = true - } - } - }) - } - }) - - if (shouldRecheck) { - chatGPTObserverThrottle = setTimeout(() => { - try { - chatGPTObserverThrottle = null - debugChatGPT("DOM changed near composer, rechecking UI") - addSupermemoryButtonToMemoriesDialog() - addSaveChatGPTElementBeforeComposerBtn() - setupChatGPTAutoFetch() - } catch (error) { - console.error("Error in ChatGPT observer callback:", error) - } - }, 300) - } - }) - - try { - chatGPTRouteObserver.observe(document.body, { - childList: true, - subtree: true, - }) - } catch (error) { - console.error("Failed to set up ChatGPT route observer:", error) - if (chatGPTUrlCheckInterval) { - clearInterval(chatGPTUrlCheckInterval) - } - chatGPTUrlCheckInterval = setInterval(checkForRouteChange, 1000) - } -} - -async function getRelatedMemoriesForChatGPT(actionSource: string) { - try { - const isAutoSearch = - actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED - const userQuery = - document.getElementById("prompt-textarea")?.textContent || "" - - const icon = document.querySelectorAll( - '[id*="sm-chatgpt-input-bar-element-before-composer"]', - )[0] - - const iconElement = icon as HTMLElement - - if (!iconElement) { - console.warn("ChatGPT icon element not found, cannot update feedback") - return - } - - if (isAutoSearch) { - const promptElement = document.getElementById("prompt-textarea") - if (promptElement) { - showLoadingSuggestion("chatgpt", promptElement) - } - setMemoryMarkerStatus(iconElement, "searching") - } else { - updateChatGPTIconFeedback("Searching memories...", iconElement) - } - - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Memory search timeout")), - UI_CONFIG.API_REQUEST_TIMEOUT, - ), - ) - - const response = await Promise.race([ - browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource: actionSource, - }), - timeoutPromise, - ]) - - if (response?.success && response?.data) { - const promptElement = document.getElementById("prompt-textarea") - if (promptElement) { - const memoryText = showMemorySuggestion( - "chatgpt", - promptElement, - response.data, - ) - debugChatGPT("memory suggestion rendered", { - memoryLength: memoryText.length, - }) - - iconElement.dataset.memoriesData = serializeMemoriesForDataset( - response.data, - ) - - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "found") - } else { - updateChatGPTIconFeedback("Included Memories", iconElement) - } - } else { - console.warn( - "ChatGPT prompt element not found after successful memory fetch", - ) - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "found") - } else { - updateChatGPTIconFeedback("Memories found", iconElement) - } - } - } else { - console.warn("No memories found or API response invalid") - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "none") - } else { - updateChatGPTIconFeedback("No memories found", iconElement) - } - } - } catch (error) { - console.error("Error getting related memories:", error) - try { - const icon = document.querySelectorAll( - '[id*="sm-chatgpt-input-bar-element-before-composer"]', - )[0] as HTMLElement - if (icon) { - if ( - actionSource === POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED - ) { - setMemoryMarkerStatus(icon, "error") - } else { - updateChatGPTIconFeedback("Error fetching memories", icon) - } - } - } catch (feedbackError) { - console.error("Failed to update error feedback:", feedbackError) - } - } -} - -function addSupermemoryButtonToMemoriesDialog() { - const dialogs = document.querySelectorAll('[role="dialog"]') - let memoriesDialog: HTMLElement | null = null - - for (const dialog of dialogs) { - const headerText = dialog.querySelector("h2") - if (headerText?.textContent?.includes("Saved memories")) { - memoriesDialog = dialog as HTMLElement - break - } - } - - if (!memoriesDialog) return - - if (memoriesDialog.querySelector("#supermemory-save-button")) return - - const deleteAllContainer = memoriesDialog.querySelector( - ".flex.items-center.gap-0\\.5", - ) - if (!deleteAllContainer) return - - const supermemoryButton = document.createElement("button") - supermemoryButton.id = "supermemory-save-button" - supermemoryButton.className = "btn relative btn-primary-outline mr-2" - - const iconUrl = browser.runtime.getURL("/new_logo.png") - - supermemoryButton.innerHTML = ` -
- supermemory - Save to supermemory -
- ` - - supermemoryButton.style.cssText = ` - background: #1C2026 !important; - color: white !important; - border: 1px solid #1C2026 !important; - border-radius: 9999px !important; - padding: 10px 16px !important; - font-weight: 500 !important; - font-size: 14px !important; - margin-right: 8px !important; - cursor: pointer !important; - ` - - supermemoryButton.addEventListener("mouseenter", () => { - supermemoryButton.style.backgroundColor = "#2B2E33" - }) - - supermemoryButton.addEventListener("mouseleave", () => { - supermemoryButton.style.backgroundColor = "#1C2026" - }) - - supermemoryButton.addEventListener("click", async () => { - await saveMemoriesToSupermemory() - }) - - deleteAllContainer.insertBefore( - supermemoryButton, - deleteAllContainer.firstChild, - ) -} - -async function saveMemoriesToSupermemory() { - try { - DOMUtils.showToast("loading") - - const memoriesTable = document.querySelector('[role="dialog"] table tbody') - if (!memoriesTable) { - DOMUtils.showToast("error") - return - } - - if (!memoriesTable.textContent) { - DOMUtils.showToast("error") - return - } - - const combinedContent = `Memories from ChatGPT:\n\n${memoriesTable.textContent}` - - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.SAVE_MEMORY, - data: { - html: combinedContent, - sourcePlatform: "chatgpt", - sourceSurface: "memories_dialog", - url: window.location.href, - }, - actionSource: "chatgpt_memories_dialog", - }) - - debugChatGPT("memory dialog saved", { - success: response.success, - }) - - if (response.success) { - DOMUtils.showToast("success") - } else { - DOMUtils.showToast("error") - } - } catch (error) { - console.error("Error saving memories to supermemory:", error) - DOMUtils.showToast("error") - } -} - -function updateChatGPTIconFeedback( - message: string, - iconElement: HTMLElement, - resetAfter = 0, -) { - const memories = iconElement.dataset.memoriesData - const fallbackReset = - resetAfter || (message === "Included Memories" ? 0 : 2200) - - if (message === "Included Memories" || message === "Memories found") { - setMemoryMarkerStatus(iconElement, "found") - showMarkerPopover(iconElement, "Included Memories", memories) - return - } - - if (message.toLowerCase().includes("searching")) { - setMemoryMarkerStatus(iconElement, "searching") - showMarkerPopover(iconElement, message) - return - } - - setMemoryMarkerStatus( - iconElement, - message.toLowerCase().includes("error") ? "error" : "none", - ) - showMarkerPopover(iconElement, message, undefined, fallbackReset) -} - -function addSaveChatGPTElementBeforeComposerBtn() { - const promptInput = getChatGPTPromptInput() - if (!promptInput) { - debugChatGPT("prompt input not found", getChatGPTDomSnapshot()) - return - } - - const composer = findChatGPTComposerRoot(promptInput) - if (!composer?.querySelector) { - debugChatGPT("composer root not found", describeElement(promptInput)) - return - } - - const existingMarkers = Array.from( - document.querySelectorAll( - `[id*="${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer"]`, - ), - ) - if (existingMarkers.length > 1) { - debugChatGPT("removed duplicate markers", existingMarkers.length) - for (const marker of existingMarkers) { - marker.remove() - } - } else if (existingMarkers.length === 1) { - debugChatGPT("marker already exists") - return - } - - const buttons = findChatGPTComposerButtons(promptInput, composer) - debugChatGPT("candidate ChatGPT buttons", { - input: describeElement(promptInput), - composer: describeElement(composer), - buttons: buttons.map((button) => ({ - label: buttonLabel(button), - element: describeElement(button), - })), - }) - - const micButton = buttons.find((button) => isChatGPTMicButton(button)) - const voiceButton = buttons.find((button) => isChatGPTVoiceButton(button)) - const sendButton = buttons.find((button) => isChatGPTSendButton(button)) - const anchorButton = - micButton || voiceButton || sendButton || buttons[buttons.length - 1] - const anchorSlot = findChatGPTButtonSlot(anchorButton, composer) - const speechContainer = composer.querySelector( - '[data-testid="composer-speech-button-container"]', - ) as HTMLElement | null - const targetContainer = - anchorSlot?.parentElement || - speechContainer?.parentElement || - promptInput.parentElement - - if (!targetContainer) { - debugChatGPT("could not find insertion target", { - anchor: anchorButton ? describeElement(anchorButton) : null, - input: describeElement(promptInput), - }) - return - } - - const saveChatGPTElement = createChatGPTInputBarElement(async () => { - await getRelatedMemoriesForChatGPT( - POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED, - ) - }) - - saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - if (anchorSlot?.parentElement === targetContainer) { - targetContainer.insertBefore(saveChatGPTElement, anchorSlot) - debugChatGPT("inserted marker before anchor button", { - anchorLabel: anchorButton ? buttonLabel(anchorButton) : null, - anchorSlot: describeElement(anchorSlot), - target: describeElement(targetContainer), - }) - } else { - targetContainer.appendChild(saveChatGPTElement) - debugChatGPT("inserted marker into fallback target", { - target: describeElement(targetContainer), - }) - } - - setupChatGPTAutoFetch() -} - -function getChatGPTPromptInput(): HTMLElement | null { - return document.querySelector( - '#prompt-textarea, [data-testid="prompt-textarea"], div[contenteditable="true"]', - ) as HTMLElement | null -} - -function findChatGPTComposerRoot(input: HTMLElement): HTMLElement { - const form = input.closest("form") as HTMLElement | null - if (form) return form - - let current: HTMLElement | null = input - for (let depth = 0; current && depth < 8; depth += 1) { - if (current.querySelectorAll("button").length >= 2) { - return current - } - current = current.parentElement - } - - return input.parentElement || document.body -} - -function findChatGPTComposerButtons( - input: HTMLElement, - composer: HTMLElement, -): HTMLButtonElement[] { - const composerButtons = Array.from(composer.querySelectorAll("button")) - if (composerButtons.length > 0) { - return composerButtons - } - - const inputRect = input.getBoundingClientRect() - const allButtons = Array.from(document.querySelectorAll("button")) - - return allButtons.filter((button) => { - const rect = button.getBoundingClientRect() - const verticallyNear = - Math.abs( - rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2), - ) < 120 - const horizontallyNear = - rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260 - - return verticallyNear && horizontallyNear - }) -} - -function buttonLabel(button: HTMLButtonElement): string { - return [ - button.id, - button.getAttribute("aria-label"), - button.getAttribute("title"), - button.getAttribute("data-testid"), - button.getAttribute("data-test-id"), - button.textContent, - ] - .filter(Boolean) - .join(" ") -} - -function isChatGPTMicButton(button: HTMLButtonElement): boolean { - return /mic|microphone|dictate/i.test(buttonLabel(button)) -} - -function isChatGPTVoiceButton(button: HTMLButtonElement): boolean { - return /voice|audio|speech/i.test(buttonLabel(button)) -} - -function isChatGPTSendButton(button: HTMLButtonElement): boolean { - const label = buttonLabel(button) - return /composer-submit-button|send|submit/i.test(label) -} - -function findChatGPTButtonSlot( - button: HTMLButtonElement | undefined, - composer: HTMLElement, -): HTMLElement | null { - if (!button) return null - - let current: HTMLElement | null = button - while (current?.parentElement && current.parentElement !== composer) { - const parent: HTMLElement = current.parentElement - const parentStyle = window.getComputedStyle(parent) - const hasSiblingControls = parent.children.length > 1 - const isRow = - parentStyle.display.includes("flex") && - parentStyle.flexDirection !== "column" - - if (hasSiblingControls && isRow) { - return current - } - - current = parent - } - - return current || button -} - -function describeElement(element: Element | null): string | null { - if (!element) return null - - const parts = [element.tagName.toLowerCase()] - if (element.id) parts.push(`#${element.id}`) - if (element.className && typeof element.className === "string") { - parts.push( - `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`, - ) - } - - for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) { - const value = element.getAttribute(attr) - if (value) parts.push(`[${attr}="${value}"]`) - } - - return parts.join("") -} - -function getChatGPTDomSnapshot() { - return { - promptTextareas: document.querySelectorAll("#prompt-textarea").length, - contenteditables: document.querySelectorAll('[contenteditable="true"]') - .length, - textareas: document.querySelectorAll("textarea").length, - buttons: document.querySelectorAll("button").length, - composerButtons: document.querySelectorAll("button.composer-btn").length, - speechContainers: document.querySelectorAll( - '[data-testid="composer-speech-button-container"]', - ).length, - } -} - -async function setupChatGPTAutoFetch() { - const autoSearch = (await autoSearchEnabled.getValue()) ?? false - - if (!autoSearch) { - return - } - - const promptTextarea = document.getElementById("prompt-textarea") - if ( - !promptTextarea || - promptTextarea.hasAttribute("data-supermemory-auto-fetch") - ) { - return - } - - promptTextarea.setAttribute("data-supermemory-auto-fetch", "true") - - const handleInput = () => { - const content = promptTextarea.textContent?.trim() || "" - syncAcceptedSupermemoryState(promptTextarea) - - if (content.length === 0) { - clearMemorySuggestion("chatgpt", promptTextarea) - document - .querySelectorAll( - '[id*="sm-chatgpt-input-bar-element-before-composer"]', - ) - .forEach((icon) => { - setMemoryMarkerStatus(icon as HTMLElement, "neutral") - }) - } - - if (chatGPTDebounceTimeout) { - clearTimeout(chatGPTDebounceTimeout) - } - - chatGPTDebounceTimeout = setTimeout(async () => { - if (hasAcceptedSupermemoryContext(promptTextarea)) { - clearMemorySuggestion("chatgpt", promptTextarea) - return - } - - if (content.length > 2) { - await getRelatedMemoriesForChatGPT( - POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED, - ) - } else if (content.length === 0) { - const icons = document.querySelectorAll( - '[id*="sm-chatgpt-input-bar-element-before-composer"]', - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - setMemoryMarkerStatus(iconElement, "neutral") - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - delete iconElement.dataset.memoriesData - } - }) - - if (promptTextarea.dataset.supermemories) { - clearMemorySuggestion("chatgpt", promptTextarea) - } - } - }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) - } - - promptTextarea.addEventListener("input", handleInput) -} - -function setupChatGPTPromptCapture() { - if (document.body.hasAttribute("data-chatgpt-prompt-capture-setup")) { - return - } - document.body.setAttribute("data-chatgpt-prompt-capture-setup", "true") - - const capturePromptContent = async (source: string) => { - const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false - - if (!autoCapture) { - debugChatGPT("auto prompt capture disabled") - return - } - const promptTextarea = document.getElementById("prompt-textarea") - - let promptContent = "" - if (promptTextarea) { - promptContent = promptTextarea.textContent || "" - } - - if (promptTextarea && promptContent.trim()) { - debugChatGPT("prompt submitted", { - source, - promptLength: promptContent.length, - }) - try { - await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.CAPTURE_PROMPT, - data: { - prompt: promptContent, - platform: "chatgpt", - source: window.location.href, - }, - }) - } catch (error) { - console.error("Error sending ChatGPT prompt to background:", error) - } - } - - const icons = document.querySelectorAll( - '[id*="sm-chatgpt-input-bar-element-before-composer"]', - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - delete iconElement.dataset.memoriesData - } - }) - - if (promptTextarea?.dataset.supermemories) { - clearMemorySuggestion("chatgpt", promptTextarea) - } - } - - document.addEventListener( - "click", - async (event) => { - const target = event.target as HTMLElement - if ( - target.id === "composer-submit-button" || - target.closest("#composer-submit-button") - ) { - await capturePromptContent("button click") - } - }, - true, - ) - - document.addEventListener( - "keydown", - async (event) => { - const target = event.target as HTMLElement - - if ( - (target.id === "prompt-textarea" || - target.closest("#prompt-textarea")) && - acceptMemorySuggestion( - event, - "chatgpt", - document.getElementById("prompt-textarea"), - ) - ) { - return - } - - if ( - target.id === "prompt-textarea" && - event.key === "Enter" && - !event.shiftKey - ) { - await capturePromptContent("Enter key") - } - }, - true, - ) -} diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts deleted file mode 100644 index f31c2bb6..00000000 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ /dev/null @@ -1,909 +0,0 @@ -import { - DOMAINS, - ELEMENT_IDS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, - UI_CONFIG, -} from "../../utils/constants" -import { - autoSearchEnabled, - autoCapturePromptsEnabled, -} from "../../utils/storage" -import { - createClaudeInputBarElement, - DOMUtils, -} from "../../utils/ui-components" -import { - acceptMemorySuggestion, - clearMemorySuggestion, - hasAcceptedSupermemoryContext, - serializeMemoriesForDataset, - setMemoryMarkerStatus, - showLoadingSuggestion, - showMarkerPopover, - showMemorySuggestion, - syncAcceptedSupermemoryState, -} from "./memory-suggestion" - -let claudeDebounceTimeout: NodeJS.Timeout | null = null -let claudeRouteObserver: MutationObserver | null = null -let claudeUrlCheckInterval: NodeJS.Timeout | null = null -let claudeObserverThrottle: NodeJS.Timeout | null = null -const CLAUDE_DEBUG = false -const CLAUDE_LOG_PREFIX = "[supermemory:claude]" - -export function initializeClaude() { - debugClaude("initializeClaude called", { - host: window.location.hostname, - href: window.location.href, - }) - - if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - debugClaude("not on Claude domain, skipping") - return - } - - if (document.body.hasAttribute("data-claude-initialized")) { - debugClaude("already initialized") - return - } - - setTimeout(() => { - addSupermemoryButtonToClaudeMemoryDialog() - addSupermemoryIconToClaudeInput() - setupClaudeAutoFetch() - }, 2000) - - setupClaudePromptCapture() - - setupClaudeRouteChangeDetection() - - document.body.setAttribute("data-claude-initialized", "true") - debugClaude("initialized listeners") -} - -function debugClaude(message: string, data?: unknown) { - if (!CLAUDE_DEBUG) return - - if (data === undefined) { - console.log(CLAUDE_LOG_PREFIX, message) - return - } - - console.log(CLAUDE_LOG_PREFIX, message, data) -} - -function setupClaudeRouteChangeDetection() { - if (claudeRouteObserver) { - claudeRouteObserver.disconnect() - } - if (claudeUrlCheckInterval) { - clearInterval(claudeUrlCheckInterval) - } - if (claudeObserverThrottle) { - clearTimeout(claudeObserverThrottle) - claudeObserverThrottle = null - } - - let currentUrl = window.location.href - - const checkForRouteChange = () => { - if (window.location.href !== currentUrl) { - currentUrl = window.location.href - debugClaude("route changed, re-adding supermemory icon", currentUrl) - setTimeout(() => { - addSupermemoryButtonToClaudeMemoryDialog() - addSupermemoryIconToClaudeInput() - setupClaudeAutoFetch() - }, 1000) - } - } - - claudeUrlCheckInterval = setInterval(checkForRouteChange, 2000) - - claudeRouteObserver = new MutationObserver((mutations) => { - if (claudeObserverThrottle) { - return - } - - let shouldRecheck = false - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element - if ( - element.querySelector?.('[role="dialog"]') || - element.querySelector?.('div[contenteditable="true"]') || - element.querySelector?.("textarea") || - element.querySelector?.("button") || - element.matches?.('[role="dialog"]') || - element.matches?.('div[contenteditable="true"]') || - element.matches?.("textarea") || - element.matches?.("button") || - element.textContent?.includes("Manage memory") - ) { - shouldRecheck = true - } - } - }) - } - }) - - if (shouldRecheck) { - claudeObserverThrottle = setTimeout(() => { - try { - claudeObserverThrottle = null - debugClaude("DOM changed near composer, rechecking UI") - addSupermemoryButtonToClaudeMemoryDialog() - addSupermemoryIconToClaudeInput() - setupClaudeAutoFetch() - } catch (error) { - console.error("Error in Claude observer callback:", error) - } - }, 300) - } - }) - - try { - claudeRouteObserver.observe(document.body, { - childList: true, - subtree: true, - }) - } catch (error) { - console.error("Failed to set up Claude route observer:", error) - if (claudeUrlCheckInterval) { - clearInterval(claudeUrlCheckInterval) - } - claudeUrlCheckInterval = setInterval(checkForRouteChange, 1000) - } -} - -function addSupermemoryIconToClaudeInput() { - const input = getClaudePromptInput() - if (!input) { - debugClaude("prompt input not found", getClaudeDomSnapshot()) - return - } - - const composer = findComposerRoot(input) - if (!composer?.querySelector) { - debugClaude("composer root not found", describeElement(input)) - return - } - - const existingMarkers = Array.from( - document.querySelectorAll( - `[id*="${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}"]`, - ), - ) - if (existingMarkers.length > 1) { - debugClaude("removed duplicate markers", existingMarkers.length) - for (const marker of existingMarkers) { - marker.remove() - } - } else if (existingMarkers.length === 1) { - debugClaude("marker already exists") - return - } - - const buttons = findClaudeComposerButtons(input, composer) - debugClaude("candidate Claude buttons", { - input: describeElement(input), - composer: describeElement(composer), - buttons: buttons.map((button) => ({ - label: buttonLabel(button), - element: describeElement(button), - })), - }) - - const micButton = buttons.find((button) => isClaudeMicButton(button)) - const voiceButton = buttons.find((button) => isClaudeVoiceButton(button)) - const sendButton = buttons.find((button) => isClaudeSendButton(button)) - const anchorButton = - micButton || voiceButton || sendButton || buttons[buttons.length - 1] - const anchorSlot = findClaudeButtonSlot(anchorButton, composer) - const targetContainer = anchorSlot?.parentElement || input.parentElement - - if (!targetContainer) { - debugClaude("could not find insertion target", { - anchor: anchorButton ? describeElement(anchorButton) : null, - input: describeElement(input), - }) - return - } - - const supermemoryIcon = createClaudeInputBarElement(async () => { - await getRelatedMemoriesForClaude( - POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED, - ) - }) - - supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - if (anchorSlot?.parentElement === targetContainer) { - targetContainer.insertBefore(supermemoryIcon, anchorSlot) - debugClaude("inserted marker before anchor button", { - anchorLabel: anchorButton ? buttonLabel(anchorButton) : null, - anchorSlot: describeElement(anchorSlot), - target: describeElement(targetContainer), - }) - return - } - - targetContainer.appendChild(supermemoryIcon) - debugClaude("inserted marker into fallback target", { - target: describeElement(targetContainer), - }) -} - -function getClaudePromptInput(): HTMLElement | null { - return document.querySelector( - '.ProseMirror[contenteditable="true"], div[contenteditable="true"], textarea', - ) as HTMLElement | null -} - -function findComposerRoot(input: HTMLElement): HTMLElement { - return ( - (input.closest("form") as HTMLElement | null) || - (input.closest('[data-testid*="composer"]') as HTMLElement | null) || - (input.closest('[class*="composer"]') as HTMLElement | null) || - (input.closest(".relative") as HTMLElement | null) || - input.parentElement || - document.body - ) -} - -function buttonLabel(button: HTMLButtonElement): string { - return [ - button.getAttribute("aria-label"), - button.getAttribute("title"), - button.getAttribute("data-testid"), - button.getAttribute("data-test-id"), - button.textContent, - ] - .filter(Boolean) - .join(" ") -} - -function findClaudeComposerButtons( - input: HTMLElement, - composer: HTMLElement, -): HTMLButtonElement[] { - const composerButtons = Array.from(composer.querySelectorAll("button")) - if (composerButtons.length > 0) { - return composerButtons - } - - const inputRect = input.getBoundingClientRect() - const allButtons = Array.from(document.querySelectorAll("button")) - - return allButtons.filter((button) => { - const rect = button.getBoundingClientRect() - const verticallyNear = - Math.abs( - rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2), - ) < 120 - const horizontallyNear = - rect.left > inputRect.left - 80 && rect.left < inputRect.right + 260 - - return verticallyNear && horizontallyNear - }) -} - -function isClaudeMicButton(button: HTMLButtonElement): boolean { - return /mic|microphone|dictate/i.test(buttonLabel(button)) -} - -function isClaudeVoiceButton(button: HTMLButtonElement): boolean { - return /voice|audio|speech/i.test(buttonLabel(button)) -} - -function isClaudeSendButton(button: HTMLButtonElement): boolean { - return /send|submit/i.test(buttonLabel(button)) -} - -function findClaudeButtonSlot( - button: HTMLButtonElement | undefined, - composer: HTMLElement, -): HTMLElement | null { - if (!button) return null - - let current: HTMLElement | null = button - while (current?.parentElement && current.parentElement !== composer) { - const parent: HTMLElement = current.parentElement - const parentStyle = window.getComputedStyle(parent) - const hasSiblingControls = parent.children.length > 1 - const isRow = - parentStyle.display.includes("flex") && - parentStyle.flexDirection !== "column" - - if (hasSiblingControls && isRow) { - return current - } - - current = parent - } - - return current || button -} - -function describeElement(element: Element | null): string | null { - if (!element) return null - - const parts = [element.tagName.toLowerCase()] - if (element.id) parts.push(`#${element.id}`) - if (element.className && typeof element.className === "string") { - parts.push( - `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`, - ) - } - - for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) { - const value = element.getAttribute(attr) - if (value) parts.push(`[${attr}="${value}"]`) - } - - return parts.join("") -} - -function getClaudeDomSnapshot() { - return { - proseMirrors: document.querySelectorAll(".ProseMirror").length, - contenteditables: document.querySelectorAll('[contenteditable="true"]') - .length, - textareas: document.querySelectorAll("textarea").length, - buttons: document.querySelectorAll("button").length, - } -} - -async function getRelatedMemoriesForClaude(actionSource: string) { - try { - const isAutoSearch = - actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED - let userQuery = "" - - const supermemoryContainer = document.querySelector( - '[data-supermemory-icon-added="true"]', - ) - if (supermemoryContainer?.parentElement?.previousElementSibling) { - const pTag = - supermemoryContainer.parentElement.previousElementSibling.querySelector( - "p", - ) - userQuery = pTag?.innerText || pTag?.textContent || "" - } - - if (!userQuery.trim()) { - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - userQuery = - textareaElement?.innerText || textareaElement?.textContent || "" - } - - if (!userQuery.trim()) { - const inputElements = document.querySelectorAll( - 'div[contenteditable="true"], textarea, input[type="text"]', - ) - for (const element of inputElements) { - const text = - (element as HTMLElement).innerText || - (element as HTMLInputElement).value - if (text?.trim()) { - userQuery = text.trim() - break - } - } - } - - debugClaude("query extracted", { - queryLength: userQuery.length, - }) - - if (!userQuery.trim()) { - debugClaude("memory search skipped because query is empty") - return - } - - const icon = document.querySelector('[id*="sm-claude-input-bar-element"]') - - const iconElement = icon as HTMLElement - - if (!iconElement) { - console.warn("Claude icon element not found, cannot update feedback") - return - } - - if (isAutoSearch) { - const input = getClaudePromptInput() - if (input) { - showLoadingSuggestion("claude", input) - } - setMemoryMarkerStatus(iconElement, "searching") - } else { - updateClaudeIconFeedback("Searching memories...", iconElement) - } - - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Memory search timeout")), - UI_CONFIG.API_REQUEST_TIMEOUT, - ), - ) - - const response = await Promise.race([ - browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource: actionSource, - }), - timeoutPromise, - ]) - - debugClaude("memory search response", { - success: response?.success, - }) - - if (response?.success && response?.data) { - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - - if (textareaElement) { - const memoryText = showMemorySuggestion( - "claude", - textareaElement, - response.data, - ) - debugClaude("memory suggestion rendered", { - memoryLength: memoryText.length, - }) - - iconElement.dataset.memoriesData = serializeMemoriesForDataset( - response.data, - ) - - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "found") - } else { - updateClaudeIconFeedback("Included Memories", iconElement) - } - } else { - console.warn( - "Claude input area not found after successful memory fetch", - ) - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "found") - } else { - updateClaudeIconFeedback("Memories found", iconElement) - } - } - } else { - console.warn("No memories found or API response invalid for Claude") - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "none") - } else { - updateClaudeIconFeedback("No memories found", iconElement) - } - } - } catch (error) { - console.error("Error getting related memories for Claude:", error) - try { - const icon = document.querySelector( - '[id*="sm-claude-input-bar-element"]', - ) as HTMLElement - if (icon) { - if ( - actionSource === POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED - ) { - setMemoryMarkerStatus(icon, "error") - } else { - updateClaudeIconFeedback("Error fetching memories", icon) - } - } - } catch (feedbackError) { - console.error("Failed to update Claude error feedback:", feedbackError) - } - } -} - -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", - }) - - debugClaude("memory dialog saved", { - success: response.success, - }) - - 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, - resetAfter = 0, -) { - const memories = iconElement.dataset.memoriesData - const fallbackReset = - resetAfter || (message === "Included Memories" ? 0 : 2200) - - if (message === "Included Memories" || message === "Memories found") { - setMemoryMarkerStatus(iconElement, "found") - showMarkerPopover(iconElement, "Included Memories", memories) - return - } - - if (message.toLowerCase().includes("searching")) { - setMemoryMarkerStatus(iconElement, "searching") - showMarkerPopover(iconElement, message) - return - } - - setMemoryMarkerStatus( - iconElement, - message.toLowerCase().includes("error") ? "error" : "none", - ) - showMarkerPopover(iconElement, message, undefined, fallbackReset) -} - -function setupClaudePromptCapture() { - if (document.body.hasAttribute("data-claude-prompt-capture-setup")) { - return - } - document.body.setAttribute("data-claude-prompt-capture-setup", "true") - const captureClaudePromptContent = async (source: string) => { - const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false - - if (!autoCapture) { - debugClaude("auto prompt capture disabled") - return - } - let promptContent = "" - - const contentEditableDiv = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - if (contentEditableDiv) { - promptContent = - contentEditableDiv.textContent || contentEditableDiv.innerText || "" - } - - if (!promptContent) { - const textarea = document.querySelector("textarea") as HTMLTextAreaElement - if (textarea) { - promptContent = textarea.value || "" - } - } - - if (promptContent.trim()) { - debugClaude("prompt submitted", { - source, - promptLength: promptContent.length, - }) - - try { - await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.CAPTURE_PROMPT, - data: { - prompt: promptContent, - platform: "claude", - source: window.location.href, - }, - }) - } catch (error) { - console.error("Error sending Claude prompt to background:", error) - } - } - - const icons = document.querySelectorAll( - '[id*="sm-claude-input-bar-element"]', - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - delete iconElement.dataset.memoriesData - } - }) - - if (contentEditableDiv?.dataset.supermemories) { - clearMemorySuggestion("claude", contentEditableDiv) - } - } - - document.addEventListener( - "click", - async (event) => { - const target = event.target as HTMLElement - if (target.closest('[data-supermemory-connected-indicator="true"]')) { - return - } - - const sendButton = target.closest("button") - - if ( - sendButton && - buttonLabel(sendButton as HTMLButtonElement).match(/send|submit/i) - ) { - await captureClaudePromptContent("button click") - } - }, - true, - ) - - document.addEventListener( - "keydown", - async (event) => { - const target = event.target as HTMLElement - - const activeInput = - (target.closest('div[contenteditable="true"]') as HTMLElement | null) || - (target.matches("textarea") ? (target as HTMLTextAreaElement) : null) - if (acceptMemorySuggestion(event, "claude", activeInput)) { - return - } - - if ( - (target.matches('div[contenteditable="true"]') || - target.matches(".ProseMirror") || - target.matches("textarea") || - target.closest('div[contenteditable="true"]') || - target.closest(".ProseMirror")) && - event.key === "Enter" && - !event.shiftKey - ) { - await captureClaudePromptContent("Enter key") - } - }, - true, - ) -} - -async function setupClaudeAutoFetch() { - const autoSearch = (await autoSearchEnabled.getValue()) ?? false - if (!autoSearch) { - return - } - - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - - if ( - !textareaElement || - textareaElement.hasAttribute("data-supermemory-auto-fetch") - ) { - return - } - - textareaElement.setAttribute("data-supermemory-auto-fetch", "true") - - const handleInput = () => { - const content = textareaElement.textContent?.trim() || "" - syncAcceptedSupermemoryState(textareaElement) - - if (content.length === 0) { - clearMemorySuggestion("claude", textareaElement) - document - .querySelectorAll('[id*="sm-claude-input-bar-element"]') - .forEach((icon) => { - setMemoryMarkerStatus(icon as HTMLElement, "neutral") - }) - } - - if (claudeDebounceTimeout) { - clearTimeout(claudeDebounceTimeout) - } - - claudeDebounceTimeout = setTimeout(async () => { - if (hasAcceptedSupermemoryContext(textareaElement)) { - clearMemorySuggestion("claude", textareaElement) - return - } - - if (content.length > 2) { - await getRelatedMemoriesForClaude( - POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED, - ) - } else if (content.length === 0) { - const icons = document.querySelectorAll( - '[id*="sm-claude-input-bar-element"]', - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - setMemoryMarkerStatus(iconElement, "neutral") - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - delete iconElement.dataset.memoriesData - } - }) - - if (textareaElement.dataset.supermemories) { - clearMemorySuggestion("claude", textareaElement) - } - } - }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) - } - - textareaElement.addEventListener("input", handleInput) -} diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts deleted file mode 100644 index f819d3d6..00000000 --- a/apps/browser-extension/entrypoints/content/gemini.ts +++ /dev/null @@ -1,664 +0,0 @@ -import { - DOMAINS, - ELEMENT_IDS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, - UI_CONFIG, -} from "../../utils/constants" -import { - autoCapturePromptsEnabled, - autoSearchEnabled, -} from "../../utils/storage" -import { - createGeminiInputBarElement, - DOMUtils, -} from "../../utils/ui-components" -import { - acceptMemorySuggestion, - clearMemorySuggestion, - hasAcceptedSupermemoryContext, - serializeMemoriesForDataset, - setMemoryMarkerStatus, - showLoadingSuggestion, - showMarkerPopover, - showMemorySuggestion, - syncAcceptedSupermemoryState, -} from "./memory-suggestion" - -let geminiDebounceTimeout: NodeJS.Timeout | null = null -let geminiRouteObserver: MutationObserver | null = null -let geminiUrlCheckInterval: NodeJS.Timeout | null = null -let geminiObserverThrottle: NodeJS.Timeout | null = null -const GEMINI_DEBUG = false -const GEMINI_LOG_PREFIX = "[supermemory:gemini]" - -type GeminiInput = HTMLElement | HTMLTextAreaElement - -export function initializeGemini() { - debugGemini("initializeGemini called", { - host: window.location.hostname, - href: window.location.href, - }) - - if (!DOMUtils.isOnDomain(DOMAINS.GEMINI)) { - debugGemini("not on Gemini domain, skipping") - return - } - - if (document.body.hasAttribute("data-gemini-initialized")) { - debugGemini("already initialized") - return - } - - setTimeout(() => { - addSupermemoryIconToGeminiInput() - setupGeminiAutoFetch() - }, 2000) - - setupGeminiPromptCapture() - setupGeminiRouteChangeDetection() - - document.body.setAttribute("data-gemini-initialized", "true") - debugGemini("initialized listeners") -} - -function debugGemini(message: string, data?: unknown) { - if (!GEMINI_DEBUG) return - - if (data === undefined) { - console.log(GEMINI_LOG_PREFIX, message) - return - } - - console.log(GEMINI_LOG_PREFIX, message, data) -} - -function setupGeminiRouteChangeDetection() { - if (geminiRouteObserver) { - geminiRouteObserver.disconnect() - } - if (geminiUrlCheckInterval) { - clearInterval(geminiUrlCheckInterval) - } - if (geminiObserverThrottle) { - clearTimeout(geminiObserverThrottle) - geminiObserverThrottle = null - } - - let currentUrl = window.location.href - - const recheckGeminiUI = () => { - addSupermemoryIconToGeminiInput() - setupGeminiAutoFetch() - } - - const checkForRouteChange = () => { - if (window.location.href !== currentUrl) { - currentUrl = window.location.href - debugGemini("route changed, rechecking UI", currentUrl) - setTimeout(recheckGeminiUI, 1000) - } - } - - geminiUrlCheckInterval = setInterval(checkForRouteChange, 2000) - - geminiRouteObserver = new MutationObserver((mutations) => { - if (geminiObserverThrottle) { - return - } - - const shouldRecheck = mutations.some((mutation) => - Array.from(mutation.addedNodes).some((node) => { - if (node.nodeType !== Node.ELEMENT_NODE) { - return false - } - - const element = node as Element - return ( - element.matches?.("rich-textarea, textarea, button") || - element.matches?.('[contenteditable="true"]') || - !!element.querySelector?.( - 'rich-textarea, textarea, button, [contenteditable="true"]', - ) - ) - }), - ) - - if (shouldRecheck) { - geminiObserverThrottle = setTimeout(() => { - geminiObserverThrottle = null - debugGemini("DOM changed near Gemini composer, rechecking UI") - recheckGeminiUI() - }, 300) - } - }) - - try { - geminiRouteObserver.observe(document.body, { - childList: true, - subtree: true, - }) - } catch (error) { - console.error("Failed to set up Gemini route observer:", error) - if (geminiUrlCheckInterval) { - clearInterval(geminiUrlCheckInterval) - } - geminiUrlCheckInterval = setInterval(checkForRouteChange, 1000) - } -} - -function addSupermemoryIconToGeminiInput() { - const input = getGeminiPromptInput() - if (!input) { - debugGemini("prompt input not found", getGeminiDomSnapshot()) - return - } - - const composer = findGeminiComposerRoot(input) - if (!composer?.querySelector) { - debugGemini("composer root not found", describeElement(input)) - return - } - - const existingMarkers = Array.from( - document.querySelectorAll( - `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, - ), - ) - if (existingMarkers.length > 1) { - debugGemini("removed duplicate markers", existingMarkers.length) - for (const marker of existingMarkers) { - marker.remove() - } - } else if (existingMarkers.length === 1) { - debugGemini("marker already exists") - return - } - - const buttons = findGeminiComposerButtons(input, composer) - debugGemini("candidate Gemini buttons", { - input: describeElement(input), - composer: describeElement(composer), - buttons: buttons.map((button) => ({ - label: buttonLabel(button), - element: describeElement(button), - })), - }) - - const micButton = buttons.find((button) => isGeminiMicButton(button)) - const sendButton = buttons.find((button) => isGeminiSendButton(button)) - const anchorButton = micButton || sendButton || buttons[buttons.length - 1] - const anchorSlot = findGeminiButtonSlot(anchorButton, composer) - const targetContainer = - anchorSlot?.parentElement || - (input.closest("rich-textarea") as HTMLElement | null)?.parentElement || - input.parentElement - - if (!targetContainer) { - debugGemini("could not find insertion target", { - anchor: anchorButton ? describeElement(anchorButton) : null, - input: describeElement(input), - }) - return - } - - const supermemoryIcon = createGeminiInputBarElement(async () => { - await getRelatedMemoriesForGemini( - POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_SEARCHED, - ) - }) - - supermemoryIcon.id = `${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - if (anchorSlot?.parentElement === targetContainer) { - targetContainer.insertBefore(supermemoryIcon, anchorSlot) - debugGemini("inserted marker before anchor button", { - anchorLabel: anchorButton ? buttonLabel(anchorButton) : null, - anchorSlot: describeElement(anchorSlot), - target: describeElement(targetContainer), - }) - return - } - - targetContainer.appendChild(supermemoryIcon) - debugGemini("inserted marker into fallback target", { - target: describeElement(targetContainer), - }) -} - -function getGeminiPromptInput(): GeminiInput | null { - return document.querySelector( - 'rich-textarea .ql-editor[contenteditable="true"], rich-textarea [contenteditable="true"], .ql-editor[contenteditable="true"], div[contenteditable="true"], textarea', - ) as GeminiInput | null -} - -function findGeminiComposerRoot(input: GeminiInput): HTMLElement { - const form = input.closest("form") as HTMLElement | null - if (form) return form - - let current: HTMLElement | null = input - for (let depth = 0; current && depth < 8; depth += 1) { - if (current.querySelectorAll("button").length >= 2) { - return current - } - current = current.parentElement - } - - return input.parentElement || document.body -} - -function findGeminiComposerButtons( - input: GeminiInput, - composer: HTMLElement, -): HTMLButtonElement[] { - const composerButtons = Array.from(composer.querySelectorAll("button")) - if (composerButtons.length > 0) { - return composerButtons - } - - const inputRect = input.getBoundingClientRect() - const allButtons = Array.from(document.querySelectorAll("button")) - - return allButtons.filter((button) => { - const rect = button.getBoundingClientRect() - const verticallyNear = - Math.abs( - rect.top + rect.height / 2 - (inputRect.top + inputRect.height / 2), - ) < 120 - const horizontallyNear = - rect.left > inputRect.left - 80 && rect.left < inputRect.right + 240 - - return verticallyNear && horizontallyNear - }) -} - -function buttonLabel(button: HTMLButtonElement): string { - return [ - button.getAttribute("aria-label"), - button.getAttribute("title"), - button.getAttribute("data-testid"), - button.getAttribute("data-test-id"), - button.getAttribute("jsname"), - button.textContent, - ] - .filter(Boolean) - .join(" ") -} - -function isGeminiMicButton(button: HTMLButtonElement): boolean { - return /mic|microphone|voice|dictate|audio/i.test(buttonLabel(button)) -} - -function isGeminiSendButton(button: HTMLButtonElement): boolean { - const label = buttonLabel(button) - if (/send|submit/i.test(label)) { - return true - } - - return !!button.querySelector( - 'mat-icon[fonticon="send"], mat-icon[data-mat-icon-name="send"], [data-icon-name="send"]', - ) -} - -function findGeminiButtonSlot( - button: HTMLButtonElement | undefined, - composer: HTMLElement, -): HTMLElement | null { - if (!button) return null - - let current: HTMLElement | null = button - while (current?.parentElement && current.parentElement !== composer) { - const parent: HTMLElement = current.parentElement - const parentStyle = window.getComputedStyle(parent) - const hasSiblingControls = parent.children.length > 1 - const isRow = - parentStyle.display.includes("flex") && - parentStyle.flexDirection !== "column" - - if (hasSiblingControls && isRow) { - return current - } - - current = parent - } - - return current || button -} - -function describeElement(element: Element | null): string | null { - if (!element) return null - - const parts = [element.tagName.toLowerCase()] - if (element.id) parts.push(`#${element.id}`) - if (element.className && typeof element.className === "string") { - parts.push( - `.${element.className.trim().split(/\s+/).slice(0, 4).join(".")}`, - ) - } - - for (const attr of ["aria-label", "data-testid", "data-test-id", "role"]) { - const value = element.getAttribute(attr) - if (value) parts.push(`[${attr}="${value}"]`) - } - - return parts.join("") -} - -function getGeminiDomSnapshot() { - return { - richTextareas: document.querySelectorAll("rich-textarea").length, - qlEditors: document.querySelectorAll(".ql-editor").length, - contenteditables: document.querySelectorAll('[contenteditable="true"]') - .length, - textareas: document.querySelectorAll("textarea").length, - buttons: document.querySelectorAll("button").length, - } -} - -function getInputText(input: GeminiInput | null): string { - if (!input) return "" - if (input instanceof HTMLTextAreaElement) { - return input.value || "" - } - - return input.innerText || input.textContent || "" -} - -async function getRelatedMemoriesForGemini(actionSource: string) { - try { - const isAutoSearch = - actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED - const input = getGeminiPromptInput() - const userQuery = getInputText(input).trim() - debugGemini("manual/auto memory search requested", { - actionSource, - hasInput: !!input, - queryLength: userQuery.length, - }) - - if (!userQuery) { - debugGemini("memory search skipped because query is empty") - return - } - - const iconElement = document.querySelector( - `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, - ) as HTMLElement | null - - if (!iconElement) { - console.warn("Gemini icon element not found, cannot update feedback") - return - } - - if (input && isAutoSearch) { - showLoadingSuggestion("gemini", input) - } - setMemoryMarkerStatus(iconElement, "searching") - if (!isAutoSearch) { - updateGeminiIconFeedback("Searching memories...", iconElement) - } - - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Memory search timeout")), - UI_CONFIG.API_REQUEST_TIMEOUT, - ), - ) - - const response = (await Promise.race([ - browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource, - }), - timeoutPromise, - ])) as { success?: boolean; data?: string } - - debugGemini("memory search response", response) - - if (response?.success && response?.data && input) { - const memoryText = showMemorySuggestion("gemini", input, response.data) - iconElement.dataset.memoriesData = serializeMemoriesForDataset( - response.data, - ) - iconElement.dataset.supermemories = memoryText - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "found") - } else { - updateGeminiIconFeedback("Included Memories", iconElement) - } - return - } - - if (isAutoSearch) { - setMemoryMarkerStatus(iconElement, "none") - } else { - updateGeminiIconFeedback("No memories found", iconElement, 1800) - } - } catch (error) { - console.error("Error getting related memories for Gemini:", error) - const iconElement = document.querySelector( - `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, - ) as HTMLElement | null - if (iconElement) { - if ( - actionSource === POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED - ) { - setMemoryMarkerStatus(iconElement, "error") - } else { - updateGeminiIconFeedback("Error fetching memories", iconElement, 1800) - } - } - } -} - -function updateGeminiIconFeedback( - message: string, - iconElement: HTMLElement, - resetAfter = 0, -) { - const memories = iconElement.dataset.memoriesData - const fallbackReset = - resetAfter || (message === "Included Memories" ? 0 : 2200) - - if (message === "Included Memories" || message === "Memories found") { - setMemoryMarkerStatus(iconElement, "found") - showMarkerPopover(iconElement, "Included Memories", memories) - return - } - - if (message.toLowerCase().includes("searching")) { - setMemoryMarkerStatus(iconElement, "searching") - showMarkerPopover(iconElement, message) - return - } - - setMemoryMarkerStatus( - iconElement, - message.toLowerCase().includes("error") ? "error" : "none", - ) - showMarkerPopover(iconElement, message, undefined, fallbackReset) -} - -function setupGeminiPromptCapture() { - if (document.body.hasAttribute("data-gemini-prompt-capture-setup")) { - return - } - - document.body.setAttribute("data-gemini-prompt-capture-setup", "true") - - const captureGeminiPromptContent = async (source: string) => { - const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false - debugGemini("capture requested", { source, autoCapture }) - - if (!autoCapture) { - debugGemini("auto prompt capture disabled") - return - } - - const input = getGeminiPromptInput() - const promptContent = getInputText(input) - debugGemini("capture input state", { - hasInput: !!input, - promptLength: promptContent.length, - hasStoredMemories: !!input?.dataset.supermemories, - }) - - if (promptContent.trim()) { - try { - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.CAPTURE_PROMPT, - data: { - prompt: promptContent, - platform: "gemini", - source: window.location.href, - }, - }) - debugGemini("capture response", response) - } catch (error) { - console.error("Error sending Gemini prompt to background:", error) - } - } else { - debugGemini("capture skipped because prompt is empty") - } - - const icons = document.querySelectorAll( - `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - iconElement.querySelector("[data-supermemory-status-badge]")?.remove() - delete iconElement.dataset.supermemoryStatus - delete iconElement.dataset.memoriesData - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - } - }) - - if (input?.dataset.supermemories) { - clearMemorySuggestion("gemini", input) - } - } - - document.addEventListener( - "click", - async (event) => { - const target = event.target as HTMLElement - if (target.closest('[data-supermemory-connected-indicator="true"]')) { - return - } - - const sendButton = target.closest("button") - if (sendButton && isGeminiSendButton(sendButton as HTMLButtonElement)) { - debugGemini("send button click detected", { - label: buttonLabel(sendButton as HTMLButtonElement), - element: describeElement(sendButton), - }) - await captureGeminiPromptContent("button click") - } - }, - true, - ) - - document.addEventListener( - "keydown", - async (event) => { - const target = event.target as HTMLElement - - const activeInput = - (target.closest('[contenteditable="true"]') as GeminiInput | null) || - (target.matches("textarea") ? (target as HTMLTextAreaElement) : null) - if (acceptMemorySuggestion(event, "gemini", activeInput)) { - return - } - - if ( - (target.matches("textarea") || - target.matches('[contenteditable="true"]') || - target.closest('[contenteditable="true"]')) && - event.key === "Enter" && - !event.shiftKey - ) { - debugGemini("Enter submit detected", { - target: describeElement(target), - }) - await captureGeminiPromptContent("Enter key") - } - }, - true, - ) -} - -async function setupGeminiAutoFetch() { - const autoSearch = (await autoSearchEnabled.getValue()) ?? false - debugGemini("setup auto fetch", { autoSearch }) - if (!autoSearch) { - return - } - - const input = getGeminiPromptInput() - if (!input || input.hasAttribute("data-supermemory-auto-fetch")) { - debugGemini("auto fetch skipped", { - hasInput: !!input, - alreadyAttached: input?.hasAttribute("data-supermemory-auto-fetch"), - }) - return - } - - input.setAttribute("data-supermemory-auto-fetch", "true") - debugGemini("auto fetch attached", describeElement(input)) - - const handleInput = () => { - const content = getInputText(input).trim() - syncAcceptedSupermemoryState(input) - - if (content.length === 0) { - clearMemorySuggestion("gemini", input) - document - .querySelectorAll(`[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`) - .forEach((icon) => { - setMemoryMarkerStatus(icon as HTMLElement, "neutral") - }) - } - - if (geminiDebounceTimeout) { - clearTimeout(geminiDebounceTimeout) - } - - geminiDebounceTimeout = setTimeout(async () => { - if (hasAcceptedSupermemoryContext(input)) { - clearMemorySuggestion("gemini", input) - return - } - - if (content.length > 2) { - await getRelatedMemoriesForGemini( - POSTHOG_EVENT_KEY.GEMINI_CHAT_MEMORIES_AUTO_SEARCHED, - ) - } else if (content.length === 0) { - const icons = document.querySelectorAll( - `[id*="${ELEMENT_IDS.GEMINI_INPUT_BAR_ELEMENT}"]`, - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - iconElement.querySelector("[data-supermemory-status-badge]")?.remove() - delete iconElement.dataset.supermemoryStatus - delete iconElement.dataset.memoriesData - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - } - }) - - if (input.dataset.supermemories) { - clearMemorySuggestion("gemini", input) - } - } - }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) - } - - input.addEventListener("input", handleInput) -} diff --git a/apps/browser-extension/entrypoints/content/grok.ts b/apps/browser-extension/entrypoints/content/grok.ts deleted file mode 100644 index 378ad750..00000000 --- a/apps/browser-extension/entrypoints/content/grok.ts +++ /dev/null @@ -1,445 +0,0 @@ -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 deleted file mode 100644 index 776d9e9f..00000000 --- a/apps/browser-extension/entrypoints/content/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -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 { initializeGemini } from "./gemini" -import { - saveMemory, - setupGlobalKeyboardShortcut, - setupStorageListener, -} from "./shared" -import { initializeT3 } from "./t3" -import { - handleTwitterNavigation, - initializeTwitter, - openImportModal, - updateTwitterImportUI, -} from "./twitter" - -export default defineContentScript({ - matches: [""], - main() { - // Setup global event listeners - browser.runtime.onMessage.addListener((message) => { - if (message.action === MESSAGE_TYPES.SHOW_TOAST) { - DOMUtils.showToast(message.state) - } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { - return saveMemory(message.actionSource || "content_script") - } else if (message.action === MESSAGE_TYPES.TWITTER_IMPORT_OPEN_MODAL) { - return openImportModal() - } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { - updateTwitterImportUI(message) - } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - updateTwitterImportUI(message) - } - }) - - // Setup global keyboard shortcuts - setupGlobalKeyboardShortcut() - - // Setup storage listener - setupStorageListener() - - // Observer for dynamic content changes - const observeForDynamicChanges = () => { - const observer = new MutationObserver(() => { - if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { - initializeChatGPT() - } - if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) { - initializeClaude() - } - if (DOMUtils.isOnDomain(DOMAINS.GROK)) { - initializeGrok() - } - if (DOMUtils.isOnDomain(DOMAINS.GEMINI)) { - initializeGemini() - } - if (DOMUtils.isOnDomain(DOMAINS.T3)) { - initializeT3() - } - if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - handleTwitterNavigation() - } - }) - - observer.observe(document.body, { - childList: true, - subtree: true, - }) - } - - // Initialize platform-specific functionality - initializeChatGPT() - initializeClaude() - initializeGrok() - initializeGemini() - initializeT3() - initializeTwitter() - - // Start observing for dynamic changes - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", observeForDynamicChanges) - } else { - observeForDynamicChanges() - } - }, -}) diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts deleted file mode 100644 index 27b65b08..00000000 --- a/apps/browser-extension/entrypoints/content/memory-suggestion.ts +++ /dev/null @@ -1,446 +0,0 @@ -type SuggestionInput = HTMLElement | HTMLTextAreaElement - -const SUGGESTION_ATTR = "data-supermemory-memory-suggestion" -const SUPERMEMORY_PREFIX = "Supermemories of user (only for the reference):" -const SUPERMEMORY_BLUE = "#1A88FF" - -export function buildSupermemoryText(memories: unknown): string { - const memoryText = Array.isArray(memories) - ? memories.join("").trim() - : String(memories || "").trim() - - return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}` -} - -function normalizeMemoryList(memories: unknown): string[] { - const list = Array.isArray(memories) - ? memories - : memories == null - ? [] - : [memories] - return list - .map((memory) => (typeof memory === "string" ? memory : String(memory))) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0) -} - -export function serializeMemoriesForDataset(memories: unknown): string { - const list = normalizeMemoryList(memories) - return list.length > 0 ? JSON.stringify(list) : "" -} - -export function parseMemoriesFromDataset( - raw: string | null | undefined, -): string[] { - if (!raw) return [] - try { - const parsed = JSON.parse(raw) - if (Array.isArray(parsed)) return normalizeMemoryList(parsed) - } catch { - // Not JSON — fall through to the legacy delimiter split. - } - return raw - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") -} - -export function renumberIncludedMemories(memories: string[]): string[] { - return memories.map((memory, index) => { - const text = memory.replace(/^\d+\.\s*/, "").replace(/\s+$/, "") - return `${index + 1}. ${text} \n` - }) -} - -export function showMemorySuggestion( - platform: string, - input: SuggestionInput, - memories: unknown, -): string { - const suggestionText = buildSupermemoryText(memories) - input.dataset.supermemories = suggestionText - delete input.dataset.supermemoriesInjected - - removeMemorySuggestion(platform) - - const anchor = getSuggestionAnchor(input) - if (!anchor) return suggestionText - - const previousPosition = window.getComputedStyle(anchor).position - if (previousPosition === "static") { - anchor.dataset.supermemoryPreviousPosition = "static" - anchor.style.position = "relative" - } - - const suggestion = createSuggestionContainer(platform, input, anchor) - suggestion.dataset.supermemorySuggestionState = "ready" - suggestion.style.gap = "8px" - suggestion.style.alignItems = "center" - - const text = document.createElement("span") - text.style.cssText = ` - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - ` - text.textContent = suggestionText.trim() - - const tabKey = document.createElement("span") - tabKey.style.cssText = ` - display: inline-flex; - align-items: center; - justify-content: center; - height: 20px; - padding: 0 8px; - border-radius: 999px; - background: ${SUPERMEMORY_BLUE}; - color: #FFFFFF; - font-size: 11px; - font-weight: 700; - line-height: 1; - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16) inset, 0 6px 18px rgba(26, 136, 255, 0.24); - flex-shrink: 0; - ` - tabKey.textContent = "Tab" - - suggestion.appendChild(text) - suggestion.appendChild(tabKey) - anchor.appendChild(suggestion) - - return suggestionText -} - -export function showLoadingSuggestion( - platform: string, - input: SuggestionInput, -) { - removeMemorySuggestion(platform) - - const anchor = getSuggestionAnchor(input) - if (!anchor) return - - const previousPosition = window.getComputedStyle(anchor).position - if (previousPosition === "static") { - anchor.dataset.supermemoryPreviousPosition = "static" - anchor.style.position = "relative" - } - - ensureSuggestionAnimationStyle() - - const suggestion = createSuggestionContainer(platform, input, anchor) - suggestion.dataset.supermemorySuggestionState = "loading" - suggestion.style.gap = "4px" - suggestion.setAttribute("aria-label", "supermemory searching memories") - - for (let index = 0; index < 3; index += 1) { - const dot = document.createElement("span") - dot.style.cssText = ` - width: 5px; - height: 5px; - border-radius: 999px; - background: ${SUPERMEMORY_BLUE}; - animation: supermemorySuggestionDot 1s ease-in-out infinite; - animation-delay: ${index * 0.14}s; - ` - suggestion.appendChild(dot) - } - - anchor.appendChild(suggestion) -} - -function createSuggestionContainer( - platform: string, - input: SuggestionInput, - anchor: HTMLElement, -): HTMLDivElement { - const suggestion = document.createElement("div") - suggestion.setAttribute(SUGGESTION_ATTR, platform) - const position = getCaretPosition(input, anchor) - const verticalOffset = platform === "gemini" ? -10 : 0 - suggestion.style.cssText = ` - position: absolute; - left: ${position.left + 6}px; - top: ${position.top + verticalOffset}px; - max-width: min(540px, calc(100% - ${position.left + 220}px)); - display: inline-flex; - align-items: center; - height: 22px; - color: rgba(255, 255, 255, 0.34); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - line-height: 1.35; - pointer-events: none; - z-index: 2147483646; - ` - return suggestion -} - -export function removeMemorySuggestion(platform: string) { - const elements = document.querySelectorAll( - `[${SUGGESTION_ATTR}="${platform}"]`, - ) - for (const element of elements) { - element.remove() - } -} - -export function acceptMemorySuggestion( - event: KeyboardEvent, - platform: string, - input: SuggestionInput | null, -): boolean { - if (event.key !== "Tab" || !input?.dataset.supermemories) { - return false - } - - event.preventDefault() - event.stopPropagation() - - const text = input.dataset.supermemories - appendTextToInput(input, text) - delete input.dataset.supermemories - input.dataset.supermemoriesInjected = "true" - removeMemorySuggestion(platform) - - return true -} - -export function hasAcceptedSupermemoryContext( - input: SuggestionInput | null, -): boolean { - if (!input) return false - const text = - input instanceof HTMLTextAreaElement - ? input.value - : input.innerText || input.textContent || "" - - return text.includes(SUPERMEMORY_PREFIX) -} - -export function syncAcceptedSupermemoryState(input: SuggestionInput | null) { - if (!input?.dataset.supermemoriesInjected) return - - if (!hasAcceptedSupermemoryContext(input)) { - delete input.dataset.supermemoriesInjected - } -} - -export function clearMemorySuggestion( - platform: string, - input: SuggestionInput | null, -) { - removeMemorySuggestion(platform) - if (input?.dataset.supermemories) { - delete input.dataset.supermemories - } - if (input?.dataset.supermemoriesInjected) { - delete input.dataset.supermemoriesInjected - } -} - -export function setMemoryMarkerStatus( - iconElement: HTMLElement | null, - status: "neutral" | "searching" | "found" | "none" | "error", -) { - if (!iconElement) return - - iconElement.querySelector("[data-supermemory-status-badge]")?.remove() - - if (status === "neutral" || status === "none") { - delete iconElement.dataset.supermemoryStatus - return - } - - iconElement.dataset.supermemoryStatus = status - const badge = document.createElement("span") - badge.dataset.supermemoryStatusBadge = "true" - badge.style.cssText = ` - position: absolute; - top: 3px; - right: 3px; - width: ${status === "searching" ? "7px" : "8px"}; - height: ${status === "searching" ? "7px" : "8px"}; - border-radius: 999px; - background: ${status === "found" ? "#36F3D7" : status === "searching" ? SUPERMEMORY_BLUE : status === "error" ? "#EF4444" : "rgba(255, 255, 255, 0.55)"}; - border: 1px solid rgba(5, 7, 10, 0.9); - box-shadow: ${status === "found" ? "0 0 0 2px rgba(54, 243, 215, 0.18)" : "none"}; - pointer-events: none; - ` - iconElement.appendChild(badge) -} - -export function showMarkerPopover( - iconElement: HTMLElement, - message: string, - memories?: string, - resetAfter = 0, -) { - iconElement.querySelector("[data-supermemory-marker-popover]")?.remove() - ensureSuggestionAnimationStyle() - - const popover = document.createElement("div") - popover.dataset.supermemoryMarkerPopover = "true" - popover.style.cssText = ` - position: absolute; - right: 0; - bottom: calc(100% + 10px); - min-width: 168px; - max-width: 280px; - padding: 10px; - border-radius: 12px; - background: rgba(10, 14, 20, 0.96); - border: 1px solid rgba(255, 255, 255, 0.12); - color: #FAFAFA; - box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 12px; - line-height: 1.35; - text-align: left; - z-index: 2147483647; - pointer-events: auto; - ` - - const title = document.createElement("div") - title.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - font-weight: 700; - margin-bottom: ${memories ? "8px" : "0"}; - ` - - if (message.toLowerCase().includes("searching")) { - const dots = document.createElement("span") - dots.style.cssText = "display: inline-flex; gap: 3px; align-items: center;" - for (let index = 0; index < 3; index += 1) { - const dot = document.createElement("span") - dot.style.cssText = ` - width: 4px; - height: 4px; - border-radius: 999px; - background: ${SUPERMEMORY_BLUE}; - animation: supermemorySuggestionDot 1s ease-in-out infinite; - animation-delay: ${index * 0.14}s; - ` - dots.appendChild(dot) - } - title.appendChild(dots) - } - - const titleText = document.createElement("span") - titleText.textContent = - message === "Included Memories" ? "Included memories" : message - title.appendChild(titleText) - popover.appendChild(title) - - if (memories) { - const list = document.createElement("div") - list.style.cssText = ` - display: flex; - flex-direction: column; - gap: 6px; - max-height: 160px; - overflow-y: auto; - color: rgba(255, 255, 255, 0.76); - ` - - parseMemoriesFromDataset(memories) - .slice(0, 5) - .forEach((memory) => { - const item = document.createElement("div") - item.textContent = memory - list.appendChild(item) - }) - - popover.appendChild(list) - } - - iconElement.appendChild(popover) - - if (resetAfter > 0) { - setTimeout(() => { - popover.remove() - }, resetAfter) - } -} - -function ensureSuggestionAnimationStyle() { - if (document.getElementById("supermemory-suggestion-animation-style")) { - return - } - - const style = document.createElement("style") - style.id = "supermemory-suggestion-animation-style" - style.textContent = ` - @keyframes supermemorySuggestionDot { - 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); } - 40% { opacity: 1; transform: translateY(-1px); } - } - ` - document.head.appendChild(style) -} - -function getSuggestionAnchor(input: SuggestionInput): HTMLElement | null { - return ( - (input.closest("form") as HTMLElement | null) || - (input.closest('[role="textbox"]') as HTMLElement | null)?.parentElement || - input.parentElement - ) -} - -function getCaretPosition(input: SuggestionInput, anchor: HTMLElement) { - const anchorRect = anchor.getBoundingClientRect() - - if (!(input instanceof HTMLTextAreaElement)) { - const selection = window.getSelection() - if (selection?.rangeCount) { - const range = selection.getRangeAt(0).cloneRange() - if (input.contains(range.startContainer)) { - range.collapse(true) - let rect = range.getBoundingClientRect() - if (rect.width === 0 && rect.height === 0) { - const marker = document.createElement("span") - marker.textContent = "\u200b" - range.insertNode(marker) - rect = marker.getBoundingClientRect() - marker.remove() - } - - if (rect.width || rect.height) { - return { - left: Math.max(18, rect.right - anchorRect.left + 4), - top: Math.max(10, rect.top - anchorRect.top), - } - } - } - } - } - - const inputRect = input.getBoundingClientRect() - return { - left: Math.max(18, inputRect.left - anchorRect.left + 18), - top: Math.max(10, inputRect.top - anchorRect.top + 8), - } -} - -function appendTextToInput(input: SuggestionInput, text: string) { - if (input instanceof HTMLTextAreaElement) { - input.value = `${input.value}${text}` - input.dispatchEvent(new Event("input", { bubbles: true })) - return - } - - input.focus() - const selection = window.getSelection() - const range = document.createRange() - range.selectNodeContents(input) - range.collapse(false) - range.insertNode(document.createTextNode(text)) - range.collapse(false) - selection?.removeAllRanges() - selection?.addRange(range) - input.dispatchEvent( - new InputEvent("input", { bubbles: true, inputType: "insertText" }), - ) -} diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts deleted file mode 100644 index 12647908..00000000 --- a/apps/browser-extension/entrypoints/content/shared.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { MESSAGE_TYPES } from "../../utils/constants" -import { bearerToken, userData } from "../../utils/storage" -import type { APIResponse } from "../../utils/types" -import { DOMUtils } from "../../utils/ui-components" -import { default as TurndownService } from "turndown" - -export async function saveMemory( - actionSource = "content_script", -): Promise { - try { - DOMUtils.showToast("loading") - - const highlightedText = window.getSelection()?.toString() || "" - const url = window.location.href - - const ogImage = - document - .querySelector('meta[property="og:image"]') - ?.getAttribute("content") || - document - .querySelector('meta[name="og:image"]') - ?.getAttribute("content") || - undefined - - const title = - document - .querySelector('meta[property="og:title"]') - ?.getAttribute("content") || - document - .querySelector('meta[name="og:title"]') - ?.getAttribute("content") || - document.title || - undefined - - const data: { - html?: string - markdown?: string - highlightedText?: string - url: string - ogImage?: string - title?: string - } = { - url, - } - - if (ogImage) { - data.ogImage = ogImage - } - - if (title) { - data.title = title - } - - if (highlightedText) { - data.highlightedText = highlightedText - } else { - const bodyClone = document.body.cloneNode(true) as HTMLElement - const scripts = bodyClone.querySelectorAll("script") - for (const script of scripts) { - script.remove() - } - const html = bodyClone.innerHTML - - // Convert HTML to markdown - const turndownService = new TurndownService() - const markdown = turndownService.turndown(html) - data.markdown = markdown - } - - const response = (await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.SAVE_MEMORY, - data, - actionSource, - })) as APIResponse - - if (response?.success) { - DOMUtils.showToast("success") - return response - } - DOMUtils.showToast("error") - return { - success: false, - error: response?.error || "Failed to save memory", - } - } catch (error) { - console.error("Error saving memory:", error) - DOMUtils.showToast("error") - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } - } -} - -export function setupGlobalKeyboardShortcut() { - document.addEventListener("keydown", async (event) => { - if ( - (event.ctrlKey || event.metaKey) && - event.shiftKey && - event.key === "m" - ) { - event.preventDefault() - await saveMemory("keyboard_shortcut") - } - }) -} - -export function setupStorageListener() { - window.addEventListener("message", async (event) => { - if (event.source !== window) { - return - } - const token = event.data.token - const user = event.data.userData - if (token && user) { - if ( - !( - window.location.hostname === "localhost" || - window.location.hostname === "supermemory.ai" || - window.location.hostname === "app.supermemory.ai" - ) - ) { - return - } - - try { - await Promise.all([ - bearerToken.setValue(token), - userData.setValue(user), - ]) - } catch { - // Do nothing - } - } - }) -} diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts deleted file mode 100644 index bddd83ed..00000000 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ /dev/null @@ -1,744 +0,0 @@ -import { - DOMAINS, - ELEMENT_IDS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, - UI_CONFIG, -} from "../../utils/constants" -import { - autoSearchEnabled, - autoCapturePromptsEnabled, -} from "../../utils/storage" -import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components" -import { - buildSupermemoryText, - parseMemoriesFromDataset, - renumberIncludedMemories, - serializeMemoriesForDataset, -} from "./memory-suggestion" - -let t3DebounceTimeout: NodeJS.Timeout | null = null -let t3RouteObserver: MutationObserver | null = null -let t3UrlCheckInterval: NodeJS.Timeout | null = null -let t3ObserverThrottle: NodeJS.Timeout | null = null -let t3IncludedPopup: { - el: HTMLElement - onClick: (event: MouseEvent) => void - timer: ReturnType -} | null = null - -function disposeT3IncludedPopup() { - if (!t3IncludedPopup) return - document.removeEventListener("click", t3IncludedPopup.onClick) - clearTimeout(t3IncludedPopup.timer) - t3IncludedPopup.el.remove() - t3IncludedPopup = null -} - -export function initializeT3() { - if (!DOMUtils.isOnDomain(DOMAINS.T3)) { - return - } - - if (document.body.hasAttribute("data-t3-initialized")) { - return - } - - setTimeout(() => { - addSupermemoryIconToT3Input() - setupT3AutoFetch() - }, 2000) - - setupT3PromptCapture() - - setupT3RouteChangeDetection() - - document.body.setAttribute("data-t3-initialized", "true") -} - -function setupT3RouteChangeDetection() { - if (t3RouteObserver) { - t3RouteObserver.disconnect() - } - if (t3UrlCheckInterval) { - clearInterval(t3UrlCheckInterval) - } - if (t3ObserverThrottle) { - clearTimeout(t3ObserverThrottle) - t3ObserverThrottle = null - } - - let currentUrl = window.location.href - - const checkForRouteChange = () => { - if (window.location.href !== currentUrl) { - disposeT3IncludedPopup() - currentUrl = window.location.href - setTimeout(() => { - addSupermemoryIconToT3Input() - setupT3AutoFetch() - }, 1000) - } - } - - t3UrlCheckInterval = setInterval(checkForRouteChange, 2000) - - t3RouteObserver = new MutationObserver((mutations) => { - if (t3ObserverThrottle) { - return - } - - let shouldRecheck = false - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element - if ( - element.querySelector?.("textarea") || - element.querySelector?.('div[contenteditable="true"]') || - element.matches?.("textarea") || - element.matches?.('div[contenteditable="true"]') - ) { - shouldRecheck = true - } - } - }) - } - }) - - if (shouldRecheck) { - t3ObserverThrottle = setTimeout(() => { - try { - t3ObserverThrottle = null - addSupermemoryIconToT3Input() - setupT3AutoFetch() - } catch (error) { - console.error("Error in T3 observer callback:", error) - } - }, 300) - } - }) - - try { - t3RouteObserver.observe(document.body, { - childList: true, - subtree: true, - }) - } catch (error) { - console.error("Failed to set up T3 route observer:", error) - if (t3UrlCheckInterval) { - clearInterval(t3UrlCheckInterval) - } - t3UrlCheckInterval = setInterval(checkForRouteChange, 1000) - } -} - -function addSupermemoryIconToT3Input() { - const targetContainers = document.querySelectorAll( - ".flex.min-w-0.items-center.gap-2", - ) - - const container = targetContainers[0] - if (!container) { - return - } - - if (container.hasAttribute("data-supermemory-icon-added")) { - return - } - - const existingIcon = container.querySelector( - `#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`, - ) - if (existingIcon) { - container.setAttribute("data-supermemory-icon-added", "true") - return - } - - const supermemoryIcon = createT3InputBarElement(async () => { - await getRelatedMemoriesForT3(POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED) - }) - - supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}` - - container.setAttribute("data-supermemory-icon-added", "true") - - container.insertBefore(supermemoryIcon, container.firstChild) -} - -async function getRelatedMemoriesForT3(actionSource: string) { - try { - let userQuery = "" - - const supermemoryContainer = document.querySelector( - '[data-supermemory-icon-added="true"]', - ) - if (supermemoryContainer?.parentElement?.previousElementSibling) { - const textareaElement = - supermemoryContainer.parentElement.previousElementSibling.querySelector( - "textarea", - ) - userQuery = textareaElement?.value || "" - } - - if (!userQuery.trim()) { - const textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - userQuery = - textareaElement?.innerText || textareaElement?.textContent || "" - } - - if (!userQuery.trim()) { - const textareas = document.querySelectorAll("textarea") - for (const textarea of textareas) { - const text = (textarea as HTMLTextAreaElement).value - if (text?.trim()) { - userQuery = text.trim() - break - } - } - } - - if (!userQuery.trim()) { - return - } - - const icon = document.querySelector('[id*="sm-t3-input-bar-element"]') - - const iconElement = icon as HTMLElement - - if (!iconElement) { - console.warn("T3 icon element not found, cannot update feedback") - return - } - - updateT3IconFeedback("Searching memories...", iconElement) - - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Memory search timeout")), - UI_CONFIG.API_REQUEST_TIMEOUT, - ), - ) - - const response = await Promise.race([ - browser.runtime.sendMessage({ - action: MESSAGE_TYPES.GET_RELATED_MEMORIES, - data: userQuery, - actionSource: actionSource, - }), - timeoutPromise, - ]) - - if (response?.success && response?.data) { - let textareaElement = null - const supermemoryContainer = document.querySelector( - '[data-supermemory-icon-added="true"]', - ) - if (supermemoryContainer?.parentElement?.previousElementSibling) { - textareaElement = - supermemoryContainer.parentElement.previousElementSibling.querySelector( - "textarea", - ) - } - - if (!textareaElement) { - textareaElement = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - } - - if (textareaElement) { - textareaElement.dataset.supermemories = buildSupermemoryText( - response.data, - ) - - iconElement.dataset.memoriesData = serializeMemoriesForDataset( - response.data, - ) - - updateT3IconFeedback("Included Memories", iconElement) - } else { - console.warn("T3 input area not found after successful memory fetch") - updateT3IconFeedback("Memories found", iconElement) - } - } else { - console.warn("No memories found or API response invalid for T3") - updateT3IconFeedback("No memories found", iconElement) - } - } catch (error) { - console.error("Error getting related memories for T3:", error) - try { - const icon = document.querySelector( - '[id*="sm-t3-input-bar-element"]', - ) as HTMLElement - if (icon) { - updateT3IconFeedback("Error fetching memories", icon) - } - } catch (feedbackError) { - console.error("Failed to update T3 error feedback:", feedbackError) - } - } -} - -function updateT3IconFeedback( - message: string, - iconElement: HTMLElement, - resetAfter = 0, -) { - if (!iconElement.dataset.originalHtml) { - iconElement.dataset.originalHtml = iconElement.innerHTML - } - - disposeT3IncludedPopup() - - const feedbackDiv = document.createElement("div") - feedbackDiv.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - padding: 6px 8px; - background: #513EA9; - border-radius: 6px; - color: white; - font-size: 12px; - font-weight: 500; - cursor: ${message === "Included Memories" ? "pointer" : "default"}; - position: relative; - ` - - feedbackDiv.innerHTML = ` - - ${message} - ` - - if (message === "Included Memories" && iconElement.dataset.memoriesData) { - const popup = document.createElement("div") - popup.style.cssText = ` - position: fixed; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - background: #1a1a1a; - color: white; - padding: 0; - border-radius: 12px; - font-size: 13px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - max-width: 500px; - max-height: 400px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); - z-index: 999999; - display: none; - border: 1px solid #333; - ` - - const header = document.createElement("div") - header.style.cssText = ` - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-bottom: 1px solid #333; - opacity: 0.8; - ` - header.innerHTML = ` - Included Memories - ` - - const content = document.createElement("div") - content.style.cssText = ` - padding: 0; - max-height: 300px; - overflow-y: auto; - ` - - const individualMemories = parseMemoriesFromDataset( - iconElement.dataset.memoriesData, - ) - - individualMemories.forEach((memory, index) => { - const memoryItem = document.createElement("div") - memoryItem.style.cssText = ` - display: flex; - align-items: center; - gap: 6px; - padding: 10px; - font-size: 13px; - line-height: 1.4; - ` - - const memoryText = document.createElement("div") - memoryText.style.cssText = ` - flex: 1; - color: #e5e5e5; - ` - memoryText.textContent = memory.trim() - - const removeBtn = document.createElement("button") - removeBtn.style.cssText = ` - background: transparent; - color: #9ca3af; - border: none; - padding: 4px; - border-radius: 4px; - cursor: pointer; - flex-shrink: 0; - height: fit-content; - display: flex; - align-items: center; - justify-content: center; - ` - removeBtn.innerHTML = `` - removeBtn.dataset.memoryIndex = index.toString() - - removeBtn.addEventListener("mouseenter", () => { - removeBtn.style.color = "#ef4444" - }) - removeBtn.addEventListener("mouseleave", () => { - removeBtn.style.color = "#9ca3af" - }) - - memoryItem.appendChild(memoryText) - memoryItem.appendChild(removeBtn) - content.appendChild(memoryItem) - }) - - popup.appendChild(header) - popup.appendChild(content) - document.body.appendChild(popup) - - feedbackDiv.addEventListener("mouseenter", () => { - const textSpan = feedbackDiv.querySelector("span:last-child") - if (textSpan) { - textSpan.textContent = "Click to see memories" - } - }) - - feedbackDiv.addEventListener("mouseleave", () => { - const textSpan = feedbackDiv.querySelector("span:last-child") - if (textSpan) { - textSpan.textContent = "Included Memories" - } - }) - - feedbackDiv.addEventListener("click", (e) => { - e.stopPropagation() - popup.style.display = "block" - }) - - const onClick = (e: MouseEvent) => { - if (!popup.contains(e.target as Node)) { - popup.style.display = "none" - } - } - document.addEventListener("click", onClick) - t3IncludedPopup = { - el: popup, - onClick, - timer: setTimeout(disposeT3IncludedPopup, 300000), - } - - content.querySelectorAll("button[data-memory-index]").forEach((button) => { - const htmlButton = button as HTMLButtonElement - htmlButton.addEventListener("click", () => { - const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) - htmlButton.parentElement?.remove() - - const remainingMemories = parseMemoriesFromDataset( - iconElement.dataset.memoriesData, - ) - remainingMemories.splice(index, 1) - const remaining = renumberIncludedMemories(remainingMemories) - - const textareaElement = - (document.querySelector("textarea") as HTMLTextAreaElement) || - (document.querySelector('div[contenteditable="true"]') as HTMLElement) - - // Only wipe when nothing remains — `<= 1` used to discard the last kept memory. - if (remaining.length === 0) { - if (textareaElement?.dataset.supermemories) { - delete textareaElement.dataset.supermemories - } - delete iconElement.dataset.memoriesData - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - disposeT3IncludedPopup() - return - } - - iconElement.dataset.memoriesData = - serializeMemoriesForDataset(remaining) - if (textareaElement) { - textareaElement.dataset.supermemories = - buildSupermemoryText(remaining) - } - - content - .querySelectorAll("button[data-memory-index]") - .forEach((btn, newIndex) => { - const htmlBtn = btn as HTMLButtonElement - htmlBtn.dataset.memoryIndex = String(newIndex) - const label = htmlBtn.previousElementSibling - if (label) { - label.textContent = remaining[newIndex].trim() - } - }) - }) - }) - } - - iconElement.innerHTML = "" - iconElement.appendChild(feedbackDiv) - - if (resetAfter > 0) { - setTimeout(() => { - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - }, resetAfter) - } -} - -function setupT3PromptCapture() { - if (document.body.hasAttribute("data-t3-prompt-capture-setup")) { - return - } - document.body.setAttribute("data-t3-prompt-capture-setup", "true") - - const captureT3PromptContent = async (_source: string) => { - const autoCapture = (await autoCapturePromptsEnabled.getValue()) ?? false - - if (!autoCapture) { - return - } - let promptContent = "" - - const textarea = document.querySelector("textarea") as HTMLTextAreaElement - if (textarea) { - promptContent = textarea.value || "" - } - - if (!promptContent) { - const contentEditableDiv = document.querySelector( - 'div[contenteditable="true"]', - ) as HTMLElement - if (contentEditableDiv) { - promptContent = - contentEditableDiv.textContent || contentEditableDiv.innerText || "" - } - } - - const textareaElement = - textarea || - (document.querySelector('div[contenteditable="true"]') as HTMLElement) - const storedMemories = textareaElement?.dataset.supermemories - if ( - storedMemories && - textareaElement && - !promptContent.includes("Supermemories of user") - ) { - if (textareaElement.tagName === "TEXTAREA") { - ;(textareaElement as HTMLTextAreaElement).value = - `${promptContent} ${storedMemories}` - promptContent = (textareaElement as HTMLTextAreaElement).value - } else { - textareaElement.appendChild(document.createTextNode(storedMemories)) - promptContent = - textareaElement.textContent || textareaElement.innerText || "" - } - } - - if (promptContent.trim()) { - try { - await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.CAPTURE_PROMPT, - data: { - prompt: promptContent, - platform: "t3", - source: window.location.href, - }, - }) - } catch (error) { - console.error("Error sending T3 prompt to background:", error) - } - } - - const icons = document.querySelectorAll('[id*="sm-t3-input-bar-element"]') - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - delete iconElement.dataset.memoriesData - } - }) - - if (textareaElement?.dataset.supermemories) { - delete textareaElement.dataset.supermemories - } - disposeT3IncludedPopup() - } - - const handleT3SendButtonClick = async (event: Event) => { - const target = event.target as HTMLElement - const sendButton = - target.closest("button.focus-visible\\:ring-ring") || - target.closest('button[class*="bg-[rgb(162,59,103)]"]') || - target.closest('button[class*="rounded-lg"]') - - if (sendButton) { - const textareaElement = - (document.querySelector("textarea") as HTMLTextAreaElement) || - (document.querySelector('div[contenteditable="true"]') as HTMLElement) - - const hasMemories = - textareaElement?.dataset.supermemories || - ( - document.querySelector( - '[id*="sm-t3-input-bar-element"]', - ) as HTMLElement - )?.dataset.memoriesData - - if (!hasMemories) { - return // No memories present, let the button click proceed normally - } - - event.preventDefault() - event.stopPropagation() - - await captureT3PromptContent("button click") - - setTimeout(() => { - const form = sendButton.closest("form") - if (form) { - form.requestSubmit() - } else { - const newEvent = new MouseEvent("click", { - bubbles: true, - cancelable: true, - view: window, - }) - document.removeEventListener("click", handleT3SendButtonClick, true) - sendButton.dispatchEvent(newEvent) - setTimeout(() => { - document.addEventListener("click", handleT3SendButtonClick, true) - }, 100) - } - }, 100) - } - } - - const handleT3EnterKey = async (event: KeyboardEvent) => { - const target = event.target as HTMLElement - - if ( - (target.matches("textarea") || - target.matches('div[contenteditable="true"]')) && - event.key === "Enter" && - !event.shiftKey - ) { - const textareaElement = - (document.querySelector("textarea") as HTMLTextAreaElement) || - (document.querySelector('div[contenteditable="true"]') as HTMLElement) - - const hasMemories = - textareaElement?.dataset.supermemories || - ( - document.querySelector( - '[id*="sm-t3-input-bar-element"]', - ) as HTMLElement - )?.dataset.memoriesData - - if (!hasMemories) { - return // No memories present, let the Enter key proceed normally - } - - event.preventDefault() - event.stopPropagation() - await captureT3PromptContent("Enter key") - - setTimeout(() => { - const form = target.closest("form") - if (form) { - form.requestSubmit() - } else { - const newEvent = new KeyboardEvent("keydown", { - key: "Enter", - code: "Enter", - bubbles: true, - cancelable: true, - }) - target.dispatchEvent(newEvent) - } - }, 100) - } - } - - document.addEventListener("click", handleT3SendButtonClick, true) - document.addEventListener("keydown", handleT3EnterKey, true) -} - -async function setupT3AutoFetch() { - const autoSearch = (await autoSearchEnabled.getValue()) ?? false - - if (!autoSearch) { - return - } - - const textareaElement = - (document.querySelector("textarea") as HTMLTextAreaElement) || - (document.querySelector('div[contenteditable="true"]') as HTMLElement) - - if ( - !textareaElement || - textareaElement.hasAttribute("data-supermemory-auto-fetch") - ) { - return - } - - textareaElement.setAttribute("data-supermemory-auto-fetch", "true") - - const handleInput = () => { - if (t3DebounceTimeout) { - clearTimeout(t3DebounceTimeout) - } - - t3DebounceTimeout = setTimeout(async () => { - let content = "" - if (textareaElement.tagName === "TEXTAREA") { - content = (textareaElement as HTMLTextAreaElement).value?.trim() || "" - } else { - content = textareaElement.textContent?.trim() || "" - } - - if (content.length > 2) { - await getRelatedMemoriesForT3( - POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_AUTO_SEARCHED, - ) - } else if (content.length === 0) { - const icons = document.querySelectorAll( - '[id*="sm-t3-input-bar-element"]', - ) - - icons.forEach((icon) => { - const iconElement = icon as HTMLElement - if (iconElement.dataset.originalHtml) { - iconElement.innerHTML = iconElement.dataset.originalHtml - delete iconElement.dataset.originalHtml - delete iconElement.dataset.memoriesData - } - }) - - if (textareaElement.dataset.supermemories) { - delete textareaElement.dataset.supermemories - } - disposeT3IncludedPopup() - } - }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) - } - - textareaElement.addEventListener("input", handleInput) -} diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts deleted file mode 100644 index 875f4bab..00000000 --- a/apps/browser-extension/entrypoints/content/twitter.ts +++ /dev/null @@ -1,739 +0,0 @@ -import { - DOMAINS, - ELEMENT_IDS, - MESSAGE_TYPES, - POSTHOG_EVENT_KEY, - STORAGE_KEYS, - UI_CONFIG, -} from "../../utils/constants" -import { trackEvent } from "../../utils/posthog" -import { - createProjectSelectionModal, - createSaveTweetElement, - DOMUtils, -} from "../../utils/ui-components" - -async function loadSpaceGroteskFonts(): Promise { - if (document.getElementById("supermemory-modal-styles")) { - return Promise.resolve() - } - - const style = document.createElement("style") - style.id = "supermemory-modal-styles" - style.textContent = ` - @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap'); - ` - document.head.appendChild(style) - - await document.fonts.ready -} - -/** - * Check if import intent is valid (exists and not expired) - */ -async function checkAndConsumeImportIntent(): Promise { - try { - const result = await browser.storage.local.get( - STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL, - ) - const intentUntil = result[ - STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL - ] as number | undefined - - if (intentUntil && Date.now() < intentUntil) { - await browser.storage.local.remove( - STORAGE_KEYS.TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL, - ) - return true - } - return false - } catch (error) { - console.error("Error checking import intent:", error) - return false - } -} - -/** - * Check if onboarding toast has been shown before - */ -async function hasOnboardingBeenShown(): Promise { - try { - const result = await browser.storage.local.get( - STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN, - ) - return !!result[STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN] - } catch (error) { - console.error("Error checking onboarding status:", error) - return true // Default to true to avoid showing toast on error - } -} - -/** - * Mark onboarding toast as shown - */ -async function markOnboardingAsShown(): Promise { - try { - await browser.storage.local.set({ - [STORAGE_KEYS.TWITTER_BOOKMARKS_ONBOARDING_SEEN]: true, - }) - } catch (error) { - console.error("Error marking onboarding as shown:", error) - } -} - -export async function initializeTwitter() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return - } - - if (window.location.pathname === "/i/bookmarks") { - setTimeout(async () => { - if (window.location.pathname === "/i/bookmarks") { - await handleBookmarksPageLoad() - } - }, 2000) - } else { - // Clean up any injected UI if navigating away - removeAllTwitterUI() - } -} - -/** - * Handle what to show when user lands on bookmarks page - */ -async function handleBookmarksPageLoad() { - if (window.location.pathname !== "/i/bookmarks") { - return - } - - addTwitterImportButtonForFolders() // Add buttons to bookmark folders - - const hasIntent = await checkAndConsumeImportIntent() - - if (hasIntent) { - await openImportModal() - return - } - - const onboardingShown = await hasOnboardingBeenShown() - - if (!onboardingShown) { - await showOnboardingToast() - await markOnboardingAsShown() - } -} - -/** - * Opens the import modal and handles the import flow - */ -export async function openImportModal() { - try { - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.FETCH_PROJECTS, - }) - - const projects = response.success && response.data ? response.data : [] - - if (projects.length === 0) { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - }) - await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, { - source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`, - }) - } else { - await showAllBookmarksProjectModal(projects) - } - } catch (error) { - console.error("Error opening import modal:", error) - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - }) - } -} - -async function showAllBookmarksProjectModal( - projects: Array<{ id: string; name: string; containerTag: string }>, -) { - await loadSpaceGroteskFonts() - - const modal = createProjectSelectionModal( - projects, - async (selectedProject) => { - modal.remove() - - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - selectedProject: selectedProject, - }) - await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, { - source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`, - project_selected: true, - }) - } catch (error) { - console.error("Error importing all bookmarks:", error) - } - }, - () => { - modal.remove() - }, - ) - - document.body.appendChild(modal) -} - -/** - * Shows the one-time onboarding toast with progress bar - */ -async function showOnboardingToast() { - await loadSpaceGroteskFonts() - - // Remove any existing toast - const existingToast = document.getElementById( - ELEMENT_IDS.TWITTER_ONBOARDING_TOAST, - ) - if (existingToast) { - existingToast.remove() - } - - const duration = UI_CONFIG.ONBOARDING_TOAST_DURATION - - // Create toast container - const toast = document.createElement("div") - toast.id = ELEMENT_IDS.TWITTER_ONBOARDING_TOAST - toast.style.cssText = ` - position: fixed; - bottom: 20px; - right: 20px; - z-index: 2147483647; - background: #ffffff; - border-radius: 12px; - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - color: #374151; - min-width: 320px; - max-width: 380px; - box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); - animation: smSlideInUp 0.3s ease-out; - overflow: hidden; - ` - - // Add keyframe animations if not already present - if (!document.getElementById("supermemory-onboarding-toast-styles")) { - const style = document.createElement("style") - style.id = "supermemory-onboarding-toast-styles" - style.textContent = ` - @keyframes smSlideInUp { - from { transform: translateY(100%); opacity: 0; } - to { transform: translateY(0); opacity: 1; } - } - @keyframes smFadeOut { - from { transform: translateY(0); opacity: 1; } - to { transform: translateY(100%); opacity: 0; } - } - @keyframes smProgressGrow { - from { transform: scaleX(0); } - to { transform: scaleX(1); } - } - @keyframes smPulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } - } - ` - document.head.appendChild(style) - } - - // Header with icon, text and close button - const header = document.createElement("div") - header.style.cssText = - "display: flex; align-items: flex-start; gap: 12px; position: relative;" - - const iconUrl = browser.runtime.getURL("/new_logo.png") - const icon = document.createElement("img") - icon.src = iconUrl - icon.alt = "Supermemory" - icon.style.cssText = - "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;" - - const textContainer = document.createElement("div") - textContainer.style.cssText = - "display: flex; flex-direction: column; gap: 4px; flex: 1;" - - const title = document.createElement("span") - title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;" - title.textContent = "Import X/Twitter Bookmarks" - - const description = document.createElement("span") - description.style.cssText = - "font-size: 13px; color: #6b7280; line-height: 1.4;" - description.textContent = - "You can import all your Twitter bookmarks to Supermemory with one click." - - textContainer.appendChild(title) - textContainer.appendChild(description) - - // Close button - const closeButton = document.createElement("button") - closeButton.setAttribute("aria-label", "Close onboarding toast") - closeButton.style.cssText = ` - position: absolute; - top: 0; - right: 0; - background: transparent; - border: none; - cursor: pointer; - padding: 4px; - color: #9ca3af; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background-color 0.2s; - ` - closeButton.innerHTML = ` - - ` - closeButton.addEventListener("mouseenter", () => { - closeButton.style.backgroundColor = "#f3f4f6" - }) - closeButton.addEventListener("mouseleave", () => { - closeButton.style.backgroundColor = "transparent" - }) - closeButton.addEventListener("click", () => { - dismissToast(toast) - }) - - header.appendChild(icon) - header.appendChild(textContainer) - header.appendChild(closeButton) - - // Action buttons - const buttonsContainer = document.createElement("div") - buttonsContainer.style.cssText = "display: flex; gap: 8px; margin-top: 4px;" - - const importButton = document.createElement("button") - importButton.style.cssText = ` - padding: 8px 16px; - border: none; - border-radius: 8px; - background: linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%); - color: white; - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: opacity 0.2s; - font-family: inherit; - ` - importButton.textContent = "Import now" - importButton.addEventListener("mouseenter", () => { - importButton.style.opacity = "0.9" - }) - importButton.addEventListener("mouseleave", () => { - importButton.style.opacity = "1" - }) - importButton.addEventListener("click", async () => { - dismissToast(toast) - await openImportModal() - }) - - const learnMoreButton = document.createElement("button") - learnMoreButton.style.cssText = ` - padding: 8px 16px; - border: 1px solid #e5e7eb; - border-radius: 8px; - background: transparent; - color: #374151; - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s; - font-family: inherit; - ` - learnMoreButton.textContent = "Learn more" - learnMoreButton.addEventListener("mouseenter", () => { - learnMoreButton.style.backgroundColor = "#f9fafb" - }) - learnMoreButton.addEventListener("mouseleave", () => { - learnMoreButton.style.backgroundColor = "transparent" - }) - learnMoreButton.addEventListener("click", () => { - window.open("https://docs.supermemory.ai/connectors/twitter", "_blank") - }) - - buttonsContainer.appendChild(importButton) - buttonsContainer.appendChild(learnMoreButton) - - // Progress bar container - const progressBarContainer = document.createElement("div") - progressBarContainer.setAttribute("role", "progressbar") - progressBarContainer.setAttribute("aria-valuemin", "0") - progressBarContainer.setAttribute("aria-valuemax", "100") - progressBarContainer.setAttribute("aria-valuenow", "0") - progressBarContainer.setAttribute( - "aria-label", - "Onboarding toast auto-dismiss progress", - ) - progressBarContainer.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 3px; - background: #e5e7eb; - ` - - const progressBar = document.createElement("div") - progressBar.style.cssText = ` - height: 100%; - background: linear-gradient(90deg, #0ff0d2, #5bd3fb, #1e0ff0); - transform-origin: left; - animation: smProgressGrow ${duration}ms linear forwards; - ` - - // Update progress bar ARIA value as animation progresses - const startTime = Date.now() - const updateProgress = () => { - const elapsed = Date.now() - startTime - const progress = Math.min(100, Math.round((elapsed / duration) * 100)) - progressBarContainer.setAttribute("aria-valuenow", String(progress)) - if (progress < 100) { - requestAnimationFrame(updateProgress) - } - } - requestAnimationFrame(updateProgress) - - progressBarContainer.appendChild(progressBar) - - // Assemble toast - toast.appendChild(header) - toast.appendChild(buttonsContainer) - toast.appendChild(progressBarContainer) - - document.body.appendChild(toast) - - // Auto-dismiss after duration - setTimeout(() => { - if (document.body.contains(toast)) { - dismissToast(toast) - } - }, duration) -} - -/** - * Dismiss the toast with animation - */ -function dismissToast(toast: HTMLElement) { - toast.style.animation = "smFadeOut 0.3s ease-out forwards" - setTimeout(() => { - if (document.body.contains(toast)) { - toast.remove() - } - }, 300) -} - -/** - * Remove all Twitter-specific injected UI - */ -function removeAllTwitterUI() { - // Remove import button (legacy) - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON) - } - // Remove onboarding toast - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_ONBOARDING_TOAST) - } - // Remove import progress toast - if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST)) { - DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST) - } - // Remove any folder buttons - document.querySelectorAll("[data-supermemory-button]").forEach((button) => { - button.remove() - }) -} - -/** - * Shows or updates the import progress toast in the bottom-right - */ -function showOrUpdateImportProgressToast(message: string, isComplete = false) { - let toast = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST) - - if (!toast) { - // Ensure animation styles are available - if (!document.getElementById("supermemory-onboarding-toast-styles")) { - const style = document.createElement("style") - style.id = "supermemory-onboarding-toast-styles" - style.textContent = ` - @keyframes smSlideInUp { - from { transform: translateY(100%); opacity: 0; } - to { transform: translateY(0); opacity: 1; } - } - @keyframes smFadeOut { - from { transform: translateY(0); opacity: 1; } - to { transform: translateY(100%); opacity: 0; } - } - @keyframes smPulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } - } - ` - document.head.appendChild(style) - } - - // Create new toast - toast = document.createElement("div") - toast.id = ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST - toast.style.cssText = ` - position: fixed; - bottom: 20px; - right: 20px; - z-index: 2147483647; - background: #ffffff; - border-radius: 12px; - padding: 14px 16px; - display: flex; - align-items: center; - gap: 12px; - font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - color: #374151; - min-width: 280px; - max-width: 360px; - box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12); - animation: smSlideInUp 0.3s ease-out; - ` - - const iconUrl = browser.runtime.getURL("/new_logo.png") - const icon = document.createElement("img") - icon.src = iconUrl - icon.alt = "Supermemory" - icon.id = "sm-import-progress-icon" - icon.style.cssText = - "width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; animation: smPulse 1.5s ease-in-out infinite;" - - const textSpan = document.createElement("span") - textSpan.id = "sm-import-progress-text" - textSpan.style.cssText = "font-weight: 500; flex: 1;" - textSpan.textContent = message - - toast.appendChild(icon) - toast.appendChild(textSpan) - document.body.appendChild(toast) - } else { - // Update existing toast - const textSpan = toast.querySelector( - "#sm-import-progress-text", - ) as HTMLSpanElement - if (textSpan) { - textSpan.textContent = message - } - } - - // Style for completion - if (isComplete) { - const icon = toast.querySelector( - "#sm-import-progress-icon", - ) as HTMLImageElement - if (icon) { - icon.style.animation = "none" - icon.style.opacity = "1" - } - - const textSpan = toast.querySelector( - "#sm-import-progress-text", - ) as HTMLSpanElement - if (textSpan) { - textSpan.style.color = "#059669" - } - - // Auto-dismiss after 4 seconds on completion - setTimeout(() => { - const existingToast = document.getElementById( - ELEMENT_IDS.TWITTER_IMPORT_PROGRESS_TOAST, - ) - if (existingToast) { - dismissToast(existingToast) - } - }, 4000) - } -} - -export function updateTwitterImportUI(message: { - type: string - importedMessage?: string - totalImported?: number -}) { - if (message.type === MESSAGE_TYPES.IMPORT_UPDATE && message.importedMessage) { - showOrUpdateImportProgressToast(message.importedMessage, false) - } - - if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - showOrUpdateImportProgressToast( - `✓ Imported ${message.totalImported} tweets!`, - true, - ) - } -} - -export async function handleTwitterNavigation() { - if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { - return - } - - if (window.location.pathname === "/i/bookmarks") { - addTwitterImportButtonForFolders() - await handleBookmarksPageLoad() - } else { - removeAllTwitterUI() - } -} - -/** - * Adds import buttons to bookmark folders - */ -function addTwitterImportButtonForFolders() { - if (window.location.pathname !== "/i/bookmarks") { - return - } - - const targetElements = document.querySelectorAll( - ".css-175oi2r.r-1wtj0ep.r-16x9es5.r-1mmae3n.r-o7ynqc.r-6416eg.r-1ny4l3l.r-1loqt21", - ) - - targetElements.forEach((element) => { - addButtonToElement(element as HTMLElement) - }) -} - -/** - * Adds an import button to a bookmark folder element - */ -function addButtonToElement(element: HTMLElement) { - if (element.querySelector("[data-supermemory-button]")) { - return - } - - loadSpaceGroteskFonts() - - const button = createSaveTweetElement(async () => { - const url = element.getAttribute("href") - const bookmarkCollectionId = url?.split("/").pop() - if (bookmarkCollectionId) { - await showFolderProjectSelectionModal(bookmarkCollectionId) - } - }) - - button.setAttribute("data-supermemory-button", "true") - - element.appendChild(button) - element.style.flexDirection = "row" - element.style.alignItems = "center" - element.style.justifyContent = "center" - element.style.gap = "10px" - element.style.padding = "10px" -} - -/** - * Shows the project selection modal for folder imports - */ -async function showFolderProjectSelectionModal(bookmarkCollectionId: string) { - await loadSpaceGroteskFonts() - - const modal = createProjectSelectionModal( - [], - async (selectedProject) => { - modal.remove() - - try { - await browser.runtime.sendMessage({ - type: MESSAGE_TYPES.BATCH_IMPORT_ALL, - isFolderImport: true, - bookmarkCollectionId: bookmarkCollectionId, - selectedProject: selectedProject, - }) - } catch (error) { - console.error("Error importing bookmarks:", error) - } - }, - () => { - modal.remove() - }, - ) - - document.body.appendChild(modal) - - try { - const response = await browser.runtime.sendMessage({ - action: MESSAGE_TYPES.FETCH_PROJECTS, - }) - - if (response.success && response.data) { - const projects = response.data - updateModalWithProjects(modal, projects) - } else { - console.error("Failed to fetch projects:", response.error) - updateModalWithProjects(modal, []) - } - } catch (error) { - console.error("Error fetching projects:", error) - updateModalWithProjects(modal, []) - } -} - -/** - * Updates the modal with fetched projects - */ -function updateModalWithProjects( - modal: HTMLElement, - projects: Array<{ id: string; name: string; containerTag: string }>, -) { - const select = modal.querySelector("#project-select") as HTMLSelectElement - if (!select) return - - while (select.children.length > 1) { - select.removeChild(select.children[1]) - } - - if (projects.length === 0) { - const noProjectsOption = document.createElement("option") - noProjectsOption.value = "" - noProjectsOption.textContent = "No projects available" - noProjectsOption.disabled = true - select.appendChild(noProjectsOption) - - const importButton = modal.querySelector( - "button:last-child", - ) as HTMLButtonElement - if (importButton) { - importButton.disabled = true - importButton.style.cssText = ` - padding: 10px 16px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - background: rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.3); - font-size: 14px; - font-weight: 500; - cursor: not-allowed; - transition: all 0.2s ease; - ` - } - } else { - projects.forEach((project) => { - const option = document.createElement("option") - option.value = project.id - option.textContent = project.name - option.dataset.containerTag = project.containerTag - select.appendChild(option) - }) - } -} diff --git a/apps/browser-extension/entrypoints/popup/App.css b/apps/browser-extension/entrypoints/popup/App.css deleted file mode 100644 index f91be533..00000000 --- a/apps/browser-extension/entrypoints/popup/App.css +++ /dev/null @@ -1,42 +0,0 @@ -@import "tailwindcss"; - -/* Custom Font Definitions */ -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 300; - font-display: swap; - src: url("/fonts/SpaceGrotesk-Light.ttf") format("truetype"); -} - -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url("/fonts/SpaceGrotesk-Regular.ttf") format("truetype"); -} - -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url("/fonts/SpaceGrotesk-Medium.ttf") format("truetype"); -} - -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url("/fonts/SpaceGrotesk-SemiBold.ttf") format("truetype"); -} - -@font-face { - font-family: "Space Grotesk"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url("/fonts/SpaceGrotesk-Bold.ttf") format("truetype"); -} diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx deleted file mode 100644 index ace10a03..00000000 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ /dev/null @@ -1,1364 +0,0 @@ -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 ( -
- - - {isVisible && ( -
- {content} -
-
- )} -
- ) -} - -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) - 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 ( -
- - -
-
- - - Loading... -
-
-
- ) - } - - return ( -
- -
- {userSignedIn ? ( -
- {/* Tab Navigation */} -
- - - -
- - {/* Tab Content */} - {activeTab === "save" ? ( -
- {/* Current Page Info */} -
-
-

- {currentTitle || "Current Page"} -

-

- {currentUrl} -

-
-
- - {/* Space Selection */} -
-
- - Save to Space - - {showProjectSelector && ( - - )} -
- - {showProjectSelector ? ( -
- {loadingProjects ? ( -
- Loading spaces... -
- ) : ( - projects.map((project) => ( - - )) - )} -
- ) : ( - - )} -
- - {/* Save Button at Bottom */} -
- - {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}
-												
-
- -
-
- -
-
- - 2 - - Paste results below -
-