diff options
| author | CodeWithShreyans <[email protected]> | 2025-09-04 16:46:47 +0000 |
|---|---|---|
| committer | CodeWithShreyans <[email protected]> | 2025-09-04 16:46:47 +0000 |
| commit | 84fea4a6981385145dd84a58a610b4782caec047 (patch) | |
| tree | 3d3f84d0ce15a096086c28e86e863ea99d25006b /apps/web/app | |
| parent | feat: openai python sdk (#409) (diff) | |
| download | archived-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.tsx | 299 | ||||
| -rw-r--r-- | apps/web/app/upgrade-mcp/page.tsx | 324 |
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> + ) +} |