diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 97466f35..566f0a36 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useCustomer } from "autumn-js/react" import { cn } from "@lib/utils" -import { dmSans125ClassName } from "@/lib/fonts" +import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import { hasActivePlan } from "@lib/queries" import { $fetch } from "@lib/api" import { authClient } from "@lib/auth" @@ -23,6 +23,7 @@ import { ArrowRight, BookOpen, Check, + Info, Loader, Plus, Search, @@ -338,7 +339,7 @@ const SECTIONS: Array<{ className="size-6 rounded object-contain" /> ), - docsUrl: "https://docs.supermemory.ai/supermemory-mcp/introduction", + docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction", })), }, { @@ -381,6 +382,7 @@ const SECTIONS: Array<{ simpleTitle: "Your Docs, Sheets and Slides, searchable", icon: , pro: true, + docsUrl: "https://supermemory.ai/docs/connectors/google-drive", }, { kind: "connector", @@ -391,6 +393,7 @@ const SECTIONS: Array<{ simpleTitle: "All your Notion pages, in supermemory", icon: , pro: true, + docsUrl: "https://supermemory.ai/docs/connectors/notion", }, { kind: "connector", @@ -401,6 +404,7 @@ const SECTIONS: Array<{ simpleTitle: "Your OneDrive files, ready to recall", icon: , pro: true, + docsUrl: "https://supermemory.ai/docs/connectors/onedrive", }, ], }, @@ -531,22 +535,369 @@ function IconBox({ children }: { children: ReactNode }) { ) } -function DocsLink({ href }: { href: string }) { +type InfoUseCase = { + title: string + description: string +} + +type InfoModalCloseReason = Parameters< + typeof analytics.integrationInfoModalClosed +>[0]["close_reason"] + +const MCP_INFO_USE_CASES: InfoUseCase[] = [ + { + title: "Persistent assistant memory", + description: + "Store useful context during conversations and recall it later from this MCP client.", + }, + { + title: "Shared context across tools", + description: + "Use the same Supermemory account across MCP-compatible clients so memory follows the user between sessions.", + }, + { + title: "Profiles and project context", + description: + "Bring user profiles and project-scoped memories into supported AI clients when they need context.", + }, +] + +const ITEM_INFO_USE_CASES: Record = { + "plugin-claude_code": [ + { + title: "Session context injection", + description: + "Fetch relevant project memories, user preferences, and past interactions when Claude Code starts a session.", + }, + { + title: "Automatic coding capture", + description: + "Save useful tool activity like edits, new files, shell commands, and spawned tasks for future sessions.", + }, + ], + "plugin-codex": [ + { + title: "Recall before each prompt", + description: + "Inject relevant memories and profile context into Codex before each prompt.", + }, + { + title: "Capture after sessions", + description: + "Store conversation transcripts after a session, scoped to the current project and user.", + }, + { + title: "Explicit memory skills", + description: + "Use supermemory-search, supermemory-save, and supermemory-forget when memory needs direct control.", + }, + ], + "plugin-opencode": [ + { + title: "Project memory in OpenCode", + description: + "Inject preferences, project knowledge, and past interactions at the start of OpenCode sessions.", + }, + { + title: "Smart session capture", + description: + "Save memories from explicit phrases like remember or save this, and summarize long sessions during compaction.", + }, + ], + "plugin-openclaw": [ + { + title: "Memory across messaging channels", + description: + "Give OpenClaw memory across WhatsApp, Telegram, Discord, Slack, iMessage, and other channels.", + }, + { + title: "Auto-recall and auto-capture", + description: + "Inject relevant memories before AI turns and store conversation exchanges after turns.", + }, + { + title: "Direct memory tools", + description: + "Let the AI store, search, forget, and inspect profile memories during conversations.", + }, + ], + "plugin-hermes": [ + { + title: "Semantic memory for Hermes", + description: + "Add long-term memory, profile recall, search, and session-aware ingest to Hermes.", + }, + { + title: "Turn and session memory", + description: + "Prefetch relevant context before turns, capture completed turns, and ingest full sessions for richer graph updates.", + }, + { + title: "Organized containers", + description: + "Use profile-scoped memory and optional multi-container tags for work, personal, or project-specific context.", + }, + ], + "google-drive": [ + { + title: "Scoped Drive sync", + description: + "Sync selected Google Docs, Sheets, Slides, and PDFs after OAuth and the hosted file picker.", + }, + { + title: "Fresh knowledge base", + description: + "Keep selected Drive files updated in Supermemory, with scheduled and manual import support.", + }, + ], + notion: [ + { + title: "Workspace knowledge sync", + description: + "Sync Notion pages, databases, and blocks into Supermemory from connected workspaces.", + }, + { + title: "Rich Notion context", + description: + "Preserve rich formatting and database properties so Notion content remains useful for retrieval.", + }, + ], + onedrive: [ + { + title: "Microsoft 365 documents", + description: + "Sync Word documents, Excel spreadsheets, and PowerPoint presentations from OneDrive.", + }, + { + title: "Personal and business accounts", + description: + "Connect personal or business OneDrive accounts and keep Office files updated through sync.", + }, + ], + chrome: [ + { + title: "Save from the browser", + description: + "Capture webpages into Supermemory while browsing instead of manually copying content.", + }, + { + title: "Bring bookmarks into memory", + description: + "Import saved browser context so it can be searched and reused later.", + }, + ], + shortcuts: [ + { + title: "Quick mobile capture", + description: + "Add memories from iPhone, iPad, or Mac through Apple Shortcuts.", + }, + { + title: "Save without opening the app", + description: + "Send useful snippets and links into Supermemory from native Apple workflows.", + }, + ], + raycast: [ + { + title: "Fast desktop capture", + description: + "Add memories from Raycast on Mac without leaving the launcher.", + }, + { + title: "Search from Raycast", + description: + "Look up Supermemory content directly from your desktop command bar.", + }, + ], + "x-bookmarks": [ + { + title: "Import saved X posts", + description: + "Turn X/Twitter bookmarks into searchable Supermemory memories.", + }, + { + title: "Reuse social research", + description: + "Bring bookmarked threads, references, and ideas into the same memory layer as your other tools.", + }, + ], +} + +function getInfoUseCases(id: string): InfoUseCase[] { + return ITEM_INFO_USE_CASES[id] ?? MCP_INFO_USE_CASES +} + +function ItemInfoButton({ + name, + onClick, +}: { + name: string + onClick: () => void +}) { return ( - e.stopPropagation()} + + ) +} + +function ItemInfoDialog({ + actionSlot, + docsUrl, + icon, + id, + kind, + name, + onOpenChange, + open, +}: { + actionSlot: ReactNode + docsUrl?: string + icon: ReactNode + id: string + kind: ItemKind + name: string + onOpenChange: (open: boolean) => void + open: boolean +}) { + const useCases = getInfoUseCases(id) + const closeWithReason = (closeReason: InfoModalCloseReason) => { + analytics.integrationInfoModalClosed({ + kind, + id, + name, + close_reason: closeReason, + }) + onOpenChange(false) + } + return ( + { + if (nextOpen) { + onOpenChange(true) + return + } + closeWithReason("dismiss") + }} + > + e.stopPropagation()} + style={{ + boxShadow: + "0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset", + }} + className={cn( + dmSans125ClassName(), + "flex max-h-[88dvh] flex-col gap-3 overflow-hidden border border-white/[0.12] bg-[#1B1F24] p-0 px-3 pt-3 pb-4 text-[#FAFAFA] rounded-2xl md:px-4 sm:max-w-[560px] sm:rounded-[22px]", + )} + > + {name} use cases and docs +
+ {icon} +
+

+ {name} +

+

+ Use cases that apply to this Supermemory connection. +

+
+
+ {docsUrl && ( + + Docs + + )} + +
+
+
+
+
+ {useCases.map((useCase, index) => ( +
+
+ + {index + 1} + + {index < useCases.length - 1 && ( + + )} +
+
+

+ {useCase.title} +

+

+ {useCase.description} +

+
+
+ ))} +
+
+
+
+ + {/* biome-ignore lint/a11y/noStaticElementInteractions: closes the info dialog after the nested action button runs. */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handling stays on the nested real button. */} +
closeWithReason("action")}>{actionSlot}
+
+
+
) } @@ -629,43 +980,61 @@ function ConnectionsCountPill({ count }: { count: number }) { } function ItemCard({ + actionSlot, icon, + id, + kind, name, tagline, pro, isNew, docsUrl, leftIndicator, - rightSlot, + statusSlot, }: { + actionSlot: ReactNode icon: ReactNode + id: string + kind: ItemKind name: string tagline: string pro?: boolean isNew?: boolean docsUrl?: string leftIndicator?: ReactNode - rightSlot: ReactNode + statusSlot?: ReactNode }) { + const [infoOpen, setInfoOpen] = useState(false) return ( + // biome-ignore lint/a11y/useSemanticElements: the card contains nested action buttons, so it cannot be a native button.
setInfoOpen(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + setInfoOpen(true) + } + }} className={cn( - "flex h-full flex-col gap-4 rounded-[12px] bg-[#14161A] p-4 transition-colors hover:bg-[#16181D]", + "group relative flex h-full cursor-pointer flex-col gap-4 rounded-[12px] bg-[#14161A] p-4 transition-colors hover:bg-[#16181D] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA]/45", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", )} > + setInfoOpen(true)} /> +
{icon} - {docsUrl && ( - // biome-ignore lint/a11y/noStaticElementInteractions: wrapper to stop event propagation -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - -
- )}
@@ -691,7 +1060,24 @@ function ItemCard({ {tagline}

-
{rightSlot}
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the status action. */} +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + {statusSlot} +
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the primary action. */} +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + {actionSlot} +
+
) @@ -1413,7 +1799,7 @@ export function IntegrationsView() { className="object-contain" /> ), - docsUrl: "https://docs.supermemory.ai/supermemory-mcp/introduction", + docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction", ctaLabel: "Connect", onCta: () => { void setMcpClient(null) @@ -1444,7 +1830,7 @@ export function IntegrationsView() { className="object-contain" /> ), - docsUrl: "https://docs.supermemory.ai/integrations/claude-code", + docsUrl: "https://supermemory.ai/docs/integrations/claude-code", ctaLabel: claudeCodeConnected ? "Active" : claudeCodeNeedsPro @@ -1502,46 +1888,35 @@ export function IntegrationsView() { switch (item.kind) { case "plugin": { const activeKey = activePluginById.get(item.pluginId) - const activeCount = activeCountByPlugin.get(item.pluginId) ?? 0 const needsProUpgrade = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId) if (activeKey) { const busy = connectingPlugin === item.pluginId return ( -
- { - trackCard(item) - setConnectedPluginId(item.pluginId) - }} - /> - -
+ ) } if (setupPluginIds.has(item.pluginId)) { @@ -1585,24 +1960,21 @@ export function IntegrationsView() { const needsProUpgrade = !isAutumnLoading && !hasProProduct if (count > 0) { return ( -
- - -
+ ) } if (needsProUpgrade) { @@ -1692,17 +2064,46 @@ export function IntegrationsView() { } } + const renderStatus = (item: Item): ReactNode => { + switch (item.kind) { + case "plugin": { + const activeKey = activePluginById.get(item.pluginId) + if (!activeKey) return null + return ( + { + trackCard(item) + setConnectedPluginId(item.pluginId) + }} + /> + ) + } + case "connector": { + const count = connectionsByProvider[item.provider].length + if (count <= 0) return null + return + } + default: + return null + } + } + const renderItemCard = (item: Item) => ( ) @@ -2194,7 +2595,7 @@ export function IntegrationsView() {
void className?: string showIdentity?: boolean - onClose?: () => void }) { const { user, org } = useAuth() const router = useRouter() @@ -159,8 +157,7 @@ export function SettingsContent({ } const handleIntegrations = () => { - router.push("/?view=integrations") - onClose?.() + void router.push("/?view=integrations") } const handleDeleteAccount = async () => { diff --git a/apps/web/components/settings/settings-modal.tsx b/apps/web/components/settings/settings-modal.tsx index ee4a5784..89c52a3f 100644 --- a/apps/web/components/settings/settings-modal.tsx +++ b/apps/web/components/settings/settings-modal.tsx @@ -123,7 +123,6 @@ export function SettingsModalProvider({ children }: { children: ReactNode }) { activeTab={tab} onTabChange={handleTabChange} showIdentity={false} - onClose={() => setParam(null)} className="flex-1 min-h-0 w-full overflow-y-auto md:overflow-hidden px-5 md:px-4 pt-4 pb-6" /> diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index f9f1dfee..ea2742aa 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -50,6 +50,12 @@ export const analytics = { // integrations surface (main Nova page) integrationCardClicked: (props: { kind: string; id: string; name: string }) => safeCapture("integration_card_clicked", props), + integrationInfoModalClosed: (props: { + kind: string + id: string + name: string + close_reason: "dismiss" | "close_button" | "im_good" | "action" + }) => safeCapture("integration_info_modal_closed", props), nextAppResearchCtaDismissed: () => safeCapture("next_app_research_cta_dismissed"), diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index 0c69d57b..e1a4ffde 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -34,7 +34,7 @@ export const PLUGIN_CATALOG: Record = { name: "Claude Code", tagline: "Remembers your conventions, decisions, and project context", icon: "/images/plugins/claude-code.svg", - docsUrl: "https://docs.supermemory.ai/integrations/claude-code", + docsUrl: "https://supermemory.ai/docs/integrations/claude-code", installSteps: [ { title: "Save your API key", @@ -56,7 +56,7 @@ export const PLUGIN_CATALOG: Record = { name: "Codex", tagline: "Persistent memory for the Codex CLI — free on every plan", icon: "/images/plugins/codex.png", - docsUrl: "https://docs.supermemory.ai/integrations/codex", + docsUrl: "https://supermemory.ai/docs/integrations/codex", githubUrl: "https://github.com/supermemoryai/codex-supermemory", installSteps: [ { @@ -79,7 +79,7 @@ export const PLUGIN_CATALOG: Record = { name: "OpenCode", tagline: "Long-term memory for your OpenCode sessions", icon: "/images/plugins/opencode.svg", - docsUrl: "https://docs.supermemory.ai/integrations/opencode", + docsUrl: "https://supermemory.ai/docs/integrations/opencode", githubUrl: "https://github.com/supermemoryai/opencode-supermemory", usesOAuth: true, installSteps: [ @@ -108,7 +108,7 @@ export const PLUGIN_CATALOG: Record = { name: "OpenClaw", tagline: "Cross-platform memory across Telegram, Discord, Slack", icon: "/images/plugins/openclaw.svg", - docsUrl: "https://docs.supermemory.ai/integrations/openclaw", + docsUrl: "https://supermemory.ai/docs/integrations/openclaw", installSteps: [ { title: "Install the plugin", @@ -128,7 +128,7 @@ export const PLUGIN_CATALOG: Record = { name: "Hermes", tagline: "Persistent memory for the Hermes agent — free on every plan", icon: "/images/plugins/hermes.svg", - docsUrl: "https://docs.supermemory.ai/integrations/hermes", + docsUrl: "https://supermemory.ai/docs/integrations/hermes", installSteps: [ { title: "Run Hermes memory setup",