feat: enhanced the integration page for more conversion (#899)

This commit is contained in:
MaheshtheDev 2026-05-04 17:41:52 +00:00
parent a605909191
commit 805cf3cb93
13 changed files with 580 additions and 625 deletions

View file

@ -172,7 +172,7 @@ function buildSpotlightCatalog(
analytics.onboardingIntegrationClicked({ integration })
const openPluginsPanel = () => {
void router.push("/?view=integrations&plugins=true")
void router.push("/?view=plugins")
}
return {
@ -229,7 +229,7 @@ function buildSpotlightCatalog(
pro: true,
onOpen: () => {
track("connections")
void router.push("/?view=integrations&integration=connections")
void router.push("/?view=connections")
},
},
],
@ -272,7 +272,7 @@ function buildSpotlightCatalog(
icon: <img src="/onboarding/x.png" alt="X" className="size-10" />,
onOpen: () => {
track("import_x")
void router.push("/?view=integrations&integration=import")
void router.push("/?view=import")
},
},
],

View file

@ -10,12 +10,19 @@ import {
} from "react"
import { AnimatePresence, motion } from "motion/react"
import { useQueryState } from "nuqs"
import { Header } from "@/components/header"
import { Header, PublicHeader } from "@/components/header"
import { ChatSidebar, HomeChatComposer } from "@/components/chat"
import { DashboardView } from "@/components/dashboard-view"
import { MemoriesGrid } from "@/components/memories-grid"
import { GraphLayoutView } from "@/components/graph-layout-view"
import { IntegrationsView } from "@/components/integrations-view"
import { IntegrationsView, DetailWrapper } from "@/components/integrations-view"
import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
import { ChromeDetail } from "@/components/integrations/chrome-detail"
import { ShortcutsDetail } from "@/components/integrations/shortcuts-detail"
import { RaycastDetail } from "@/components/integrations/raycast-detail"
import { ConnectionsDetail } from "@/components/integrations/connections-detail"
import { PluginsDetail } from "@/components/integrations/plugins-detail"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import { AddDocumentModal } from "@/components/add-document"
import { DocumentModal } from "@/components/document-modal"
@ -50,8 +57,6 @@ import {
qParam,
docParam,
fullscreenParam,
integrationParam,
pluginsPanelParam,
type IntegrationParamValue,
} from "@/lib/search-params"
import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label"
@ -148,20 +153,6 @@ export default function NewPage() {
"fullscreen",
fullscreenParam,
)
const [integrationFromUrl, setIntegration] = useQueryState(
"integration",
integrationParam,
)
const [pluginsPanelFromUrl, setPluginsPanel] = useQueryState(
"plugins",
pluginsPanelParam,
)
useEffect(() => {
if (integrationFromUrl || pluginsPanelFromUrl === true) {
void setViewMode("integrations")
}
}, [integrationFromUrl, pluginsPanelFromUrl, setViewMode])
// Ephemeral local state (not worth URL-encoding)
const [fullscreenInitialContent, setFullscreenInitialContent] = useState("")
@ -494,20 +485,14 @@ export default function NewPage() {
const handleOpenIntegrations = useCallback(
(integration?: IntegrationParamValue) => {
setViewMode("integrations")
if (integration) {
setIntegration(integration)
} else {
setIntegration(null)
}
void setViewMode(integration ?? "integrations")
},
[setViewMode, setIntegration],
[setViewMode],
)
const handleOpenPlugins = useCallback(() => {
void setViewMode("integrations")
void setPluginsPanel(true)
}, [setViewMode, setPluginsPanel])
void setViewMode("plugins")
}, [setViewMode])
const handleAddMemory = useCallback(
(tab: "note" | "link") => {
@ -564,16 +549,20 @@ export default function NewPage() {
/>
</>
)}
<Header
onAddMemory={() => {
analytics.addDocumentModalOpened()
setAddDoc("note")
}}
onOpenSearch={() => {
analytics.searchOpened({ source: "header" })
setIsSearchOpen(true)
}}
/>
{!session && viewMode === "mcp" ? (
<PublicHeader />
) : (
<Header
onAddMemory={() => {
analytics.addDocumentModalOpened()
setAddDoc("note")
}}
onOpenSearch={() => {
analytics.searchOpened({ source: "header" })
setIsSearchOpen(true)
}}
/>
)}
<AnimatePresence mode="wait">
<motion.main
key={`main-container-${viewMode}`}
@ -611,6 +600,44 @@ export default function NewPage() {
<div className="min-h-0 min-w-0 flex-1 p-4 pt-2! md:p-6 md:pr-0">
<IntegrationsView />
</div>
) : viewMode === "mcp" ? (
<MCPDetailView
onBack={() => void setViewMode("integrations")}
/>
) : viewMode === "plugins" ? (
<DetailWrapper
onBack={() => void setViewMode("integrations")}
>
<PluginsDetail />
</DetailWrapper>
) : viewMode === "chrome" ? (
<DetailWrapper
onBack={() => void setViewMode("integrations")}
>
<ChromeDetail />
</DetailWrapper>
) : viewMode === "shortcuts" ? (
<DetailWrapper
onBack={() => void setViewMode("integrations")}
>
<ShortcutsDetail />
</DetailWrapper>
) : viewMode === "raycast" ? (
<DetailWrapper
onBack={() => void setViewMode("integrations")}
>
<RaycastDetail />
</DetailWrapper>
) : viewMode === "connections" ? (
<DetailWrapper
onBack={() => void setViewMode("integrations")}
>
<ConnectionsDetail />
</DetailWrapper>
) : viewMode === "import" ? (
<XBookmarksDetailView
onBack={() => void setViewMode("integrations")}
/>
) : viewMode === "graph" && !isMobile ? (
<div className="min-h-0 min-w-0 flex-1">
<GraphLayoutView />

View file

@ -349,7 +349,7 @@ function AuthConnectContent() {
</button>
<a
href="https://app.supermemory.ai/?plugins=true"
href="https://app.supermemory.ai/?view=plugins"
className={dmSans125ClassName(
"text-[12px] text-[#737373] hover:text-[#FAFAFA] transition-colors",
)}

View file

@ -1,15 +1,19 @@
"use client"
import { useEffect } from "react"
import { usePathname, useRouter } from "next/navigation"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useAuth } from "@lib/auth-context"
export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const router = useRouter()
const searchParams = useSearchParams()
const { session, organizations, isRestoring } = useAuth()
const isMcpPublicPage = searchParams.get("view") === "mcp"
useEffect(() => {
if (isMcpPublicPage) return
if (isRestoring) return
if (!session) {
router.replace(
@ -21,7 +25,7 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
if (organizations.length > 0) return
if (pathname.startsWith("/onboarding")) return
router.replace("/onboarding")
}, [session, organizations, isRestoring, pathname, router])
}, [session, organizations, isRestoring, pathname, router, isMcpPublicPage])
return children
}

View file

@ -187,11 +187,37 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
key={mode}
type="button"
role="tab"
aria-selected={viewMode === mode}
aria-selected={
mode === "integrations"
? [
"integrations",
"mcp",
"plugins",
"chrome",
"connections",
"shortcuts",
"raycast",
"import",
].includes(viewMode)
: viewMode === mode
}
onClick={() => void setViewMode(mode)}
className={cn(
"inline-flex h-[calc(100%-1px)] min-h-0 cursor-pointer items-center justify-center gap-1 rounded-full border border-transparent px-2.5 text-xs font-medium whitespace-nowrap transition-colors sm:gap-1.5 sm:px-3 sm:text-sm",
viewMode === mode
(
mode === "integrations"
? [
"integrations",
"mcp",
"plugins",
"chrome",
"connections",
"shortcuts",
"raycast",
"import",
].includes(viewMode)
: viewMode === mode
)
? "border-[#2261CA33] bg-[#00173C] text-white"
: "text-foreground hover:bg-white/5",
dmSansClassName(),
@ -375,3 +401,56 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
</div>
)
}
export function PublicHeader() {
return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-2 p-2.5 md:p-3">
<Link
href="/"
className="flex items-center gap-2 hover:opacity-90 transition-opacity"
>
<Logo className="h-6 md:h-7" />
<div className="hidden sm:flex flex-col items-start">
<p className="text-[10px] leading-tight text-[#6B6B6B]">Your AI</p>
<p className="-mt-0.5 text-base leading-none font-medium text-white/90">
supermemory
</p>
</div>
</Link>
<div className="flex items-center gap-2">
<p
className={cn(
"hidden md:block text-[13px] text-[#4B5563]",
dmSansClassName(),
)}
>
Connect your tools, search everything.
</p>
<Link href="/login">
<button
type="button"
className={cn(
"text-[13px] font-medium text-[#8B8B8B] hover:text-white transition-colors px-3 h-8 cursor-pointer",
dmSansClassName(),
)}
>
Sign in
</button>
</Link>
<Link href="/login/new">
<button
type="button"
className={cn(
"flex items-center gap-1.5 text-[13px] font-medium text-white",
"bg-[#4BA0FA] hover:bg-[#4BA0FA]/90 rounded-full px-4 h-8 transition-colors cursor-pointer",
dmSansClassName(),
)}
>
Get started free
</button>
</Link>
</div>
</div>
)
}

View file

@ -1,17 +1,16 @@
"use client"
import { useState, useEffect } from "react"
import { useQueryState } from "nuqs"
import { useQuery } from "@tanstack/react-query"
import { useCustomer } from "autumn-js/react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { hasActivePlan } from "@lib/queries"
import { $fetch } from "@lib/api"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
import { ChromeDetail } from "@/components/integrations/chrome-detail"
import { ShortcutsDetail } from "@/components/integrations/shortcuts-detail"
import { RaycastDetail } from "@/components/integrations/raycast-detail"
import { ConnectionsDetail } from "@/components/integrations/connections-detail"
import { PluginsDetail } from "@/components/integrations/plugins-detail"
import {
ChromeIcon,
AppleShortcutsIcon,
@ -19,13 +18,14 @@ import {
} from "@/components/integration-icons"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { ArrowLeft, Sun } from "lucide-react"
import {
integrationParam,
pluginsPanelParam,
type IntegrationParamValue,
} from "@/lib/search-params"
import { CHROME_EXTENSION_URL } from "@repo/lib/constants"
import { analytics } from "@/lib/analytics"
import Image from "next/image"
import { IntegrationGridCard } from "@/components/integrations/integration-grid-card"
import { useViewMode } from "@/lib/view-mode-context"
import type { ViewParamValue } from "@/lib/search-params"
type Connection = z.infer<typeof ConnectionResponseSchema>
type CardId =
| "mcp"
@ -42,6 +42,7 @@ interface IntegrationCardDef {
description: string
icon: React.ReactNode
pro?: boolean
externalHref?: string
}
const cards: IntegrationCardDef[] = [
@ -109,6 +110,7 @@ const cards: IntegrationCardDef[] = [
title: "Chrome Extension",
description: "Save any webpage, import bookmarks, sync ChatGPT memories",
icon: <ChromeIcon className="size-14" />,
externalHref: CHROME_EXTENSION_URL,
},
{
id: "shortcuts",
@ -130,7 +132,7 @@ const cards: IntegrationCardDef[] = [
},
]
function DetailWrapper({
export function DetailWrapper({
onBack,
children,
}: {
@ -154,74 +156,91 @@ function DetailWrapper({
)
}
const INTEGRATION_TO_CARD: Record<IntegrationParamValue, CardId> = {
import: "import",
chrome: "chrome",
connections: "connections",
}
const CARD_GROUPS: Array<{ label: string; ids: CardId[] }> = [
{ label: "AI tools", ids: ["plugins", "mcp"] },
{
label: "Apps & extensions",
ids: ["connections", "chrome", "shortcuts", "raycast", "import"],
},
]
export function IntegrationsView() {
const [integration, setIntegration] = useQueryState(
"integration",
integrationParam,
)
const [pluginsPanel, setPluginsPanel] = useQueryState(
"plugins",
pluginsPanelParam,
)
const [selectedCard, setSelectedCard] = useState<CardId | null>(null)
const { setViewMode } = useViewMode()
const { org } = useAuth()
const autumn = useCustomer()
const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
useEffect(() => {
if (pluginsPanel === true) {
setSelectedCard("plugins")
return
const { data: connections = [] } = useQuery({
queryKey: ["connections"],
queryFn: async () => {
const response = await $fetch("@post/connections/list", {
body: { containerTags: [] },
})
if (response.error)
throw new Error(response.error?.message || "Failed to load connections")
return response.data as Connection[]
},
staleTime: 30 * 1000,
enabled: hasProProduct,
})
const { data: facetsData } = useQuery({
queryKey: ["document-facets", []],
queryFn: async () => {
const response = await $fetch("@post/documents/documents/facets", {
body: { containerTags: [] },
disableValidation: true,
})
if (response.error)
throw new Error(response.error?.message || "Failed to fetch facets")
return response.data as {
facets: Array<{ category: string; count: number }>
total: number
}
},
staleTime: 5 * 60 * 1000,
})
type ApiKey = { metadata: Record<string, unknown> | null }
const { data: apiKeys = [] } = useQuery({
queryKey: ["api-keys", org?.id],
queryFn: async () => {
if (!org?.id) return []
const data = (await authClient.apiKey.list({
fetchOptions: { query: { metadata: { organizationId: org.id } } },
})) as unknown as ApiKey[]
return data.filter((key) => key.metadata?.organizationId === org.id)
},
enabled: !!org?.id,
staleTime: 30 * 1000,
})
const connectedPluginCount = apiKeys.filter(
(key) => key.metadata?.sm_type === "plugin_auth",
).length
const tweetCount =
facetsData?.facets.find((f) => f.category === "tweet")?.count ?? 0
const getStatusLabel = (
id: CardId,
): { label: string; variant: "connected" | "neutral" } | undefined => {
if (id === "connections" && hasProProduct) {
return connections.length > 0
? { label: `${connections.length} connected`, variant: "connected" }
: { label: "Not connected", variant: "neutral" }
}
if (integration && INTEGRATION_TO_CARD[integration]) {
setSelectedCard(INTEGRATION_TO_CARD[integration])
if (id === "import") {
return tweetCount > 0
? { label: `${tweetCount} tweets imported`, variant: "connected" }
: undefined
}
}, [integration, pluginsPanel])
const handleBack = () => {
setSelectedCard(null)
setIntegration(null)
void setPluginsPanel(null)
}
switch (selectedCard) {
case "mcp":
return <MCPDetailView onBack={handleBack} />
case "import":
return <XBookmarksDetailView onBack={handleBack} />
case "chrome":
return (
<DetailWrapper onBack={handleBack}>
<ChromeDetail />
</DetailWrapper>
)
case "shortcuts":
return (
<DetailWrapper onBack={handleBack}>
<ShortcutsDetail />
</DetailWrapper>
)
case "raycast":
return (
<DetailWrapper onBack={handleBack}>
<RaycastDetail />
</DetailWrapper>
)
case "connections":
return (
<DetailWrapper onBack={handleBack}>
<ConnectionsDetail />
</DetailWrapper>
)
case "plugins":
return (
<DetailWrapper onBack={handleBack}>
<PluginsDetail />
</DetailWrapper>
)
if (id === "plugins") {
return connectedPluginCount > 0
? { label: `${connectedPluginCount} connected`, variant: "connected" }
: undefined
}
return undefined
}
return (
@ -237,17 +256,51 @@ export function IntegrationsView() {
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{cards.map((card) => (
<IntegrationGridCard
key={card.id}
title={card.title}
description={card.description}
icon={card.icon}
pro={card.pro}
onClick={() => setSelectedCard(card.id)}
/>
))}
<div className="space-y-6">
{CARD_GROUPS.map((group) => {
const groupCards = cards.filter((c) => group.ids.includes(c.id))
return (
<div key={group.label}>
<div className="flex items-center gap-3 mb-3">
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-[#3A4455] shrink-0">
{group.label}
</span>
<div className="flex-1 h-px bg-[#0F1621]" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{groupCards.map((card) => {
const status = getStatusLabel(card.id)
return (
<IntegrationGridCard
key={card.id}
title={card.title}
description={card.description}
icon={card.icon}
pro={card.pro}
statusLabel={status?.label}
statusVariant={status?.variant}
isExternal={!!card.externalHref}
onClick={() => {
if (card.externalHref) {
window.open(
card.externalHref,
"_blank",
"noopener,noreferrer",
)
analytics.onboardingChromeExtensionClicked({
source: "integrations",
})
} else {
void setViewMode(card.id as ViewParamValue)
}
}}
/>
)
})}
</div>
</div>
)
})}
</div>
</div>
</div>

View file

@ -7,7 +7,7 @@ import { hasActivePlan } from "@lib/queries"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Check, Plus, Trash2, Zap } from "lucide-react"
import { Check, Clock, FolderOpen, Plus, Trash2, Zap } from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { useQueryState } from "nuqs"
@ -68,10 +68,10 @@ function ConnectionRow({
}
const getProjectName = (tag: string): string => {
if (tag === DEFAULT_PROJECT_ID) return "Default Project"
if (tag === DEFAULT_PROJECT_ID) return "Default"
return (
projects.find((p) => p.containerTag === tag)?.name ??
tag.replace(/^sm_project_/, "")
tag.replace(/^sm_project_/, "").replace(/_/g, " ")
)
}
@ -136,31 +136,48 @@ function ConnectionRow({
<Trash2 className="size-[22px]" />
</button>
</div>
<div className="flex items-center gap-2 flex-wrap">
{projectName && (
<>
<div className="flex items-center gap-3 pt-2.5 border-t border-[rgba(82,89,102,0.12)]">
<div className="flex items-center gap-2 flex-1 flex-wrap">
{projectName && (
<div className="flex items-center gap-1">
<FolderOpen className="size-3 text-[#4B5563]" />
<span
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373] capitalize",
)}
>
{projectName}
</span>
</div>
)}
<div className="flex items-center gap-1">
<Clock className="size-3 text-[#4B5563]" />
<span
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
"text-[12px] text-[#737373]",
)}
>
Project: {projectName}
{formatRelativeTime(connection.createdAt)}
</span>
<div className="size-[3px] rounded-full bg-[#737373]" />
</>
)}
<span
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
Added: {formatRelativeTime(connection.createdAt)}
</span>
<div className="size-[3px] rounded-full bg-[#737373]" />
<span
className={cn(dmSans125ClassName(), "text-[14px] text-[#737373]")}
>
{documentCount} {config.documentLabel} connected
</span>
</div>
</div>
<div className="flex items-baseline gap-1 shrink-0">
<span
className={cn(
dmSans125ClassName(),
"text-[14px] font-semibold text-[#FAFAFA]",
)}
>
{documentCount}
</span>
<span
className={cn(dmSans125ClassName(), "text-[12px] text-[#737373]")}
>
{config.documentLabel}
</span>
</div>
</div>
</div>
</div>

View file

@ -3,18 +3,25 @@
import type { ReactNode } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { ExternalLink } from "lucide-react"
export function IntegrationGridCard({
title,
description,
icon,
pro,
statusLabel,
statusVariant = "neutral",
isExternal,
onClick,
}: {
title: string
description: string
icon: ReactNode
pro?: boolean
statusLabel?: string
statusVariant?: "connected" | "neutral"
isExternal?: boolean
onClick: () => void
}) {
return (
@ -35,6 +42,9 @@ export function IntegrationGridCard({
PRO
</span>
) : null}
{isExternal ? (
<ExternalLink className="absolute top-3 left-3 size-3 text-[#3A4455] opacity-0 group-hover:opacity-100 transition-opacity" />
) : null}
<div className="absolute top-2 right-2 opacity-60 group-hover:opacity-100 transition-opacity">
{icon}
</div>
@ -48,6 +58,18 @@ export function IntegrationGridCard({
>
{description}
</p>
{statusLabel ? (
<span
className={cn(
"inline-block mt-2 text-[10px] font-medium px-1.5 py-0.5 rounded-full",
statusVariant === "connected"
? "bg-[#00AC3F]/10 text-[#00AC3F]"
: "bg-[#737373]/10 text-[#737373]",
)}
>
{statusLabel}
</span>
) : null}
</div>
</button>
)

View file

@ -10,12 +10,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
ArrowRight,
BookOpen,
Brain,
Check,
CheckCircle,
Copy,
ExternalLink,
Key,
Loader,
Trash2,
Zap,
@ -30,7 +28,6 @@ import {
DialogTitle,
DialogPortal,
} from "@ui/components/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/components/tabs"
/** Match `FREE_TIER_PLUGIN_IDS` in mono `packages/lib/plugins.ts`. */
function isFreeTierPlugin(pluginId: string): boolean {
@ -52,11 +49,11 @@ const PLUGIN_CATALOG: Record<string, PluginInfo> = {
id: "claude_code",
name: "Claude Code",
description:
"Persistent memory for Claude Code. Remembers your coding context, patterns, and decisions across sessions.",
"Claude Code remembers your conventions, past decisions, and project context across every session — no re-explaining yourself.",
features: [
"Auto-recalls relevant context at session start",
"Captures important observations from tool usage",
"Builds persistent user profile from interactions",
"Picks up where you left off at session start",
"Captures decisions and patterns from tool usage",
"Builds a persistent profile of how you work",
],
icon: "/images/plugins/claude-code.svg",
docsUrl: "https://docs.supermemory.ai/integrations/claude-code",
@ -66,11 +63,11 @@ const PLUGIN_CATALOG: Record<string, PluginInfo> = {
id: "opencode",
name: "OpenCode",
description:
"Memory layer for OpenCode. Enhances your coding assistant with long-term memory capabilities.",
"Gives OpenCode persistent memory — your patterns, preferences, and decisions carry forward automatically, session to session.",
features: [
"Semantic search across previous sessions",
"Auto-capture of coding decisions",
"Context injection before each prompt",
"Context injected before each prompt",
],
icon: "/images/plugins/opencode.svg",
docsUrl: "https://docs.supermemory.ai/integrations/opencode",
@ -79,11 +76,11 @@ const PLUGIN_CATALOG: Record<string, PluginInfo> = {
id: "openclaw",
name: "OpenClaw",
description:
"Multi-platform memory for OpenClaw. Works across Telegram, WhatsApp, Discord, Slack and more.",
"Persists memory across Telegram, WhatsApp, Discord, and Slack. OpenClaw knows who users are and what they talked about before.",
features: [
"Cross-channel memory persistence",
"Cross-channel memory that follows the user",
"Automatic conversation capture",
"User profile building across platforms",
"User profiles built across every platform",
],
icon: "/images/plugins/openclaw.svg",
docsUrl: "https://docs.supermemory.ai/integrations/openclaw",
@ -92,11 +89,12 @@ const PLUGIN_CATALOG: Record<string, PluginInfo> = {
hermes: {
id: "hermes",
name: "Hermes",
description: "Memory layer for Hermes agent",
description:
"Hermes never forgets. Conversations, user profiles, and context persist so every session feels like a continuation, not a cold start.",
features: [
"Semantic search across previous sessions",
"Auto-capture of conversation context",
"Builds persistent user profile from interactions",
"Persistent user profile built over time",
],
icon: "/images/plugins/hermes.svg",
docsUrl: "https://docs.supermemory.ai/integrations/hermes",
@ -113,119 +111,27 @@ interface ConnectedPlugin {
keyStart?: string | null
}
function ProUpgradeBanner({ onUpgrade }: { onUpgrade: () => void }) {
function ProUpgradeNudge({ onUpgrade }: { onUpgrade: () => void }) {
return (
<div
className={cn(
"bg-gradient-to-br from-[#0D121A] to-[#14161A] rounded-[14px] p-6 border border-[#4BA0FA]/20 mb-6",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
)}
>
<div className="flex flex-col gap-5">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#4BA0FA]/10 shrink-0">
<Zap className="size-6 text-[#4BA0FA]" />
</div>
<div className="flex-1">
<h3
className={cn(
dmSans125ClassName(),
"font-semibold text-[18px] text-[#FAFAFA]",
)}
>
Unlock Pro plugins
</h3>
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373] mt-1",
)}
>
Connect Claude Code, OpenCode, OpenClaw, Cursor, and more with a
Pro plan.
</p>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-3">
{[
{
icon: Brain,
title: "Context Retention",
desc: "AI remembers your preferences across sessions",
},
{
icon: Zap,
title: "Instant Recall",
desc: "Past decisions surface automatically when relevant",
},
{
icon: Key,
title: "Secure & Private",
desc: "Your data stays yours with encrypted storage",
},
].map(({ icon: Icon, title, desc }) => (
<div key={title} className="flex items-start gap-2.5">
<Icon className="mt-0.5 size-4 text-[#4BA0FA] shrink-0" />
<div>
<p
className={cn(
dmSans125ClassName(),
"text-[13px] font-medium text-[#FAFAFA]",
)}
>
{title}
</p>
<p
className={cn(
dmSans125ClassName(),
"text-[11px] text-[#737373]",
)}
>
{desc}
</p>
</div>
</div>
))}
</div>
<div className="flex items-center gap-3 flex-wrap">
{Object.values(PLUGIN_CATALOG)
.filter((p) => !isFreeTierPlugin(p.id))
.map((plugin) => (
<div
key={plugin.id}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-[#1E293B] bg-[#080B0F]"
>
<Image
alt={plugin.name}
className="size-5"
height={20}
src={plugin.icon}
width={20}
/>
</div>
))}
<span
className={cn(dmSans125ClassName(), "text-[12px] text-[#737373]")}
>
Claude Code, OpenCode, OpenClaw & more
</span>
</div>
<button
type="button"
onClick={onUpgrade}
className={cn(
"w-full sm:w-auto flex items-center justify-center gap-2",
"bg-[#4BA0FA] hover:bg-[#4BA0FA]/90 text-white",
"rounded-full h-11 px-6 font-medium text-sm transition-colors cursor-pointer",
dmSans125ClassName(),
)}
>
Upgrade to Pro
</button>
<div className="flex items-center justify-between gap-3 bg-[#4BA0FA]/5 border border-[#4BA0FA]/20 rounded-xl px-4 py-3 mb-6">
<div className="flex items-center gap-2">
<Zap className="size-4 text-[#4BA0FA] shrink-0" />
<p className={cn(dmSans125ClassName(), "text-[13px] text-[#8B8B8B]")}>
Unlock Claude Code, OpenCode, OpenClaw and more with{" "}
<span className="text-white font-medium">Pro</span>
</p>
</div>
<button
type="button"
onClick={onUpgrade}
className={cn(
"shrink-0 flex items-center gap-1.5 text-[12px] font-medium text-white",
"bg-[#4BA0FA] hover:bg-[#4BA0FA]/90 rounded-full px-3 h-7 transition-colors cursor-pointer",
dmSans125ClassName(),
)}
>
Upgrade
</button>
</div>
)
}
@ -321,11 +227,20 @@ function PluginCard({
<div
className={cn(
"bg-[#0D121A] rounded-[12px] p-4 flex flex-col gap-3 border",
isConnected ? "border-[#4BA0FA]/30" : "border-[rgba(82,89,102,0.2)]",
isConnected
? "border-[#4BA0FA]/30"
: needsProUpgrade
? "border-[rgba(82,89,102,0.12)]"
: "border-[rgba(82,89,102,0.2)]",
)}
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-[#1E293B] bg-[#080B0F]">
<div
className={cn(
"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-[#1E293B] bg-[#080B0F]",
needsProUpgrade && "opacity-50",
)}
>
<Image
alt={plugin.name}
className="size-6"
@ -339,7 +254,8 @@ function PluginCard({
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[14px] text-[#FAFAFA]",
"font-medium text-[14px]",
needsProUpgrade ? "text-[#737373]" : "text-[#FAFAFA]",
)}
>
{plugin.name}
@ -349,11 +265,17 @@ function PluginCard({
<CheckCircle className="size-2.5" /> Connected
</span>
)}
{needsProUpgrade && (
<span className="text-[10px] font-bold text-[#00171A] bg-[#4BA0FA] px-1.5 py-0.5 rounded-[3px]">
PRO
</span>
)}
</div>
<p
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373] mt-0.5",
"text-[12px] mt-0.5",
needsProUpgrade ? "text-[#4B5563]" : "text-[#737373]",
)}
>
{plugin.description}
@ -361,7 +283,7 @@ function PluginCard({
</div>
</div>
<ul className="space-y-1.5">
<ul className={cn("space-y-1.5", needsProUpgrade && "opacity-40")}>
{plugin.features.map((feature) => (
<li key={feature} className="flex items-start gap-2">
<ArrowRight className="mt-0.5 size-3 shrink-0 text-[#4BA0FA]" />
@ -524,16 +446,6 @@ export function PluginsDetail() {
[connectedPlugins],
)
const freeConnected = useMemo(
() => connectedPlugins.filter((p) => isFreeTierPlugin(p.pluginId)),
[connectedPlugins],
)
const proConnected = useMemo(
() => connectedPlugins.filter((p) => !isFreeTierPlugin(p.pluginId)),
[connectedPlugins],
)
const createPluginKeyMutation = useMutation({
mutationFn: async (pluginId: string) => {
const API_URL =
@ -607,31 +519,11 @@ export function PluginsDetail() {
const isLoading = autumn.isLoading
const availablePlugins = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)
const freePluginIds = useMemo(() => {
const ids = new Set(
availablePlugins.filter(
(id) => PLUGIN_CATALOG[id] && isFreeTierPlugin(id),
),
)
if (PLUGIN_CATALOG.hermes) ids.add("hermes")
return [...ids]
}, [availablePlugins])
const proPluginIds = useMemo(
() =>
availablePlugins.filter(
(id) => PLUGIN_CATALOG[id] && !isFreeTierPlugin(id),
),
[availablePlugins],
)
const allCatalogPluginIds = useMemo(
() => availablePlugins.filter((id) => PLUGIN_CATALOG[id]),
[availablePlugins],
)
const showPaidAllInOne = !isLoading && hasProProduct
return (
<>
<div
@ -640,221 +532,66 @@ export function PluginsDetail() {
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
)}
>
{showPaidAllInOne ? (
<div className="flex flex-col gap-6">
{connectedPlugins.length > 0 && (
<div className="flex flex-col gap-3">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
Connected
</span>
{connectedPlugins.map((plugin) => (
<ConnectedPluginRow
key={plugin.id}
plugin={plugin}
info={PLUGIN_CATALOG[plugin.pluginId]}
onRevoke={handleRevoke}
/>
))}
</div>
)}
{!hasProProduct && !isLoading && (
<ProUpgradeNudge onUpgrade={handleUpgrade} />
)}
<div className="flex flex-col gap-3">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
{connectedPlugins.length > 0
? "Add more plugins"
: "Available plugins"}
</span>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{allCatalogPluginIds.map((pluginId) => {
const plugin = PLUGIN_CATALOG[pluginId]
if (!plugin) return null
const isConnected = connectedPluginIds.includes(pluginId)
const isCurrentlyConnecting = connectingPlugin === pluginId
return (
<PluginCard
key={pluginId}
plugin={plugin}
pluginId={pluginId}
isConnected={isConnected}
isCurrentlyConnecting={isCurrentlyConnecting}
connectingPlugin={connectingPlugin}
needsProUpgrade={false}
onConnect={(id) => createPluginKeyMutation.mutate(id)}
onUpgrade={handleUpgrade}
/>
)
})}
</div>
</div>
</div>
) : (
<Tabs defaultValue="free" className="gap-0">
<TabsList
{connectedPlugins.length > 0 && (
<div className="flex flex-col gap-3 mb-6">
<span
className={cn(
"grid h-auto w-full grid-cols-2 gap-0 rounded-none border-0 border-b border-[#252a33] bg-transparent p-0",
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
<TabsTrigger
value="free"
className={cn(
"relative flex min-h-12 w-full min-w-0 cursor-pointer items-center justify-center rounded-none border-0 border-transparent bg-transparent px-3 py-3 text-[15px] font-medium shadow-none",
"text-[#737373] hover:text-[#FAFAFA] transition-colors",
"data-[state=active]:bg-transparent data-[state=active]:text-[#FAFAFA] data-[state=active]:shadow-none",
"after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-t-[1px] after:bg-[#4BA0FA] after:opacity-0 data-[state=active]:after:opacity-100",
dmSans125ClassName(),
)}
>
Free plugins
</TabsTrigger>
<TabsTrigger
value="pro"
className={cn(
"relative flex min-h-12 w-full min-w-0 cursor-pointer items-center justify-center rounded-none border-0 border-transparent bg-transparent px-3 py-3 text-[15px] font-medium shadow-none",
"text-[#737373] hover:text-[#FAFAFA] transition-colors",
"data-[state=active]:bg-transparent data-[state=active]:text-[#FAFAFA] data-[state=active]:shadow-none",
"after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-t-[1px] after:bg-[#4BA0FA] after:opacity-0 data-[state=active]:after:opacity-100",
dmSans125ClassName(),
)}
>
Pro plugins
</TabsTrigger>
</TabsList>
<TabsContent value="free" className="mt-5">
<p
className={cn(
dmSans125ClassName(),
"text-[13px] text-[#737373] mb-4",
)}
>
Included on every plan connect with no upgrade.
</p>
{freeConnected.length > 0 && (
<div className="flex flex-col gap-3 mb-6">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
Connected
</span>
{freeConnected.map((plugin) => (
<ConnectedPluginRow
key={plugin.id}
plugin={plugin}
info={PLUGIN_CATALOG[plugin.pluginId]}
onRevoke={handleRevoke}
/>
))}
</div>
)}
<div className="flex flex-col gap-3">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
{freeConnected.length > 0 ? "Add or manage" : "Available"}
</span>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{freePluginIds.map((pluginId) => {
const plugin = PLUGIN_CATALOG[pluginId]
if (!plugin) return null
const isConnected = connectedPluginIds.includes(pluginId)
const isCurrentlyConnecting = connectingPlugin === pluginId
return (
<PluginCard
key={pluginId}
plugin={plugin}
pluginId={pluginId}
isConnected={isConnected}
isCurrentlyConnecting={isCurrentlyConnecting}
connectingPlugin={connectingPlugin}
needsProUpgrade={false}
onConnect={(id) => createPluginKeyMutation.mutate(id)}
onUpgrade={handleUpgrade}
/>
)
})}
</div>
</div>
</TabsContent>
<TabsContent value="pro" className="mt-5">
{!hasProProduct && !isLoading && (
<ProUpgradeBanner onUpgrade={handleUpgrade} />
)}
{proConnected.length > 0 && (
<div className="flex flex-col gap-3 mb-6">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
Connected
</span>
{proConnected.map((plugin) => (
<ConnectedPluginRow
key={plugin.id}
plugin={plugin}
info={PLUGIN_CATALOG[plugin.pluginId]}
onRevoke={handleRevoke}
/>
))}
</div>
)}
<div className="flex flex-col gap-3">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
{proConnected.length > 0 ? "Add more" : "Available plugins"}
</span>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{proPluginIds.map((pluginId) => {
const plugin = PLUGIN_CATALOG[pluginId]
if (!plugin) return null
const isConnected = connectedPluginIds.includes(pluginId)
const isCurrentlyConnecting = connectingPlugin === pluginId
const needsProUpgrade = !hasProProduct
return (
<PluginCard
key={pluginId}
plugin={plugin}
pluginId={pluginId}
isConnected={isConnected}
isCurrentlyConnecting={isCurrentlyConnecting}
connectingPlugin={connectingPlugin}
needsProUpgrade={needsProUpgrade}
onConnect={(id) => createPluginKeyMutation.mutate(id)}
onUpgrade={handleUpgrade}
/>
)
})}
</div>
</div>
</TabsContent>
</Tabs>
Connected
</span>
{connectedPlugins.map((plugin) => (
<ConnectedPluginRow
key={plugin.id}
plugin={plugin}
info={PLUGIN_CATALOG[plugin.pluginId]}
onRevoke={handleRevoke}
/>
))}
</div>
)}
<div className="flex flex-col gap-3">
<span
className={cn(
dmSans125ClassName(),
"font-semibold text-[16px] text-[#FAFAFA]",
)}
>
{connectedPlugins.length > 0
? "Add more plugins"
: "Available plugins"}
</span>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{allCatalogPluginIds.map((pluginId) => {
const plugin = PLUGIN_CATALOG[pluginId]
if (!plugin) return null
const isConnected = connectedPluginIds.includes(pluginId)
const isCurrentlyConnecting = connectingPlugin === pluginId
const needsProUpgrade =
!isLoading && !hasProProduct && !isFreeTierPlugin(pluginId)
return (
<PluginCard
key={pluginId}
plugin={plugin}
pluginId={pluginId}
isConnected={isConnected}
isCurrentlyConnecting={isCurrentlyConnecting}
connectingPlugin={connectingPlugin}
needsProUpgrade={needsProUpgrade}
onConnect={(id) => createPluginKeyMutation.mutate(id)}
onUpgrade={handleUpgrade}
/>
)
})}
</div>
</div>
</div>
<Dialog

View file

@ -652,31 +652,33 @@ interface MCPDetailViewProps {
export function MCPDetailView({ onBack }: MCPDetailViewProps) {
return (
<div className="flex h-full flex-col p-6 md:p-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<Button
variant="link"
className="h-auto self-start p-0 text-[#FAFAFA] hover:text-[#BFBFBF] hover:no-underline"
onClick={onBack}
>
Back
</Button>
</div>
<div className="flex flex-1 w-full flex-col p-6 md:p-8">
<div className="mx-auto flex w-full max-w-3xl flex-1 flex-col">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<Button
variant="link"
className="h-auto self-start p-0 text-[#FAFAFA] hover:text-[#BFBFBF] hover:no-underline"
onClick={onBack}
>
Back
</Button>
</div>
<div className="flex flex-1 flex-col items-center justify-start">
<h1
className={cn(
"mb-1 text-2xl font-semibold tracking-[-0.02em] text-[#FAFAFA]",
dmSansClassName(),
)}
>
Connect Supermemory MCP to your AI Tools
</h1>
<p className={cn("mb-4 text-sm text-[#737373]", dmSansClassName())}>
Connect Cursor, Claude, VS Code, and more via MCP.
</p>
<div className="flex flex-1 flex-col items-center justify-start">
<h1
className={cn(
"mb-1 text-2xl font-semibold tracking-[-0.02em] text-[#FAFAFA]",
dmSansClassName(),
)}
>
Connect Supermemory MCP to your AI Tools
</h1>
<p className={cn("mb-4 text-sm text-[#737373]", dmSansClassName())}>
Connect Cursor, Claude, VS Code, and more via MCP.
</p>
<MCPSteps variant="full" />
<MCPSteps variant="full" />
</div>
</div>
</div>
)

View file

@ -34,75 +34,77 @@ export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) {
}
return (
<div className="flex flex-col h-full p-8">
<div className="mb-6">
<Button
variant="link"
className="text-white hover:text-gray-300 p-0 hover:no-underline cursor-pointer"
onClick={onBack}
>
Back
</Button>
</div>
<div className="flex flex-col items-start justify-start flex-1">
<div>
<h1 className="text-white text-[20px] font-medium mb-3 text-start">
Import your X bookmarks via the Chrome Extension
</h1>
<p
className={cn(
"text-[#8B8B8B] text-sm mb-6 text-start max-w-2xl",
dmSansClassName(),
)}
<div className="flex flex-col flex-1 w-full p-8">
<div className="mx-auto w-full max-w-3xl flex flex-col flex-1">
<div className="mb-6">
<Button
variant="link"
className="text-white hover:text-gray-300 p-0 hover:no-underline cursor-pointer"
onClick={onBack}
>
Bring your X bookmarks into Supermemory in just a few clicks.
They'll be automatically embedded so you can easily find what you
need, right when you need it.
</p>
<div className="grid grid-cols-3 gap-4 mb-6 max-w-5xl w-full">
{steps.map((step) => (
<div
key={step.number}
className="flex flex-col items-center text-center bg-[#080B0F] p-3 rounded-[10px]"
>
<div className="rounded-2xl p-6 mb-3 w-full aspect-4/4 flex items-center justify-center relative overflow-hidden">
<Image
src={step.image}
alt={`Step ${step.number}`}
fill
className="object-cover"
unoptimized
/>
</div>
<div className="flex flex-col items-start justify-start">
<div className="mb-2">
<span className="text-white text-sm font-medium">
Step {step.number}
</span>
</div>
<p
className={cn(
"text-[#8B8B8B] text-sm text-start",
dmSansClassName(),
)}
>
{step.title}
</p>
</div>
</div>
))}
</div>
Back
</Button>
</div>
<Button
className="rounded-xl px-4 py-2 text-white h-10 cursor-pointer mx-auto bg-black"
onClick={handleInstall}
>
Install Chrome Extension
</Button>
<div className="flex flex-col items-start justify-start flex-1">
<div>
<h1 className="text-white text-[20px] font-medium mb-3 text-start">
Import your X bookmarks via the Chrome Extension
</h1>
<p
className={cn(
"text-[#8B8B8B] text-sm mb-6 text-start max-w-2xl",
dmSansClassName(),
)}
>
Bring your X bookmarks into Supermemory in just a few clicks.
They'll be automatically embedded so you can easily find what you
need, right when you need it.
</p>
<div className="grid grid-cols-3 gap-4 mb-6 max-w-5xl w-full">
{steps.map((step) => (
<div
key={step.number}
className="flex flex-col items-center text-center bg-[#080B0F] p-3 rounded-[10px]"
>
<div className="rounded-2xl p-6 mb-3 w-full aspect-4/4 flex items-center justify-center relative overflow-hidden">
<Image
src={step.image}
alt={`Step ${step.number}`}
fill
className="object-cover"
unoptimized
/>
</div>
<div className="flex flex-col items-start justify-start">
<div className="mb-2">
<span className="text-white text-sm font-medium">
Step {step.number}
</span>
</div>
<p
className={cn(
"text-[#8B8B8B] text-sm text-start",
dmSansClassName(),
)}
>
{step.title}
</p>
</div>
</div>
))}
</div>
</div>
<Button
className="rounded-xl px-4 py-2 text-white h-10 cursor-pointer mx-auto bg-black"
onClick={handleInstall}
>
Install Chrome Extension
</Button>
</div>
</div>
</div>
)

View file

@ -29,21 +29,28 @@ const viewLiterals = [
"list",
"integrations",
"chat",
] as const
const integrationLiterals = [
"import",
// Integration sub-views — each card is its own view
"mcp",
"plugins",
"chrome",
"connections",
"notion",
"google-drive",
"shortcuts",
"raycast",
"import",
] as const
export type IntegrationParamValue = (typeof integrationLiterals)[number]
export const integrationParam = parseAsStringLiteral(integrationLiterals)
export type ViewParamValue = (typeof viewLiterals)[number]
export const viewParam =
parseAsStringLiteral(viewLiterals).withDefault("dashboard")
export const pluginsPanelParam = parseAsBoolean
// Kept for backwards compat with components that pass integration hints
export type IntegrationParamValue =
| "mcp"
| "plugins"
| "chrome"
| "connections"
| "shortcuts"
| "raycast"
| "import"
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault(
[],
)

View file

@ -18,6 +18,11 @@ export default async function proxy(request: Request) {
return NextResponse.next()
}
// MCP setup page is public — no auth required
if (url.searchParams.get("view") === "mcp") {
return NextResponse.next()
}
if (url.pathname.startsWith("/api/")) {
if (!sessionCookie) {
console.debug("[MIDDLEWARE] API route without session, returning 401")
@ -66,6 +71,6 @@ export default async function proxy(request: Request) {
export const config = {
matcher: [
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|bg-rectangle.png|onboarding|ingest|login|api/emails).*)",
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|bg-rectangle.png|onboarding|ingest|login|api/emails|mcp-supported-tools|mcp-icon.svg).*)",
],
}