diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 597b3f3f9..fac8cc402 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 9f11aa80d..7cb434101 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 5465c38b4..544002e0d 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 2e45cac1f..9dbf66f74 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 4ccc45602..55e1215a1 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 - } - if (integration && INTEGRATION_TO_CARD[integration]) { - setSelectedCard(INTEGRATION_TO_CARD[integration]) - } - }, [integration, pluginsPanel]) + 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 handleBack = () => { - setSelectedCard(null) - setIntegration(null) - void setPluginsPanel(null) - } + 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 - switch (selectedCard) { - case "mcp": - return - case "import": - return - case "chrome": - return ( - - - - ) - case "shortcuts": - return ( - - - - ) - case "raycast": - return ( - - - - ) - case "connections": - return ( - - - - ) - case "plugins": - return ( - - - - ) + 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 (id === "import") { + return tweetCount > 0 + ? { label: `${tweetCount} tweets imported`, variant: "connected" } + : undefined + } + 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 d3613400f..5ae871781 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 cfaf7da0f..5bf6ae917 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 0cbd50e57..8f157a69d 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 9a6b37c37..7a95b0697 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. -

      +

      + 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} - +
      + {steps.map((step) => ( +
      +
      + {`Step +
      +
      +
      + + Step {step.number} + +
      +

      + {step.title} +

      -

      - {step.title} -

      -
      - ))} + ))} +
      -
      - + +
      ) diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index d190052cd..31ae6364e 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -9,6 +9,10 @@ import { XIcon, Search, Check } from "lucide-react" import { Button } from "@ui/components/button" import { DEFAULT_PROJECT_ID } from "@lib/constants" import type { ContainerTagListType } from "@lib/types" +import { + compareSpacesUserFirst, + spaceSelectorDisplayName, +} from "@/lib/ingest-auto-space" interface SelectSpacesModalProps { isOpen: boolean @@ -75,24 +79,26 @@ export function SelectSpacesModal({ name: "My Space", emoji: "📁", containerTag: DEFAULT_PROJECT_ID, - } + isExperimental: false, + isNova: false, + createdAt: "", + updatedAt: "", + } as ContainerTagListType - const allSpaces = [ - defaultSpace, - ...projects.filter((p) => p.containerTag !== DEFAULT_PROJECT_ID), - ] + const rest = projects + .filter((p) => p.containerTag !== DEFAULT_PROJECT_ID) + .sort(compareSpacesUserFirst) - let result = allSpaces - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase() - result = allSpaces.filter( - (p) => - p.containerTag.toLowerCase().includes(query) || - p.name?.toLowerCase().includes(query), - ) + const allSpaces = [defaultSpace, ...rest] + if (!searchQuery.trim()) { + return allSpaces } - - return result + const query = searchQuery.trim().toLowerCase() + return allSpaces.filter( + (p) => + p.containerTag.toLowerCase().includes(query) || + (p.name ?? "").toLowerCase().includes(query), + ) }, [projects, searchQuery]) return ( @@ -169,7 +175,7 @@ export function SelectSpacesModal({ type="button" onClick={() => handleToggle(project.containerTag)} className={cn( - "flex items-center gap-3 w-full px-3 py-2.5 rounded-[12px] cursor-pointer transition-colors text-left", + "flex min-w-0 max-w-full items-center gap-3 w-full px-3 py-2.5 rounded-[12px] cursor-pointer transition-colors text-left", isSelected ? "bg-[#14161A] border border-[rgba(82,89,102,0.3)]" : "bg-transparent border border-transparent hover:bg-[#14161A]/50", @@ -198,9 +204,14 @@ export function SelectSpacesModal({ {isSelected && }
      )} - {project.emoji || "📁"} - - {project.name ?? project.containerTag} + + {project.emoji || "📁"} + + + {spaceSelectorDisplayName(project, project.containerTag)} ) diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index e8e2e68ce..248889b73 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -34,6 +34,10 @@ import { } from "@repo/ui/components/select" import { Button } from "@repo/ui/components/button" import { analytics } from "@/lib/analytics" +import { + compareSpacesUserFirst, + spaceSelectorDisplayName, +} from "@/lib/ingest-auto-space" export interface SpaceSelectorProps { selectedProjects: string[] @@ -89,9 +93,19 @@ export function SpaceSelector({ const { allProjects, isLoading } = useContainerTags() + const sortedOtherSpaces = useMemo( + () => + allProjects + .filter( + (p: ContainerTagListType) => p.containerTag !== DEFAULT_PROJECT_ID, + ) + .sort(compareSpacesUserFirst), + [allProjects], + ) + const displayInfo = useMemo(() => { if (selectedProjects.length === 1) { - const containerTag = selectedProjects[0] + const containerTag = selectedProjects[0] ?? "" if (containerTag === DEFAULT_PROJECT_ID) { return { name: "My Space", emoji: "📁", isMultiple: false } } @@ -99,7 +113,7 @@ export function SpaceSelector({ (p: ContainerTagListType) => p.containerTag === containerTag, ) return { - name: found?.name || containerTag, + name: spaceSelectorDisplayName(found, containerTag), emoji: found?.emoji || "📁", isMultiple: false, } @@ -113,7 +127,6 @@ export function SpaceSelector({ } } - // Nothing selected — default to "My Space" return { name: "My Space", emoji: "📁", isMultiple: false } }, [allProjects, selectedProjects]) @@ -261,6 +274,7 @@ export function SpaceSelector({ "min-w-0 truncate text-sm font-medium text-white", "max-w-[10rem] md:max-w-[15rem]", )} + title={isLoading ? undefined : displayInfo.name} > {isLoading ? "…" : displayInfo.name} @@ -285,7 +299,7 @@ export function SpaceSelector({ -
      -
      -
      - - My Spaces - -
      +
      +
      + + My Spaces + +
      +
      handleSelectSingleSpace(DEFAULT_PROJECT_ID)} className={cn( - "flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium", + "flex min-w-0 max-w-full items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium", selectedProjects.length === 1 && selectedProjects[0] === DEFAULT_PROJECT_ID ? "bg-[#293952]/40" @@ -312,59 +331,57 @@ export function SpaceSelector({ )} > 📁 - My Space + My Space - {allProjects - .filter( - (p: ContainerTagListType) => - p.containerTag !== DEFAULT_PROJECT_ID, - ) - .map((project: ContainerTagListType) => ( - - handleSelectSingleSpace(project.containerTag) - } - className={cn( - "flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium group", - selectedProjects.length === 1 && - selectedProjects[0] === project.containerTag - ? "bg-[#293952]/40" - : "opacity-60 hover:opacity-100 hover:bg-[#293952]/40", - )} + {sortedOtherSpaces.map((project: ContainerTagListType) => ( + handleSelectSingleSpace(project.containerTag)} + className={cn( + "flex min-w-0 max-w-full items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium group", + selectedProjects.length === 1 && + selectedProjects[0] === project.containerTag + ? "bg-[#293952]/40" + : "opacity-60 hover:opacity-100 hover:bg-[#293952]/40", + )} + > + + {project.emoji || "📁"} + + - - {project.emoji || "📁"} - - - {project.name ?? project.containerTag} - - {enableDelete && ( - - )} - - ))} + {spaceSelectorDisplayName(project, project.containerTag)} + + {enableDelete && ( + + )} + + ))}
      - +