aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app
diff options
context:
space:
mode:
authorCodeWithShreyans <[email protected]>2025-09-04 16:46:47 +0000
committerCodeWithShreyans <[email protected]>2025-09-04 16:46:47 +0000
commit84fea4a6981385145dd84a58a610b4782caec047 (patch)
tree3d3f84d0ce15a096086c28e86e863ea99d25006b /apps/web/app
parentfeat: openai python sdk (#409) (diff)
downloadarchived-supermemory-84fea4a6981385145dd84a58a610b4782caec047.tar.xz
archived-supermemory-84fea4a6981385145dd84a58a610b4782caec047.zip
feat: add mcp migrate route (#410)shreyans/09-03-feat_add_mcp_migrate_route
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/page.tsx299
-rw-r--r--apps/web/app/upgrade-mcp/page.tsx324
2 files changed, 473 insertions, 150 deletions
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 5018c337..6bf8cf6f 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,14 +1,14 @@
-"use client";
-
-import { useIsMobile } from "@hooks/use-mobile";
-import { useAuth } from "@lib/auth-context";
-import { $fetch } from "@repo/lib/api";
-import { MemoryGraph } from "@repo/ui/memory-graph";
-import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
-import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
-import { Logo, LogoFull } from "@ui/assets/Logo";
-import { Button } from "@ui/components/button";
-import { GlassMenuEffect } from "@ui/other/glass-effect";
+"use client"
+
+import { useIsMobile } from "@hooks/use-mobile"
+import { useAuth } from "@lib/auth-context"
+import { $fetch } from "@repo/lib/api"
+import { MemoryGraph } from "@repo/ui/memory-graph"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
+import { Logo, LogoFull } from "@ui/assets/Logo"
+import { Button } from "@ui/components/button"
+import { GlassMenuEffect } from "@ui/other/glass-effect"
import {
HelpCircle,
LayoutGrid,
@@ -16,59 +16,59 @@ import {
LoaderIcon,
MessageSquare,
Unplug,
-} from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import type { z } from "zod";
-import { ConnectAIModal } from "@/components/connect-ai-modal";
-import { InstallPrompt } from "@/components/install-prompt";
-import { MemoryListView } from "@/components/memory-list-view";
-import Menu from "@/components/menu";
-import { ProjectSelector } from "@/components/project-selector";
-import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal";
-import type { TourStep } from "@/components/tour";
-import { TourAlertDialog, useTour } from "@/components/tour";
-import { AddMemoryView } from "@/components/views/add-memory";
-import { ChatRewrite } from "@/components/views/chat";
-import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants";
-import { useViewMode } from "@/lib/view-mode-context";
-import { useChatOpen, useProject } from "@/stores";
-import { useGraphHighlights } from "@/stores/highlights";
-
-type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
-type DocumentWithMemories = DocumentsResponse["documents"][0];
+} from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import Link from "next/link"
+import { useRouter } from "next/navigation"
+import { useCallback, useEffect, useMemo, useState } from "react"
+import type { z } from "zod"
+import { ConnectAIModal } from "@/components/connect-ai-modal"
+import { InstallPrompt } from "@/components/install-prompt"
+import { MemoryListView } from "@/components/memory-list-view"
+import Menu from "@/components/menu"
+import { ProjectSelector } from "@/components/project-selector"
+import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"
+import type { TourStep } from "@/components/tour"
+import { TourAlertDialog, useTour } from "@/components/tour"
+import { AddMemoryView } from "@/components/views/add-memory"
+import { ChatRewrite } from "@/components/views/chat"
+import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"
+import { useViewMode } from "@/lib/view-mode-context"
+import { useChatOpen, useProject } from "@/stores"
+import { useGraphHighlights } from "@/stores/highlights"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
const MemoryGraphPage = () => {
- const { documentIds: allHighlightDocumentIds } = useGraphHighlights();
- const isMobile = useIsMobile();
- const { viewMode, setViewMode, isInitialized } = useViewMode();
- const { selectedProject } = useProject();
- const { setSteps, isTourCompleted } = useTour();
- const { isOpen, setIsOpen } = useChatOpen();
- const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([]);
- const [showAddMemoryView, setShowAddMemoryView] = useState(false);
- const [showReferralModal, setShowReferralModal] = useState(false);
- const [showConnectAIModal, setShowConnectAIModal] = useState(false);
- const [isHelpHovered, setIsHelpHovered] = useState(false);
+ const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
+ const isMobile = useIsMobile()
+ const { viewMode, setViewMode, isInitialized } = useViewMode()
+ const { selectedProject } = useProject()
+ const { setSteps, isTourCompleted } = useTour()
+ const { isOpen, setIsOpen } = useChatOpen()
+ const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const [showReferralModal, setShowReferralModal] = useState(false)
+ const [showConnectAIModal, setShowConnectAIModal] = useState(false)
+ const [isHelpHovered, setIsHelpHovered] = useState(false)
// Fetch projects meta to detect experimental flag
const { data: projectsMeta = [] } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
- const response = await $fetch("@get/projects");
- return response.data?.projects ?? [];
+ const response = await $fetch("@get/projects")
+ return response.data?.projects ?? []
},
staleTime: 5 * 60 * 1000,
- });
+ })
const isCurrentProjectExperimental = !!projectsMeta.find(
(p: any) => p.containerTag === selectedProject,
- )?.isExperimental;
+ )?.isExperimental
// Tour state
- const [showTourDialog, setShowTourDialog] = useState(false);
+ const [showTourDialog, setShowTourDialog] = useState(false)
// Define tour steps with useMemo to prevent recreation
const tourSteps: TourStep[] = useMemo(() => {
@@ -201,37 +201,37 @@ const MemoryGraphPage = () => {
selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
position: "left",
},
- ];
- }, []);
+ ]
+ }, [])
// Check if tour has been completed before
useEffect(() => {
- const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
+ const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
if (!hasCompletedTour && !isTourCompleted) {
const timer = setTimeout(() => {
- setShowTourDialog(true);
- setShowConnectAIModal(false);
- }, 1000); // Show after 1 second
- return () => clearTimeout(timer);
+ setShowTourDialog(true)
+ setShowConnectAIModal(false)
+ }, 1000) // Show after 1 second
+ return () => clearTimeout(timer)
}
- }, [isTourCompleted]);
+ }, [isTourCompleted])
// Set up tour steps
useEffect(() => {
- setSteps(tourSteps);
- }, [setSteps, tourSteps]);
+ setSteps(tourSteps)
+ }, [setSteps, tourSteps])
// Save tour completion to localStorage
useEffect(() => {
if (isTourCompleted) {
- localStorage.setItem(TOUR_STORAGE_KEY, "true");
+ localStorage.setItem(TOUR_STORAGE_KEY, "true")
}
- }, [isTourCompleted]);
+ }, [isTourCompleted])
// Progressive loading via useInfiniteQuery
- const IS_DEV = process.env.NODE_ENV === "development";
- const PAGE_SIZE = IS_DEV ? 100 : 100;
- const MAX_TOTAL = 1000;
+ const IS_DEV = process.env.NODE_ENV === "development"
+ const PAGE_SIZE = IS_DEV ? 100 : 100
+ const MAX_TOTAL = 1000
const {
data,
@@ -253,76 +253,75 @@ const MemoryGraphPage = () => {
containerTags: selectedProject ? [selectedProject] : undefined,
},
disableValidation: true,
- });
+ })
if (response.error) {
- throw new Error(response.error?.message || "Failed to fetch documents");
+ throw new Error(response.error?.message || "Failed to fetch documents")
}
- return response.data;
+ return response.data
},
getNextPageParam: (lastPage, allPages) => {
const loaded = allPages.reduce(
(acc, p) => acc + (p.documents?.length ?? 0),
0,
- );
- if (loaded >= MAX_TOTAL) return undefined;
+ )
+ if (loaded >= MAX_TOTAL) return undefined
- const { currentPage, totalPages } = lastPage.pagination;
+ const { currentPage, totalPages } = lastPage.pagination
if (currentPage < totalPages) {
- return currentPage + 1;
+ return currentPage + 1
}
- return undefined;
+ return undefined
},
staleTime: 5 * 60 * 1000,
- });
+ })
const baseDocuments = useMemo(() => {
return (
data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
- );
- }, [data]);
+ )
+ }, [data])
const allDocuments = useMemo(() => {
- if (injectedDocs.length === 0) return baseDocuments;
- const byId = new Map<string, DocumentWithMemories>();
- for (const d of injectedDocs) byId.set(d.id, d);
- for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d);
- return Array.from(byId.values());
- }, [baseDocuments, injectedDocs]);
+ if (injectedDocs.length === 0) return baseDocuments
+ const byId = new Map<string, DocumentWithMemories>()
+ for (const d of injectedDocs) byId.set(d.id, d)
+ for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
+ return Array.from(byId.values())
+ }, [baseDocuments, injectedDocs])
- const totalLoaded = allDocuments.length;
- const hasMore = hasNextPage;
- const isLoadingMore = isFetchingNextPage;
+ const totalLoaded = allDocuments.length
+ const hasMore = hasNextPage
+ const isLoadingMore = isFetchingNextPage
const loadMoreDocuments = useCallback(async (): Promise<void> => {
if (hasNextPage && !isFetchingNextPage) {
- await fetchNextPage();
- return;
+ await fetchNextPage()
+ return
}
- return;
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
+ return
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
// Reset injected docs when project changes
useEffect(() => {
- setInjectedDocs([]);
- }, [selectedProject]);
+ setInjectedDocs([])
+ }, [selectedProject])
// Surgical fetch of missing highlighted documents (customId-based IDs from search)
useEffect(() => {
- if (!isOpen) return;
- if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0)
- return;
- const present = new Set<string>();
+ if (!isOpen) return
+ if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
+ const present = new Set<string>()
for (const d of [...baseDocuments, ...injectedDocs]) {
- if (d.id) present.add(d.id);
- if ((d as any).customId) present.add((d as any).customId as string);
+ if (d.id) present.add(d.id)
+ if ((d as any).customId) present.add((d as any).customId as string)
}
const missing = allHighlightDocumentIds.filter(
(id: string) => !present.has(id),
- );
- if (missing.length === 0) return;
- let cancelled = false;
+ )
+ if (missing.length === 0) return
+ let cancelled = false
const run = async () => {
try {
const resp = await $fetch("@post/memories/documents/by-ids", {
@@ -332,32 +331,32 @@ const MemoryGraphPage = () => {
containerTags: selectedProject ? [selectedProject] : undefined,
},
disableValidation: true,
- });
- if (cancelled || (resp as any)?.error) return;
+ })
+ if (cancelled || (resp as any)?.error) return
const extraDocs = (resp as any)?.data?.documents as
| DocumentWithMemories[]
- | undefined;
- if (!extraDocs || extraDocs.length === 0) return;
+ | undefined
+ if (!extraDocs || extraDocs.length === 0) return
setInjectedDocs((prev) => {
const seen = new Set<string>([
...prev.map((d) => d.id),
...baseDocuments.map((d) => d.id),
- ]);
- const merged = [...prev];
+ ])
+ const merged = [...prev]
for (const doc of extraDocs) {
if (!seen.has(doc.id)) {
- merged.push(doc);
- seen.add(doc.id);
+ merged.push(doc)
+ seen.add(doc.id)
}
}
- return merged;
- });
+ return merged
+ })
} catch {}
- };
- void run();
+ }
+ void run()
return () => {
- cancelled = true;
- };
+ cancelled = true
+ }
}, [
isOpen,
allHighlightDocumentIds.join("|"),
@@ -365,39 +364,39 @@ const MemoryGraphPage = () => {
injectedDocs,
selectedProject,
$fetch,
- ]);
+ ])
// Handle view mode change
const handleViewModeChange = useCallback(
(mode: "graph" | "list") => {
- setViewMode(mode);
+ setViewMode(mode)
},
[setViewMode],
- );
+ )
useEffect(() => {
- const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
+ const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) {
- setShowConnectAIModal(true);
+ setShowConnectAIModal(true)
} else if (showTourDialog) {
- setShowConnectAIModal(false);
+ setShowConnectAIModal(false)
}
- }, [allDocuments.length, showTourDialog]);
+ }, [allDocuments.length, showTourDialog])
// Prevent body scrolling
useEffect(() => {
- document.body.style.overflow = "hidden";
- document.body.style.height = "100vh";
- document.documentElement.style.overflow = "hidden";
- document.documentElement.style.height = "100vh";
+ document.body.style.overflow = "hidden"
+ document.body.style.height = "100vh"
+ document.documentElement.style.overflow = "hidden"
+ document.documentElement.style.height = "100vh"
return () => {
- document.body.style.overflow = "";
- document.body.style.height = "";
- document.documentElement.style.overflow = "";
- document.documentElement.style.height = "";
- };
- }, []);
+ document.body.style.overflow = ""
+ document.body.style.height = ""
+ document.documentElement.style.overflow = ""
+ document.documentElement.style.height = ""
+ }
+ }, [])
return (
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
@@ -528,9 +527,9 @@ const MemoryGraphPage = () => {
<button
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
onClick={(e) => {
- e.stopPropagation();
- setShowAddMemoryView(true);
- setShowConnectAIModal(false);
+ e.stopPropagation()
+ setShowAddMemoryView(true)
+ setShowConnectAIModal(false)
}}
type="button"
>
@@ -584,9 +583,9 @@ const MemoryGraphPage = () => {
<button
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
onClick={(e) => {
- e.stopPropagation();
- setShowAddMemoryView(true);
- setShowConnectAIModal(false);
+ e.stopPropagation()
+ setShowAddMemoryView(true)
+ setShowConnectAIModal(false)
}}
type="button"
>
@@ -737,26 +736,26 @@ const MemoryGraphPage = () => {
onClose={() => setShowReferralModal(false)}
/>
</div>
- );
-};
+ )
+}
// Wrapper component to handle auth and waitlist checks
export default function Page() {
- const router = useRouter();
- const { user } = useAuth();
+ const router = useRouter()
+ const { user } = useAuth()
useEffect(() => {
// save the token for chrome extension
- const url = new URL(window.location.href);
- const rawToken = url.searchParams.get("token");
+ const url = new URL(window.location.href)
+ const rawToken = url.searchParams.get("token")
if (rawToken) {
- const encodedToken = encodeURIComponent(rawToken);
- window.postMessage({ token: encodedToken }, "*");
- url.searchParams.delete("token");
- window.history.replaceState({}, "", url.toString());
+ const encodedToken = encodeURIComponent(rawToken)
+ window.postMessage({ token: encodedToken }, "*")
+ url.searchParams.delete("token")
+ window.history.replaceState({}, "", url.toString())
}
- }, []);
+ }, [])
// Show loading state while checking authentication and waitlist status
if (!user) {
@@ -767,7 +766,7 @@ export default function Page() {
<p className="text-white/60">Loading...</p>
</div>
</div>
- );
+ )
}
// If we have a user and they have access, show the main component
@@ -776,5 +775,5 @@ export default function Page() {
<MemoryGraphPage />
<InstallPrompt />
</>
- );
+ )
}
diff --git a/apps/web/app/upgrade-mcp/page.tsx b/apps/web/app/upgrade-mcp/page.tsx
new file mode 100644
index 00000000..baffcbd2
--- /dev/null
+++ b/apps/web/app/upgrade-mcp/page.tsx
@@ -0,0 +1,324 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import { Button } from "@ui/components/button"
+import { Input } from "@ui/components/input"
+import { GlassMenuEffect } from "@ui/other/glass-effect"
+import { useMutation } from "@tanstack/react-query"
+import { useRouter, useSearchParams } from "next/navigation"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+import { Spinner } from "@/components/spinner"
+import { motion, AnimatePresence } from "motion/react"
+import { Logo, LogoFull } from "@ui/assets/Logo"
+import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react"
+import Link from "next/link"
+import { useSession } from "@lib/auth"
+
+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/memories/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
+ initial={{ opacity: 0, y: -20 }}
+ animate={{ opacity: 1, y: 0 }}
+ className="absolute top-0 left-0 right-0 z-10 p-6 flex items-center justify-between"
+ >
+ <Link
+ href="/"
+ className="pointer-events-auto hover:opacity-80 transition-opacity"
+ >
+ <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
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ transition={{
+ type: "spring",
+ stiffness: 300,
+ damping: 25,
+ delay: 0.1,
+ }}
+ className="w-full max-w-lg relative"
+ >
+ {/* 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
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.2 }}
+ className="text-center mb-8"
+ >
+ <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="w-6 h-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
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.3 }}
+ onSubmit={handleSubmit}
+ className="space-y-6"
+ >
+ <div className="space-y-2">
+ <label
+ htmlFor="mcpUrl"
+ className="text-sm font-medium text-slate-200 flex items-center gap-2"
+ >
+ <Upload className="w-4 h-4" />
+ MCP URL
+ </label>
+ <div className="relative">
+ <Input
+ id="mcpUrl"
+ type="url"
+ value={mcpUrl}
+ onChange={(e) => setMcpUrl(e.target.value)}
+ placeholder="https://mcp.supermemory.ai/userId/sse"
+ 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}
+ />
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <label
+ htmlFor="projectId"
+ className="text-sm font-medium text-slate-200"
+ >
+ Project ID
+ </label>
+ <div className="relative">
+ <Input
+ id="projectId"
+ type="text"
+ value={projectId}
+ onChange={(e) => setProjectId(e.target.value)}
+ placeholder="Project ID (default: 'default')"
+ 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}
+ />
+ </div>
+ </div>
+
+ <motion.div
+ whileHover={{ scale: 1.01 }}
+ whileTap={{ scale: 0.99 }}
+ >
+ <Button
+ type="submit"
+ 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"
+ >
+ {migrateMutation.isPending ? (
+ <>
+ <Spinner className="mr-2 w-4 h-4" />
+ Migrating documents...
+ </>
+ ) : (
+ <>
+ Start Upgrade
+ <ArrowRight className="ml-2 w-4 h-4" />
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </motion.form>
+
+ {/* Success/Error States */}
+ <AnimatePresence mode="wait">
+ {migrateMutation.isSuccess && migrateMutation.data && (
+ <motion.div
+ key="success"
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ exit={{ opacity: 0, y: -10, scale: 0.95 }}
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
+ className="mt-6"
+ >
+ <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="w-5 h-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
+ key={id}
+ className="block text-xs bg-black/30 px-3 py-2 rounded-lg border border-green-500/10 text-green-200"
+ >
+ {id}
+ </code>
+ ),
+ )}
+ </div>
+ </details>
+ )}
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ )}
+
+ {migrateMutation.isError && (
+ <motion.div
+ key="error"
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ exit={{ opacity: 0, y: -10, scale: 0.95 }}
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
+ className="mt-6"
+ >
+ <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>
+ )
+}