diff options
| author | MaheshtheDev <[email protected]> | 2025-09-01 04:00:45 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-09-01 04:00:45 +0000 |
| commit | 3157b987fc490ebf5e51bdd71c02242076409160 (patch) | |
| tree | 540c9a797f5ff14ceac736e66b3d78b79f341cfb /apps | |
| parent | Merge branch 'main' of https://github.com/supermemoryai/supermemory (diff) | |
| download | supermemory-3157b987fc490ebf5e51bdd71c02242076409160.tar.xz supermemory-3157b987fc490ebf5e51bdd71c02242076409160.zip | |
feat: redesign MCP connection flow with step-based UI and v1 migration (#399)
[Screen Recording 2025-08-31 at 12.03.49 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/f6c88b1f-9f16-47aa-8d5f-6cde61b61e4f.mov" />](https://app.graphite.dev/user-attachments/video/f6c88b1f-9f16-47aa-8d5f-6cde61b61e4f.mov)
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/components/connect-ai-modal.tsx | 544 | ||||
| -rw-r--r-- | apps/web/components/menu.tsx | 17 |
2 files changed, 419 insertions, 142 deletions
diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx index ff5f1064..5099deed 100644 --- a/apps/web/components/connect-ai-modal.tsx +++ b/apps/web/components/connect-ai-modal.tsx @@ -1,6 +1,8 @@ "use client"; -import { useIsMobile } from "@hooks/use-mobile"; +import { $fetch } from "@lib/api"; +import { useForm } from "@tanstack/react-form"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { Button } from "@ui/components/button"; import { Dialog, @@ -19,10 +21,12 @@ import { SelectValue, } from "@ui/components/select"; import { CopyableCell } from "@ui/copyable-cell"; -import { CopyIcon, ExternalLink } from "lucide-react"; +import { CopyIcon, ExternalLink, Loader2 } from "lucide-react"; import Image from "next/image"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { z } from "zod/v4"; +import { analytics } from "@/lib/analytics"; const clients = { cursor: "Cursor", @@ -36,24 +40,138 @@ const clients = { enconvo: "Enconvo", } as const; +const mcpMigrationSchema = z.object({ + url: z + .string() + .min(1, "MCP Link is required") + .regex( + /^https:\/\/mcp\.supermemory\.ai\/[^/]+\/sse$/, + "Link must be in format: https://mcp.supermemory.ai/userId/sse", + ), +}); + +interface Project { + id: string; + name: string; + containerTag: string; + createdAt: string; + updatedAt: string; + isExperimental?: boolean; +} + interface ConnectAIModalProps { children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; } -export function ConnectAIModal({ children }: ConnectAIModalProps) { - const [client, setClient] = useState<keyof typeof clients>("cursor"); - const [isOpen, setIsOpen] = useState(false); - const [showAllTools, setShowAllTools] = useState(false); - const isMobile = useIsMobile(); - const installCommand = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`; +export function ConnectAIModal({ + children, + open, + onOpenChange, +}: ConnectAIModalProps) { + const [selectedClient, setSelectedClient] = useState< + keyof typeof clients | null + >(null); + const [internalIsOpen, setInternalIsOpen] = useState(false); + const isOpen = open !== undefined ? open : internalIsOpen; + const setIsOpen = onOpenChange || setInternalIsOpen; + const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false); + const [selectedProject, setSelectedProject] = useState<string | null>("none"); + const projectId = localStorage.getItem("selectedProject") ?? "default"; + + useEffect(() => { + analytics.mcpViewOpened(); + }, []); + + const { data: projects = [], isLoading: isLoadingProjects } = useQuery({ + queryKey: ["projects"], + queryFn: async () => { + const response = await $fetch("@get/projects"); + if (response.error) { + throw new Error(response.error?.message || "Failed to load projects"); + } + return response.data?.projects || []; + }, + staleTime: 30 * 1000, + }); + + const mcpMigrationForm = useForm({ + defaultValues: { url: "" }, + onSubmit: async ({ value, formApi }) => { + const userId = extractUserIdFromMCPUrl(value.url); + if (userId) { + migrateMCPMutation.mutate({ userId, projectId }); + formApi.reset(); + } + }, + validators: { + onChange: mcpMigrationSchema, + }, + }); + + const extractUserIdFromMCPUrl = (url: string): string | null => { + const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/; + const match = url.trim().match(regex); + return match?.[1] || null; + }; + + const migrateMCPMutation = useMutation({ + mutationFn: async ({ + userId, + projectId, + }: { + userId: string; + projectId: string; + }) => { + const response = await $fetch("@post/memories/migrate-mcp", { + body: { userId, projectId }, + }); + + if (response.error) { + throw new Error( + response.error?.message || "Failed to migrate documents", + ); + } + + return response.data; + }, + onSuccess: (data) => { + toast.success("Migration completed!", { + description: `Successfully migrated ${data?.migratedCount} documents`, + }); + setIsMigrateDialogOpen(false); + }, + onError: (error) => { + toast.error("Migration failed", { + description: error instanceof Error ? error.message : "Unknown error", + }); + }, + }); + + function generateInstallCommand() { + if (!selectedClient) return ""; + + let command = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${selectedClient} --oauth=yes`; + + if (selectedProject && selectedProject !== "none") { + // Remove the "sm_project_" prefix from the containerTag + const projectIdForCommand = selectedProject.replace(/^sm_project_/, ""); + command += ` --project ${projectIdForCommand}`; + } + + return command; + } const copyToClipboard = () => { - navigator.clipboard.writeText(installCommand); + const command = generateInstallCommand(); + navigator.clipboard.writeText(command); + analytics.mcpInstallCmdCopied(); toast.success("Copied to clipboard!"); }; return ( - <Dialog open={isOpen} onOpenChange={setIsOpen}> + <Dialog onOpenChange={setIsOpen} open={isOpen}> <DialogTrigger asChild>{children}</DialogTrigger> <DialogContent className="sm:max-w-4xl"> <DialogHeader> @@ -65,144 +183,194 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) { </DialogDescription> </DialogHeader> - <div className="mb-6 block md:hidden"> - <label - className="text-sm font-medium text-white/80 block mb-2" - htmlFor="mcp-server-url" - > - MCP Server URL - </label> - <div className="p-3 bg-white/5 rounded border border-white/10"> - <CopyableCell - className="font-mono text-sm text-blue-400" - value="https://api.supermemory.ai/mcp" - /> - </div> - <p className="text-xs text-white/50 mt-2"> - Click URL to copy to clipboard. Use this URL to configure - supermemory in your AI assistant. - </p> - </div> - <div className="space-y-6"> - <div className="hidden md:block"> - <h3 className="text-sm font-medium mb-3">Supported AI Tools</h3> - <div className="grid grid-cols-2 md:grid-cols-3 gap-3 text-sm"> + {/* Step 1: Client Selection */} + <div className="space-y-4"> + <div className="flex items-center gap-3"> + <div + className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${"bg-white/10 text-white/60"}`} + > + 1 + </div> + <h3 className="text-sm font-medium">Select Your AI Client</h3> + </div> + + <div className="space-x-2"> {Object.entries(clients) - .slice(0, showAllTools ? undefined : isMobile ? 4 : 6) + .slice(0, 6) .map(([key, clientName]) => ( - <div - key={clientName} - className="flex items-center gap-3 px-3 py-3 bg-muted rounded-md" + <button + className={`pr-3 pl-1 rounded-full border cursor-pointer transition-all ${ + selectedClient === key + ? "border-blue-500 bg-blue-500/10" + : "border-white/10 hover:border-white/20 hover:bg-white/5" + }`} + key={key} + onClick={() => + setSelectedClient(key as keyof typeof clients) + } + type="button" > - <div className="w-8 h-8 relative flex-shrink-0 flex items-center justify-center"> - <Image - src={`/mcp-supported-tools/${key == "claude-code" ? "claude" : key}.png`} - alt={clientName} - width={isMobile ? 20 : 32} - height={isMobile ? 20 : 32} - className={"rounded object-contain"} - onError={(e) => { - const target = e.target as HTMLImageElement; - target.style.display = "none"; - const parent = target.parentElement; - if ( - parent && - !parent.querySelector(".fallback-text") - ) { - const fallback = document.createElement("span"); - fallback.className = - "fallback-text text-xs font-bold text-muted-foreground"; - fallback.textContent = clientName - .substring(0, 2) - .toUpperCase(); - parent.appendChild(fallback); - } - }} - /> + <div className="flex items-center gap-1"> + <div className="w-8 h-8 flex items-center justify-center"> + <Image + alt={clientName} + className="rounded object-contain" + height={20} + onError={(e) => { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + const parent = target.parentElement; + if ( + parent && + !parent.querySelector(".fallback-text") + ) { + const fallback = document.createElement("span"); + fallback.className = + "fallback-text text-sm font-bold text-white/40"; + fallback.textContent = clientName + .substring(0, 2) + .toUpperCase(); + parent.appendChild(fallback); + } + }} + src={`/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`} + width={20} + /> + </div> + <span className="text-sm font-medium text-white/80"> + {clientName} + </span> </div> - <span className="text-sm font-medium">{clientName}</span> - </div> + </button> ))} </div> - {Object.entries(clients).length > 6 && ( - <div className="mt-3 text-center"> - <Button - variant="ghost" - size="sm" - onClick={() => setShowAllTools(!showAllTools)} - className="text-xs text-muted-foreground hover:text-foreground" - > - {showAllTools ? "Show Less" : `Show All`} - </Button> - </div> - )} </div> - <div className="hidden md:block"> - <h3 className="text-sm font-medium mb-3">Quick Installation</h3> - <div className="space-y-3 flex gap-2 items-center justify-between"> - <Select - value={client} - onValueChange={(value) => - setClient(value as keyof typeof clients) - } - > - <SelectTrigger className="w-48 mb-0"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {Object.entries(clients).map(([key, value]) => ( - <SelectItem key={key} value={key}> - <div className="flex items-center gap-2"> - <div className="w-4 h-4 relative flex-shrink-0 flex items-center justify-center"> - <Image - src={`/mcp-supported-tools/${key == "claude-code" ? "claude" : key}.png`} - alt={value} - width={16} - height={16} - className="rounded object-contain" - onError={(e) => { - const target = e.target as HTMLImageElement; - target.style.display = "none"; - const parent = target.parentElement; - if ( - parent && - !parent.querySelector(".fallback-text") - ) { - const fallback = document.createElement("span"); - fallback.className = - "fallback-text text-xs font-bold text-muted-foreground"; - fallback.textContent = value - .substring(0, 1) - .toUpperCase(); - parent.appendChild(fallback); - } - }} - /> - </div> - {value} - </div> + {/* Step 2: Project Selection */} + {selectedClient && ( + <div className="space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> + 2 + </div> + <h3 className="text-sm font-medium"> + Select Target Project (Optional) + </h3> + </div> + + <div className="max-w-md"> + <Select + disabled={isLoadingProjects} + onValueChange={setSelectedProject} + value={selectedProject || "none"} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="Select project" /> + </SelectTrigger> + <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10"> + <SelectItem + className="text-white hover:bg-white/10" + value="none" + > + Auto-select project + </SelectItem> + <SelectItem + className="text-white hover:bg-white/10" + value="sm_project_default" + > + Default Project </SelectItem> - ))} - </SelectContent> - </Select> + {projects + .filter( + (p: Project) => p.containerTag !== "sm_project_default", + ) + .map((project: Project) => ( + <SelectItem + className="text-white hover:bg-white/10" + key={project.id} + value={project.containerTag} + > + {project.name} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + </div> + )} + + {/* Step 3: Command Line */} + {selectedClient && ( + <div className="space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> + 3 + </div> + <h3 className="text-sm font-medium">Installation Command</h3> + </div> - <div className="relative w-full flex items-center"> + <div className="relative"> <Input className="font-mono text-xs w-full pr-10" readOnly - value={installCommand} + value={generateInstallCommand()} /> - <Button + className="absolute top-[-1px] right-0 cursor-pointer" onClick={copyToClipboard} - className="absolute right-0 cursor-pointer" variant="ghost" > <CopyIcon className="size-4" /> </Button> </div> + + <p className="text-xs text-white/50"> + Copy and run this command in your terminal to install the MCP + server + </p> + </div> + )} + + {/* Blurred Command Placeholder */} + {!selectedClient && ( + <div className="space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> + 3 + </div> + <h3 className="text-sm font-medium">Installation Command</h3> + </div> + + <div className="relative"> + <div className="w-full h-10 bg-white/5 border border-white/10 rounded-md flex items-center px-3"> + <div className="w-full h-4 bg-white/20 rounded animate-pulse blur-sm" /> + </div> + </div> + + <p className="text-xs text-white/30"> + Select a client above to see the installation command + </p> + </div> + )} + + <div className="gap-2 hidden"> + <div> + <label + className="text-sm font-medium text-white/80 block mb-2" + htmlFor="mcp-server-url-desktop" + > + MCP Server URL + </label> + <p className="text-xs text-white/50 mt-2"> + Use this URL to configure supermemory in your AI assistant + </p> + </div> + <div className="p-1 bg-white/5 rounded-lg border border-white/10 items-center flex px-2"> + <CopyableCell + className="font-mono text-xs text-blue-400" + value="https://api.supermemory.ai/mcp" + /> </div> </div> @@ -217,22 +385,120 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) { </div> <div className="flex justify-between items-center pt-4 border-t"> - <Button - variant="outline" - onClick={() => - window.open( - "https://docs.supermemory.ai/supermemory-mcp/introduction", - "_blank", - ) - } - > - <ExternalLink className="w-4 h-4 mr-2" /> - Learn More - </Button> + <div className="flex items-center gap-4"> + <Button + onClick={() => + window.open( + "https://docs.supermemory.ai/supermemory-mcp/introduction", + "_blank", + ) + } + variant="outline" + > + <ExternalLink className="w-2 h-2 mr-2" /> + Learn More + </Button> + + <Button + onClick={() => setIsMigrateDialogOpen(true)} + variant="outline" + > + Migrate from v1 + </Button> + </div> <Button onClick={() => setIsOpen(false)}>Done</Button> </div> </div> </DialogContent> + + {/* Migration Dialog */} + {isMigrateDialogOpen && ( + <Dialog + onOpenChange={setIsMigrateDialogOpen} + open={isMigrateDialogOpen} + > + <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white"> + <div> + <DialogHeader> + <DialogTitle>Migrate from MCP v1</DialogTitle> + <DialogDescription className="text-white/60"> + Migrate your MCP documents from the legacy system. + </DialogDescription> + </DialogHeader> + <form + onSubmit={(e) => { + e.preventDefault(); + e.stopPropagation(); + mcpMigrationForm.handleSubmit(); + }} + > + <div className="grid gap-4"> + <div className="flex flex-col gap-2"> + <label className="text-sm font-medium" htmlFor="mcpUrl"> + MCP Link + </label> + <mcpMigrationForm.Field name="url"> + {({ state, handleChange, handleBlur }) => ( + <> + <Input + className="bg-white/5 border-white/10 text-white" + id="mcpUrl" + onBlur={handleBlur} + onChange={(e) => handleChange(e.target.value)} + placeholder="https://mcp.supermemory.ai/your-user-id/sse" + value={state.value} + /> + {state.meta.errors.length > 0 && ( + <p className="text-sm text-red-400 mt-1"> + {state.meta.errors.join(", ")} + </p> + )} + </> + )} + </mcpMigrationForm.Field> + <p className="text-xs text-white/50"> + Enter your old MCP Link in the format: <br /> + <span className="font-mono"> + https://mcp.supermemory.ai/userId/sse + </span> + </p> + </div> + </div> + <div className="flex justify-end gap-3 mt-4"> + <Button + className="bg-white/5 hover:bg-white/10 border-white/10 text-white" + onClick={() => { + setIsMigrateDialogOpen(false); + mcpMigrationForm.reset(); + }} + type="button" + variant="outline" + > + Cancel + </Button> + <Button + className="bg-white/10 hover:bg-white/20 text-white border-white/20" + disabled={ + migrateMCPMutation.isPending || + !mcpMigrationForm.state.canSubmit + } + type="submit" + > + {migrateMCPMutation.isPending ? ( + <> + <Loader2 className="h-4 w-4 animate-spin mr-2" /> + Migrating... + </> + ) : ( + "Migrate" + )} + </Button> + </div> + </form> + </div> + </DialogContent> + </Dialog> + )} </Dialog> ); } diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx index f59617c0..6eb81128 100644 --- a/apps/web/components/menu.tsx +++ b/apps/web/components/menu.tsx @@ -6,6 +6,7 @@ import { fetchMemoriesFeature, } from "@repo/lib/queries"; import { Button } from "@repo/ui/components/button"; +import { ConnectAIModal } from "./connect-ai-modal"; import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"; import { GlassMenuEffect } from "@ui/other/glass-effect"; import { useCustomer } from "autumn-js/react"; @@ -20,7 +21,6 @@ import { ProjectSelector } from "./project-selector"; import { useTour } from "./tour"; import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"; import { IntegrationsView } from "./views/integrations"; -import { MCPView } from "./views/mcp"; import { ProfileView } from "./views/profile"; const MCPIcon = ({ className }: { className?: string }) => { @@ -47,6 +47,7 @@ function Menu({ id }: { id?: string }) { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isCollapsing, setIsCollapsing] = useState(false); const [showAddMemoryView, setShowAddMemoryView] = useState(false); + const [showConnectAIModal, setShowConnectAIModal] = useState(false); const isMobile = useIsMobile(); const { activePanel, setActivePanel } = useMobilePanel(); const { setMenuExpanded } = useTour(); @@ -125,6 +126,11 @@ function Menu({ id }: { id?: string }) { if (isMobile) { setActivePanel("chat"); } + } else if (key === "mcp") { + // Open ConnectAIModal directly for MCP + setIsMobileMenuOpen(false); + setExpandedView(null); + setShowConnectAIModal(true); } else { if (expandedView === key) { setIsCollapsing(true); @@ -392,7 +398,6 @@ function Menu({ id }: { id?: string }) { ease: [0.4, 0, 0.2, 1], }} > - {expandedView === "mcp" && <MCPView />} {expandedView === "profile" && <ProfileView />} {expandedView === "integrations" && ( <IntegrationsView /> @@ -608,7 +613,6 @@ function Menu({ id }: { id?: string }) { {expandedView === "addUrl" && ( <AddMemoryExpandedView /> )} - {expandedView === "mcp" && <MCPView />} {expandedView === "profile" && <ProfileView />} {expandedView === "integrations" && ( <IntegrationsView /> @@ -631,6 +635,13 @@ function Menu({ id }: { id?: string }) { onClose={() => setShowAddMemoryView(false)} /> )} + + <ConnectAIModal + onOpenChange={setShowConnectAIModal} + open={showConnectAIModal} + > + <Button className="hidden">Connect AI Assistant</Button> + </ConnectAIModal> </> ); } |