mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-31 01:55:25 +00:00
fix
This commit is contained in:
parent
53eeadccca
commit
f82104ecc8
2 changed files with 119 additions and 115 deletions
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
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 { $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,
|
||||
DialogContent,
|
||||
|
|
@ -11,22 +11,22 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/components/dialog";
|
||||
import { Input } from "@ui/components/input";
|
||||
} from "@ui/components/dialog"
|
||||
import { Input } from "@ui/components/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/select";
|
||||
import { CopyableCell } from "@ui/copyable-cell";
|
||||
import { CopyIcon, ExternalLink, Loader2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
} from "@ui/components/select"
|
||||
import { CopyableCell } from "@ui/copyable-cell"
|
||||
import { CopyIcon, ExternalLink, Loader2 } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod/v4"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
const clients = {
|
||||
cursor: "Cursor",
|
||||
|
|
@ -39,7 +39,7 @@ const clients = {
|
|||
"roo-cline": "Roo Cline",
|
||||
witsy: "Witsy",
|
||||
enconvo: "Enconvo",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
const mcpMigrationSchema = z.object({
|
||||
url: z
|
||||
|
|
@ -49,21 +49,21 @@ const mcpMigrationSchema = z.object({
|
|||
/^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;
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
isExperimental?: boolean
|
||||
}
|
||||
|
||||
interface ConnectAIModalProps {
|
||||
children: React.ReactNode;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ConnectAIModal({
|
||||
|
|
@ -73,103 +73,103 @@ export function ConnectAIModal({
|
|||
}: 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";
|
||||
>(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();
|
||||
}, []);
|
||||
analytics.mcpViewOpened()
|
||||
}, [])
|
||||
|
||||
const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects");
|
||||
const response = await $fetch("@get/projects")
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load projects");
|
||||
throw new Error(response.error?.message || "Failed to load projects")
|
||||
}
|
||||
return response.data?.projects || [];
|
||||
return response.data?.projects || []
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
const mcpMigrationForm = useForm({
|
||||
defaultValues: { url: "" },
|
||||
onSubmit: async ({ value, formApi }) => {
|
||||
const userId = extractUserIdFromMCPUrl(value.url);
|
||||
const userId = extractUserIdFromMCPUrl(value.url)
|
||||
if (userId) {
|
||||
migrateMCPMutation.mutate({ userId, projectId });
|
||||
formApi.reset();
|
||||
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 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;
|
||||
userId: string
|
||||
projectId: string
|
||||
}) => {
|
||||
const response = await $fetch("@post/memories/migrate-mcp", {
|
||||
const response = await $fetch("@post/documents/migrate-mcp", {
|
||||
body: { userId, projectId },
|
||||
});
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to migrate documents",
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return response.data
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success("Migration completed!", {
|
||||
description: `Successfully migrated ${data?.migratedCount} documents`,
|
||||
});
|
||||
setIsMigrateDialogOpen(false);
|
||||
})
|
||||
setIsMigrateDialogOpen(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Migration failed", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
function generateInstallCommand() {
|
||||
if (!selectedClient) return "";
|
||||
if (!selectedClient) return ""
|
||||
|
||||
let command = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${selectedClient} --oauth=yes`;
|
||||
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}`;
|
||||
const projectIdForCommand = selectedProject.replace(/^sm_project_/, "")
|
||||
command += ` --project ${projectIdForCommand}`
|
||||
}
|
||||
|
||||
return command;
|
||||
return command
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
const command = generateInstallCommand();
|
||||
navigator.clipboard.writeText(command);
|
||||
analytics.mcpInstallCmdCopied();
|
||||
toast.success("Copied to clipboard!");
|
||||
};
|
||||
const command = generateInstallCommand()
|
||||
navigator.clipboard.writeText(command)
|
||||
analytics.mcpInstallCmdCopied()
|
||||
toast.success("Copied to clipboard!")
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={setIsOpen} open={isOpen}>
|
||||
|
|
@ -219,23 +219,27 @@ export function ConnectAIModal({
|
|||
className="rounded object-contain text-white fill-white"
|
||||
height={20}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
const parent = target.parentElement;
|
||||
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");
|
||||
const fallback = document.createElement("span")
|
||||
fallback.className =
|
||||
"fallback-text text-sm font-bold text-white/40";
|
||||
"fallback-text text-sm font-bold text-white/40"
|
||||
fallback.textContent = clientName
|
||||
.substring(0, 2)
|
||||
.toUpperCase();
|
||||
parent.appendChild(fallback);
|
||||
.toUpperCase()
|
||||
parent.appendChild(fallback)
|
||||
}
|
||||
}}
|
||||
src={key === "mcp-url" ? "/mcp-icon.svg" : `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`}
|
||||
src={
|
||||
key === "mcp-url"
|
||||
? "/mcp-icon.svg"
|
||||
: `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`
|
||||
}
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -275,9 +279,9 @@ export function ConnectAIModal({
|
|||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
"https://api.supermemory.ai/mcp",
|
||||
);
|
||||
analytics.mcpInstallCmdCopied();
|
||||
toast.success("Copied to clipboard!");
|
||||
)
|
||||
analytics.mcpInstallCmdCopied()
|
||||
toast.success("Copied to clipboard!")
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
|
|
@ -459,9 +463,9 @@ export function ConnectAIModal({
|
|||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mcpMigrationForm.handleSubmit();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
mcpMigrationForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
|
|
@ -500,8 +504,8 @@ export function ConnectAIModal({
|
|||
<Button
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
|
||||
onClick={() => {
|
||||
setIsMigrateDialogOpen(false);
|
||||
mcpMigrationForm.reset();
|
||||
setIsMigrateDialogOpen(false)
|
||||
mcpMigrationForm.reset()
|
||||
}}
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -532,5 +536,5 @@ export function ConnectAIModal({
|
|||
</Dialog>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -8,50 +8,50 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog";
|
||||
import { Input } from "@repo/ui/components/input";
|
||||
import { Label } from "@repo/ui/components/label";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations";
|
||||
} from "@repo/ui/components/dialog"
|
||||
import { Input } from "@repo/ui/components/input"
|
||||
import { Label } from "@repo/ui/components/label"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { useState } from "react"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
|
||||
interface CreateProjectDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function CreateProjectDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: CreateProjectDialogProps) {
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const { createProjectMutation } = useProjectMutations();
|
||||
const [projectName, setProjectName] = useState("")
|
||||
const { createProjectMutation } = useProjectMutations()
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
setProjectName("");
|
||||
};
|
||||
onOpenChange(false)
|
||||
setProjectName("")
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (projectName.trim()) {
|
||||
createProjectMutation.mutate(projectName, {
|
||||
onSuccess: () => {
|
||||
handleClose();
|
||||
handleClose()
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||
<DialogContent className="sm:max-w-2xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Project</DialogTitle>
|
||||
|
|
@ -61,23 +61,23 @@ export function CreateProjectDialog({
|
|||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="flex flex-col gap-2"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
className="bg-white/5 border-white/10 text-white"
|
||||
placeholder="My Awesome Project"
|
||||
value={projectName}
|
||||
id="projectName"
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && projectName.trim()) {
|
||||
handleCreate();
|
||||
handleCreate()
|
||||
}
|
||||
}}
|
||||
placeholder="My Awesome Project"
|
||||
value={projectName}
|
||||
/>
|
||||
<p className="text-xs text-white/50">
|
||||
This will help you organize your memories
|
||||
|
|
@ -90,10 +90,10 @@ export function CreateProjectDialog({
|
|||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -103,12 +103,12 @@ export function CreateProjectDialog({
|
|||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20"
|
||||
disabled={
|
||||
createProjectMutation.isPending || !projectName.trim()
|
||||
}
|
||||
onClick={handleCreate}
|
||||
type="button"
|
||||
>
|
||||
{createProjectMutation.isPending ? (
|
||||
<>
|
||||
|
|
@ -126,5 +126,5 @@ export function CreateProjectDialog({
|
|||
</Dialog>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue