mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-06 08:16:03 +00:00
328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { $fetch } from "@lib/api"
|
|
import { useSession } from "@lib/auth"
|
|
import { useMutation } from "@tanstack/react-query"
|
|
import { Logo, LogoFull } from "@ui/assets/Logo"
|
|
import { Button } from "@ui/components/button"
|
|
import { Input } from "@ui/components/input"
|
|
import { GlassMenuEffect } from "@ui/other/glass-effect"
|
|
import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react"
|
|
import { AnimatePresence, motion } from "motion/react"
|
|
import Link from "next/link"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { useEffect, useState } from "react"
|
|
import { toast } from "sonner"
|
|
import { Spinner } from "@/components/spinner"
|
|
|
|
interface MigrateMCPRequest {
|
|
userId: string
|
|
projectId: string
|
|
}
|
|
|
|
interface MigrateMCPResponse {
|
|
success: boolean
|
|
migratedCount: number
|
|
message: string
|
|
documentIds?: string[]
|
|
}
|
|
|
|
export default function MigrateMCPPage() {
|
|
const router = useRouter()
|
|
const searchParams = useSearchParams()
|
|
const [mcpUrl, setMcpUrl] = useState("")
|
|
const [projectId, setProjectId] = useState("default")
|
|
|
|
const session = useSession()
|
|
|
|
// Extract MCP URL from query parameter
|
|
useEffect(() => {
|
|
const urlParam = searchParams.get("url")
|
|
if (urlParam) {
|
|
setMcpUrl(urlParam)
|
|
}
|
|
}, [searchParams])
|
|
|
|
useEffect(() => {
|
|
console.log("session", session)
|
|
if (!session.isPending && !session.data) {
|
|
const redirectUrl = new URL("/login", window.location.href)
|
|
redirectUrl.searchParams.set("redirect", window.location.href)
|
|
router.push(redirectUrl.toString())
|
|
return
|
|
}
|
|
}, [session, router])
|
|
|
|
// Extract userId from MCP URL
|
|
const getUserIdFromUrl = (url: string) => {
|
|
return url.split("/").at(-2) || ""
|
|
}
|
|
|
|
const migrateMutation = useMutation({
|
|
mutationFn: async (data: MigrateMCPRequest) => {
|
|
const response = await $fetch("@post/documents/migrate-mcp", {
|
|
body: data,
|
|
})
|
|
|
|
if (response.error) {
|
|
throw new Error(
|
|
response.error?.message || "Failed to migrate documents",
|
|
)
|
|
}
|
|
|
|
return response.data
|
|
},
|
|
onSuccess: (data: MigrateMCPResponse) => {
|
|
toast.success("Migration completed successfully", {
|
|
description: data.message,
|
|
})
|
|
// Redirect to home page after successful migration
|
|
setTimeout(() => {
|
|
router.push("/?open=mcp")
|
|
}, 2000) // Wait 2 seconds to show the success message
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error("Migration failed", {
|
|
description: error.message || "An unexpected error occurred",
|
|
})
|
|
},
|
|
})
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
const userId = getUserIdFromUrl(mcpUrl)
|
|
if (!userId) {
|
|
toast.error("Please enter a valid MCP URL")
|
|
return
|
|
}
|
|
|
|
migrateMutation.mutate({
|
|
userId,
|
|
projectId: projectId.trim() || "default",
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0f1419] overflow-hidden relative">
|
|
{/* Background elements */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/10 via-transparent to-purple-900/10" />
|
|
|
|
{/* Top navigation */}
|
|
<motion.div
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="absolute top-0 left-0 right-0 z-10 p-6 flex items-center justify-between"
|
|
initial={{ opacity: 0, y: -20 }}
|
|
>
|
|
<Link
|
|
className="pointer-events-auto hover:opacity-80 transition-opacity"
|
|
href="/"
|
|
>
|
|
<LogoFull className="h-8 hidden md:block text-white" />
|
|
<Logo className="h-8 md:hidden text-white" />
|
|
</Link>
|
|
</motion.div>
|
|
|
|
{/* Main content */}
|
|
<div className="flex items-center justify-center min-h-screen p-4 relative z-10">
|
|
<motion.div
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
className="w-full max-w-lg relative"
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 25,
|
|
delay: 0.1,
|
|
}}
|
|
>
|
|
{/* Glass card with effect */}
|
|
<div className="relative rounded-2xl overflow-hidden">
|
|
<GlassMenuEffect rounded="rounded-2xl" />
|
|
|
|
<div className="relative z-10 p-8 md:p-10">
|
|
{/* Header */}
|
|
<motion.div
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="text-center mb-8"
|
|
initial={{ opacity: 0, y: 10 }}
|
|
transition={{ delay: 0.2 }}
|
|
>
|
|
<div className="flex items-center justify-center mb-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl" />
|
|
<div className="relative bg-blue-500/10 p-3 rounded-full border border-blue-500/20">
|
|
<Zap className="size-6 text-blue-400" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<h1 className="text-2xl md:text-3xl font-bold text-white mb-2">
|
|
Upgrade supermemory MCP
|
|
</h1>
|
|
<p className="text-slate-400 text-sm md:text-base">
|
|
Migrate your documents to the new MCP server
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Form */}
|
|
<motion.form
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="space-y-6"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
onSubmit={handleSubmit}
|
|
transition={{ delay: 0.3 }}
|
|
>
|
|
<div className="space-y-2">
|
|
<label
|
|
className="text-sm font-medium text-slate-200 flex items-center gap-2"
|
|
htmlFor="mcpUrl"
|
|
>
|
|
<Upload className="size-4" />
|
|
MCP URL
|
|
</label>
|
|
<div className="relative">
|
|
<Input
|
|
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
|
|
disabled={migrateMutation.isPending}
|
|
id="mcpUrl"
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setMcpUrl(e.target.value)
|
|
}
|
|
placeholder="https://mcp.supermemory.ai/userId/sse"
|
|
type="url"
|
|
value={mcpUrl}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label
|
|
className="text-sm font-medium text-slate-200"
|
|
htmlFor="projectId"
|
|
>
|
|
Project ID
|
|
</label>
|
|
<div className="relative">
|
|
<Input
|
|
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
|
|
disabled={migrateMutation.isPending}
|
|
id="projectId"
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setProjectId(e.target.value)
|
|
}
|
|
placeholder="Project ID (default: 'default')"
|
|
type="text"
|
|
value={projectId}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<motion.div
|
|
whileHover={{ scale: 1.01 }}
|
|
whileTap={{ scale: 0.99 }}
|
|
>
|
|
<Button
|
|
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white border-0 py-3 rounded-xl font-medium shadow-lg hover:shadow-blue-500/25 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
disabled={
|
|
migrateMutation.isPending || !getUserIdFromUrl(mcpUrl)
|
|
}
|
|
size="lg"
|
|
type="submit"
|
|
>
|
|
{migrateMutation.isPending ? (
|
|
<>
|
|
<Spinner className="mr-2 size-4" />
|
|
Migrating documents…
|
|
</>
|
|
) : (
|
|
<>
|
|
Start Upgrade
|
|
<ArrowRight className="ml-2 size-4" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
</motion.div>
|
|
</motion.form>
|
|
|
|
{/* Success/Error States */}
|
|
<AnimatePresence mode="wait">
|
|
{migrateMutation.isSuccess && migrateMutation.data && (
|
|
<motion.div
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
className="mt-6"
|
|
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
key="success"
|
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
>
|
|
<div className="relative rounded-xl overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-green-500/10 to-emerald-500/10" />
|
|
<div className="relative p-4 border border-green-500/20 rounded-xl">
|
|
<div className="text-green-400">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<CheckCircle className="size-5" />
|
|
<p className="font-medium">
|
|
Migration completed successfully!
|
|
</p>
|
|
</div>
|
|
<p className="text-sm text-green-300/80 mb-3">
|
|
Migrated {migrateMutation.data.migratedCount}{" "}
|
|
documents
|
|
</p>
|
|
{migrateMutation.data.documentIds &&
|
|
migrateMutation.data.documentIds.length > 0 && (
|
|
<details className="mt-3">
|
|
<summary className="cursor-pointer hover:text-green-300 transition-colors text-sm font-medium">
|
|
View migrated document IDs
|
|
</summary>
|
|
<div className="mt-3 space-y-2 max-h-40 overflow-y-auto">
|
|
{migrateMutation.data.documentIds.map(
|
|
(id) => (
|
|
<code
|
|
className="block text-xs bg-black/30 px-3 py-2 rounded-lg border border-green-500/10 text-green-200"
|
|
key={id}
|
|
>
|
|
{id}
|
|
</code>
|
|
),
|
|
)}
|
|
</div>
|
|
</details>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{migrateMutation.isError && (
|
|
<motion.div
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
className="mt-6"
|
|
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
key="error"
|
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
>
|
|
<div className="relative rounded-xl overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-red-500/10 to-pink-500/10" />
|
|
<div className="relative p-4 border border-red-500/20 rounded-xl">
|
|
<div className="text-red-400">
|
|
<p className="font-medium mb-1">Migration failed</p>
|
|
<p className="text-sm text-red-300/80">
|
|
{migrateMutation.error?.message ||
|
|
"An unexpected error occurred"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|