diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 597b3f3f..fac8cc40 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -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: X, onOpen: () => { track("import_x") - void router.push("/?view=integrations&integration=import") + void router.push("/?view=import") }, }, ], diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 9f11aa80..7cb43410 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -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() { /> )} -
{ - analytics.addDocumentModalOpened() - setAddDoc("note") - }} - onOpenSearch={() => { - analytics.searchOpened({ source: "header" }) - setIsSearchOpen(true) - }} - /> + {!session && viewMode === "mcp" ? ( + + ) : ( +
{ + analytics.addDocumentModalOpened() + setAddDoc("note") + }} + onOpenSearch={() => { + analytics.searchOpened({ source: "header" }) + setIsSearchOpen(true) + }} + /> + )} + ) : viewMode === "mcp" ? ( + void setViewMode("integrations")} + /> + ) : viewMode === "plugins" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "chrome" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "shortcuts" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "raycast" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "connections" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "import" ? ( + void setViewMode("integrations")} + /> ) : viewMode === "graph" && !isMobile ? (
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 5465c38b..544002e0 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -349,7 +349,7 @@ function AuthConnectContent() { { + 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 } diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index 2e45cac1..9dbf66f7 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -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) {
) } + +export function PublicHeader() { + return ( +
+ + +
+

Your AI

+

+ supermemory +

+
+ + +
+

+ Connect your tools, search everything. +

+ + + + + + +
+
+ ) +} diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 4ccc4560..55e1215a 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -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 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: , + 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 = { - 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(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 | 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 - case "import": - return - case "chrome": - return ( - - - - ) - case "shortcuts": - return ( - - - - ) - case "raycast": - return ( - - - - ) - case "connections": - return ( - - - - ) - case "plugins": - return ( - - - - ) + if (id === "plugins") { + return connectedPluginCount > 0 + ? { label: `${connectedPluginCount} connected`, variant: "connected" } + : undefined + } + return undefined } return ( @@ -237,17 +256,51 @@ export function IntegrationsView() {

-
- {cards.map((card) => ( - setSelectedCard(card.id)} - /> - ))} +
+ {CARD_GROUPS.map((group) => { + const groupCards = cards.filter((c) => group.ids.includes(c.id)) + return ( +
+
+ + {group.label} + +
+
+
+ {groupCards.map((card) => { + const status = getStatusLabel(card.id) + return ( + { + if (card.externalHref) { + window.open( + card.externalHref, + "_blank", + "noopener,noreferrer", + ) + analytics.onboardingChromeExtensionClicked({ + source: "integrations", + }) + } else { + void setViewMode(card.id as ViewParamValue) + } + }} + /> + ) + })} +
+
+ ) + })}
diff --git a/apps/web/components/integrations/connections-detail.tsx b/apps/web/components/integrations/connections-detail.tsx index d3613400..5ae87178 100644 --- a/apps/web/components/integrations/connections-detail.tsx +++ b/apps/web/components/integrations/connections-detail.tsx @@ -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({ -
- {projectName && ( - <> +
+
+ {projectName && ( +
+ + + {projectName} + +
+ )} +
+ - Project: {projectName} + {formatRelativeTime(connection.createdAt)} -
- - )} - - Added: {formatRelativeTime(connection.createdAt)} - -
- - {documentCount} {config.documentLabel} connected - +
+
+
+ + {documentCount} + + + {config.documentLabel} + +
diff --git a/apps/web/components/integrations/integration-grid-card.tsx b/apps/web/components/integrations/integration-grid-card.tsx index cfaf7da0..5bf6ae91 100644 --- a/apps/web/components/integrations/integration-grid-card.tsx +++ b/apps/web/components/integrations/integration-grid-card.tsx @@ -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 ) : null} + {isExternal ? ( + + ) : null}
{icon}
@@ -48,6 +58,18 @@ export function IntegrationGridCard({ > {description}

+ {statusLabel ? ( + + {statusLabel} + + ) : null}
) diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index 0cbd50e5..8f157a69 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -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 = { 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 = { 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 = { 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 = { 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 ( -
-
-
-
- -
-
-

- Unlock Pro plugins -

-

- Connect Claude Code, OpenCode, OpenClaw, Cursor, and more with a - Pro plan. -

-
-
- -
- {[ - { - 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 }) => ( -
- -
-

- {title} -

-

- {desc} -

-
-
- ))} -
- -
- {Object.values(PLUGIN_CATALOG) - .filter((p) => !isFreeTierPlugin(p.id)) - .map((plugin) => ( -
- {plugin.name} -
- ))} - - Claude Code, OpenCode, OpenClaw & more - -
- - +
+
+ +

+ Unlock Claude Code, OpenCode, OpenClaw and more with{" "} + Pro +

+
) } @@ -321,11 +227,20 @@ function PluginCard({
-
+
{plugin.name} {plugin.name} @@ -349,11 +265,17 @@ function PluginCard({ Connected )} + {needsProUpgrade && ( + + PRO + + )}

{plugin.description} @@ -361,7 +283,7 @@ function PluginCard({

-
    +
      {plugin.features.map((feature) => (
    • @@ -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 ( <>
      - {showPaidAllInOne ? ( -
      - {connectedPlugins.length > 0 && ( -
      - - Connected - - {connectedPlugins.map((plugin) => ( - - ))} -
      - )} + {!hasProProduct && !isLoading && ( + + )} -
      - - {connectedPlugins.length > 0 - ? "Add more plugins" - : "Available plugins"} - -
      - {allCatalogPluginIds.map((pluginId) => { - const plugin = PLUGIN_CATALOG[pluginId] - if (!plugin) return null - const isConnected = connectedPluginIds.includes(pluginId) - const isCurrentlyConnecting = connectingPlugin === pluginId - return ( - createPluginKeyMutation.mutate(id)} - onUpgrade={handleUpgrade} - /> - ) - })} -
      -
      -
      - ) : ( - - 0 && ( +
      + - - Free plugins - - - Pro plugins - - - - -

      - Included on every plan — connect with no upgrade. -

      - - {freeConnected.length > 0 && ( -
      - - Connected - - {freeConnected.map((plugin) => ( - - ))} -
      - )} - -
      - - {freeConnected.length > 0 ? "Add or manage" : "Available"} - -
      - {freePluginIds.map((pluginId) => { - const plugin = PLUGIN_CATALOG[pluginId] - if (!plugin) return null - const isConnected = connectedPluginIds.includes(pluginId) - const isCurrentlyConnecting = connectingPlugin === pluginId - return ( - createPluginKeyMutation.mutate(id)} - onUpgrade={handleUpgrade} - /> - ) - })} -
      -
      -
      - - - {!hasProProduct && !isLoading && ( - - )} - - {proConnected.length > 0 && ( -
      - - Connected - - {proConnected.map((plugin) => ( - - ))} -
      - )} - -
      - - {proConnected.length > 0 ? "Add more" : "Available plugins"} - -
      - {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 ( - createPluginKeyMutation.mutate(id)} - onUpgrade={handleUpgrade} - /> - ) - })} -
      -
      -
      - + Connected +
      + {connectedPlugins.map((plugin) => ( + + ))} +
      )} + +
      + + {connectedPlugins.length > 0 + ? "Add more plugins" + : "Available plugins"} + +
      + {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 ( + createPluginKeyMutation.mutate(id)} + onUpgrade={handleUpgrade} + /> + ) + })} +
      +
      -
      - -
      +
      +
      +
      + +
      -
      -

      - Connect Supermemory MCP to your AI Tools -

      -

      - Connect Cursor, Claude, VS Code, and more via MCP. -

      +
      +

      + Connect Supermemory MCP to your AI Tools +

      +

      + Connect Cursor, Claude, VS Code, and more via MCP. +

      - + +
      ) diff --git a/apps/web/components/onboarding/x-bookmarks-detail-view.tsx b/apps/web/components/onboarding/x-bookmarks-detail-view.tsx index 9a6b37c3..7a95b069 100644 --- a/apps/web/components/onboarding/x-bookmarks-detail-view.tsx +++ b/apps/web/components/onboarding/x-bookmarks-detail-view.tsx @@ -34,75 +34,77 @@ export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) { } return ( -
      -
      - -
      - -
      -
      -

      - Import your X bookmarks via the Chrome Extension -

      - -

      +

      +
      +
      - +
      +
      +

      + Import your X bookmarks via the Chrome Extension +

      + +

      + 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. +

      + +
      + {steps.map((step) => ( +
      +
      + {`Step +
      +
      +
      + + Step {step.number} + +
      +

      + {step.title} +

      +
      +
      + ))} +
      +
      + + +
      ) diff --git a/apps/web/lib/search-params.ts b/apps/web/lib/search-params.ts index 9868a897..b283baf5 100644 --- a/apps/web/lib/search-params.ts +++ b/apps/web/lib/search-params.ts @@ -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( [], ) diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index cf53d37e..885144ac 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -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).*)", ], }