mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat: add mcp migrate route (#410)
This commit is contained in:
parent
101f3817bd
commit
84fea4a698
6 changed files with 793 additions and 378 deletions
|
|
@ -1,14 +1,14 @@
|
|||
"use client";
|
||||
"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 { 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";
|
||||
} 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];
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
324
apps/web/app/upgrade-mcp/page.tsx
Normal file
324
apps/web/app/upgrade-mcp/page.tsx
Normal file
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,27 +1,28 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import {
|
||||
fetchConsumerProProduct,
|
||||
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";
|
||||
import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react";
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Drawer } from "vaul";
|
||||
import { useMobilePanel } from "@/lib/mobile-panel-context";
|
||||
import { TOUR_STEP_IDS } from "@/lib/tour-constants";
|
||||
import { useChatOpen } from "@/stores";
|
||||
import { ProjectSelector } from "./project-selector";
|
||||
import { useTour } from "./tour";
|
||||
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory";
|
||||
import { IntegrationsView } from "./views/integrations";
|
||||
import { ProfileView } from "./views/profile";
|
||||
} 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"
|
||||
import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react"
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { Drawer } from "vaul"
|
||||
import { useMobilePanel } from "@/lib/mobile-panel-context"
|
||||
import { TOUR_STEP_IDS } from "@/lib/tour-constants"
|
||||
import { useChatOpen } from "@/stores"
|
||||
import { ProjectSelector } from "./project-selector"
|
||||
import { useTour } from "./tour"
|
||||
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
|
||||
import { IntegrationsView } from "./views/integrations"
|
||||
import { ProfileView } from "./views/profile"
|
||||
|
||||
const MCPIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
|
|
@ -36,45 +37,67 @@ const MCPIcon = ({ className }: { className?: string }) => {
|
|||
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
|
||||
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
function Menu({ id }: { id?: string }) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const openParam = searchParams.get("open")
|
||||
|
||||
// Valid view names that can be opened via URL parameter
|
||||
const validViews = [
|
||||
"addUrl",
|
||||
"mcp",
|
||||
"projects",
|
||||
"profile",
|
||||
"integrations",
|
||||
] as const
|
||||
type ValidView = (typeof validViews)[number]
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [expandedView, setExpandedView] = useState<
|
||||
"addUrl" | "mcp" | "projects" | "profile" | "integrations" | null
|
||||
>(null);
|
||||
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();
|
||||
const autumn = useCustomer();
|
||||
const { setIsOpen } = useChatOpen();
|
||||
>(null)
|
||||
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()
|
||||
const autumn = useCustomer()
|
||||
const { setIsOpen } = useChatOpen()
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn);
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn)
|
||||
|
||||
useEffect(() => {
|
||||
if (memoriesCheck) {
|
||||
console.log({ memoriesCheck });
|
||||
console.log({ memoriesCheck })
|
||||
}
|
||||
|
||||
if (proCheck) {
|
||||
console.log({ proCheck });
|
||||
console.log({ proCheck })
|
||||
}
|
||||
}, [memoriesCheck, proCheck]);
|
||||
}, [memoriesCheck, proCheck])
|
||||
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
// Function to clear the 'open' parameter from URL
|
||||
const clearOpenParam = useCallback(() => {
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString())
|
||||
newSearchParams.delete("open")
|
||||
const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`
|
||||
router.replace(newUrl)
|
||||
}, [searchParams, router])
|
||||
|
||||
const isProUser = proCheck?.allowed ?? false
|
||||
|
||||
const shouldShowLimitWarning =
|
||||
!isProUser && memoriesUsed >= memoriesLimit * 0.8;
|
||||
!isProUser && memoriesUsed >= memoriesLimit * 0.8
|
||||
|
||||
// Map menu item keys to tour IDs
|
||||
const menuItemTourIds: Record<string, string> = {
|
||||
|
|
@ -82,7 +105,7 @@ function Menu({ id }: { id?: string }) {
|
|||
projects: TOUR_STEP_IDS.MENU_PROJECTS,
|
||||
mcp: TOUR_STEP_IDS.MENU_MCP,
|
||||
integrations: "", // No tour ID for integrations yet
|
||||
};
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
|
|
@ -115,61 +138,102 @@ function Menu({ id }: { id?: string }) {
|
|||
key: "profile" as const,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const handleMenuItemClick = (
|
||||
key: "chat" | "addUrl" | "mcp" | "projects" | "profile" | "integrations",
|
||||
) => {
|
||||
if (key === "chat") {
|
||||
setIsOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
setIsOpen(true)
|
||||
setIsMobileMenuOpen(false)
|
||||
if (isMobile) {
|
||||
setActivePanel("chat");
|
||||
setActivePanel("chat")
|
||||
}
|
||||
} else if (key === "mcp") {
|
||||
// Open ConnectAIModal directly for MCP
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setShowConnectAIModal(true);
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setShowConnectAIModal(true)
|
||||
} else {
|
||||
if (expandedView === key) {
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
} else if (key === "addUrl") {
|
||||
setShowAddMemoryView(true);
|
||||
setExpandedView(null);
|
||||
setShowAddMemoryView(true)
|
||||
setExpandedView(null)
|
||||
} else {
|
||||
setExpandedView(key);
|
||||
setExpandedView(key)
|
||||
}
|
||||
if (isMobile) {
|
||||
setActivePanel("menu");
|
||||
setActivePanel("menu")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle initial view opening based on URL parameter
|
||||
useEffect(() => {
|
||||
if (openParam) {
|
||||
if (openParam === "chat") {
|
||||
setIsOpen(true)
|
||||
setIsMobileMenuOpen(false)
|
||||
if (isMobile) {
|
||||
setActivePanel("chat")
|
||||
}
|
||||
} else if (openParam === "mcp") {
|
||||
// Open ConnectAIModal directly for MCP
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setShowConnectAIModal(true)
|
||||
} else if (openParam === "addUrl") {
|
||||
setShowAddMemoryView(true)
|
||||
setExpandedView(null)
|
||||
if (isMobile) {
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
}
|
||||
} else if (validViews.includes(openParam as ValidView)) {
|
||||
// For other valid views like "profile", "integrations"
|
||||
setExpandedView(openParam as ValidView)
|
||||
if (isMobile) {
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the parameter from URL after performing any action
|
||||
clearOpenParam()
|
||||
}
|
||||
}, [
|
||||
openParam,
|
||||
isMobile,
|
||||
setIsOpen,
|
||||
setActivePanel,
|
||||
validViews,
|
||||
clearOpenParam,
|
||||
])
|
||||
|
||||
// Watch for active panel changes on mobile
|
||||
useEffect(() => {
|
||||
if (isMobile && activePanel !== "menu" && activePanel !== null) {
|
||||
// Another panel became active, close the menu
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
}
|
||||
}, [isMobile, activePanel]);
|
||||
}, [isMobile, activePanel])
|
||||
|
||||
// Notify tour provider about expansion state changes
|
||||
useEffect(() => {
|
||||
const isExpanded = isMobile
|
||||
? isMobileMenuOpen || !!expandedView
|
||||
: isHovered || !!expandedView;
|
||||
setMenuExpanded(isExpanded);
|
||||
}, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]);
|
||||
: isHovered || !!expandedView
|
||||
setMenuExpanded(isExpanded)
|
||||
}, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded])
|
||||
|
||||
// Calculate width based on state
|
||||
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56;
|
||||
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56
|
||||
|
||||
// Dynamic z-index for mobile based on active panel
|
||||
const mobileZIndex =
|
||||
isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]";
|
||||
const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -378,8 +442,8 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -418,9 +482,9 @@ function Menu({ id }: { id?: string }) {
|
|||
open={isMobileMenuOpen || !!expandedView}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setActivePanel(null);
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setActivePanel(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -433,8 +497,8 @@ function Menu({ id }: { id?: string }) {
|
|||
className="w-14 h-14 flex items-center justify-center text-white rounded-full shadow-2xl"
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(true);
|
||||
setActivePanel("menu");
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
|
|
@ -530,13 +594,13 @@ function Menu({ id }: { id?: string }) {
|
|||
initial={{ opacity: 0, y: 10 }}
|
||||
layout
|
||||
onClick={() => {
|
||||
handleMenuItemClick(item.key);
|
||||
handleMenuItemClick(item.key)
|
||||
if (
|
||||
item.key !== "mcp" &&
|
||||
item.key !== "profile" &&
|
||||
item.key !== "integrations"
|
||||
) {
|
||||
setIsMobileMenuOpen(false);
|
||||
setIsMobileMenuOpen(false)
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
|
|
@ -600,8 +664,8 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -643,7 +707,7 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button className="hidden">Connect AI Assistant</Button>
|
||||
</ConnectAIModal>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu;
|
||||
export default Menu
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import {
|
||||
fetchConnectionsFeature,
|
||||
fetchMemoriesFeature,
|
||||
fetchSubscriptionStatus,
|
||||
} from "@lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
} from "@lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import {
|
||||
CheckCircle,
|
||||
CreditCard,
|
||||
|
|
@ -17,27 +17,27 @@ import {
|
|||
LogOut,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
} from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
export function ProfileView() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { user: session, org } = useAuth();
|
||||
const organizations = org;
|
||||
const autumn = useCustomer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { user: session, org } = useAuth()
|
||||
const organizations = org
|
||||
const autumn = useCustomer()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
|
||||
const connectionsUsed = connectionsCheck?.usage ?? 0;
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
|
||||
const connectionsUsed = connectionsCheck?.usage ?? 0
|
||||
|
||||
// Fetch subscription status with React Query
|
||||
const {
|
||||
|
|
@ -45,36 +45,36 @@ export function ProfileView() {
|
|||
consumer_pro: null,
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn as any);
|
||||
} = fetchSubscriptionStatus(autumn as any)
|
||||
|
||||
const isPro = status.consumer_pro;
|
||||
const isPro = status.consumer_pro
|
||||
|
||||
const handleLogout = () => {
|
||||
analytics.userSignedOut();
|
||||
authClient.signOut();
|
||||
router.push("/login");
|
||||
};
|
||||
analytics.userSignedOut()
|
||||
authClient.signOut()
|
||||
router.push("/login")
|
||||
}
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
setIsLoading(true);
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
});
|
||||
window.location.reload();
|
||||
})
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsLoading(false);
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle manage billing
|
||||
const handleManageBilling = async () => {
|
||||
await autumn.openBillingPortal({
|
||||
returnUrl: "https://app.supermemory.ai",
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
if (session?.isAnonymous) {
|
||||
return (
|
||||
|
|
@ -99,7 +99,7 @@ export function ProfileView() {
|
|||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -282,5 +282,5 @@ export function ProfileView() {
|
|||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
import { $fetch } from "@lib/api";
|
||||
import { getSessionCookie } from "better-auth/cookies";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSessionCookie } from "better-auth/cookies"
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
export default async function middleware(request: Request) {
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE START ===");
|
||||
const url = new URL(request.url);
|
||||
console.debug("[MIDDLEWARE] Path:", url.pathname);
|
||||
console.debug("[MIDDLEWARE] Method:", request.method);
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE START ===")
|
||||
const url = new URL(request.url)
|
||||
console.debug("[MIDDLEWARE] Path:", url.pathname)
|
||||
console.debug("[MIDDLEWARE] Method:", request.method)
|
||||
|
||||
const sessionCookie = getSessionCookie(request);
|
||||
console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie);
|
||||
const sessionCookie = getSessionCookie(request)
|
||||
console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie)
|
||||
|
||||
// Always allow access to login and waitlist pages
|
||||
const publicPaths = ["/login"];
|
||||
const publicPaths = ["/login"]
|
||||
if (publicPaths.includes(url.pathname)) {
|
||||
console.debug("[MIDDLEWARE] Public path, allowing access");
|
||||
return NextResponse.next();
|
||||
console.debug("[MIDDLEWARE] Public path, allowing access")
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// If no session cookie and not on a public path, redirect to login
|
||||
if (!sessionCookie) {
|
||||
console.debug(
|
||||
"[MIDDLEWARE] No session cookie and not on public path, redirecting to /login",
|
||||
);
|
||||
return NextResponse.redirect(new URL("/login", request.url));
|
||||
)
|
||||
const url = new URL("/login", request.url)
|
||||
url.searchParams.set("redirect", request.url)
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// TEMPORARILY DISABLED: Waitlist check
|
||||
|
|
@ -39,13 +40,13 @@ export default async function middleware(request: Request) {
|
|||
// }
|
||||
// }
|
||||
|
||||
console.debug("[MIDDLEWARE] Passing through to next handler");
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE END ===");
|
||||
return NextResponse.next();
|
||||
console.debug("[MIDDLEWARE] Passing through to next handler")
|
||||
console.debug("[MIDDLEWARE] === MIDDLEWARE END ===")
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|ingest|api|login|api/emails).*)",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { signIn } from "@lib/auth"
|
||||
import { usePostHog } from "@lib/posthog"
|
||||
import { LogoFull } from "@repo/ui/assets/Logo"
|
||||
import { TextSeparator } from "@repo/ui/components/text-separator"
|
||||
import { ExternalAuthButton } from "@ui/button/external-auth"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Badge } from "@ui/components/badge"
|
||||
import { signIn } from "@lib/auth";
|
||||
import { usePostHog } from "@lib/posthog";
|
||||
import { LogoFull } from "@repo/ui/assets/Logo";
|
||||
import { TextSeparator } from "@repo/ui/components/text-separator";
|
||||
import { ExternalAuthButton } from "@ui/button/external-auth";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { Badge } from "@ui/components/badge";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@ui/components/carousel"
|
||||
import { LabeledInput } from "@ui/input/labeled-input"
|
||||
import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium"
|
||||
import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium"
|
||||
import { Label1Regular } from "@ui/text/label/label-1-regular"
|
||||
import { Title1Bold } from "@ui/text/title/title-1-bold"
|
||||
import Autoplay from "embla-carousel-autoplay"
|
||||
import Image from "next/image"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useState, useEffect } from "react"
|
||||
} from "@ui/components/carousel";
|
||||
import { LabeledInput } from "@ui/input/labeled-input";
|
||||
import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium";
|
||||
import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium";
|
||||
import { Label1Regular } from "@ui/text/label/label-1-regular";
|
||||
import { Title1Bold } from "@ui/text/title/title-1-bold";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import Image from "next/image";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function LoginPage({
|
||||
heroText = "The unified memory API for the AI era.",
|
||||
|
|
@ -29,101 +29,122 @@ export function LoginPage({
|
|||
"Trusted by Open Source, enterprise and developers.",
|
||||
],
|
||||
}) {
|
||||
const [email, setEmail] = useState("")
|
||||
const [submittedEmail, setSubmittedEmail] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isLoadingEmail, setIsLoadingEmail] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastUsedMethod, setLastUsedMethod] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState("");
|
||||
const [submittedEmail, setSubmittedEmail] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingEmail, setIsLoadingEmail] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUsedMethod, setLastUsedMethod] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const posthog = usePostHog()
|
||||
const posthog = usePostHog();
|
||||
|
||||
const params = useSearchParams()
|
||||
const params = useSearchParams();
|
||||
|
||||
// Get redirect URL from query params
|
||||
const redirectUrl = params.get("redirect");
|
||||
|
||||
// Create callback URL that includes redirect parameter if provided
|
||||
const getCallbackURL = () => {
|
||||
const origin = window.location.origin;
|
||||
if (redirectUrl) {
|
||||
// Validate that the redirect URL is safe (same origin or allow external based on your security requirements)
|
||||
try {
|
||||
const url = new URL(redirectUrl, origin);
|
||||
return url.toString();
|
||||
} catch {
|
||||
// If redirect URL is invalid, fall back to origin
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
return origin;
|
||||
};
|
||||
|
||||
// Load last used method from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedMethod = localStorage.getItem('supermemory-last-login-method')
|
||||
setLastUsedMethod(savedMethod)
|
||||
}, [])
|
||||
const savedMethod = localStorage.getItem("supermemory-last-login-method");
|
||||
setLastUsedMethod(savedMethod);
|
||||
}, []);
|
||||
|
||||
// Record the pending login method (will be committed after successful auth)
|
||||
function setPendingLoginMethod(method: string) {
|
||||
try {
|
||||
localStorage.setItem('supermemory-pending-login-method', method)
|
||||
localStorage.setItem('supermemory-pending-login-timestamp', String(Date.now()))
|
||||
} catch { }
|
||||
localStorage.setItem("supermemory-pending-login-method", method);
|
||||
localStorage.setItem(
|
||||
"supermemory-pending-login-timestamp",
|
||||
String(Date.now()),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If we land back on this page with an error, clear any pending marker
|
||||
useEffect(() => {
|
||||
if (params.get("error")) {
|
||||
try {
|
||||
localStorage.removeItem('supermemory-pending-login-method')
|
||||
localStorage.removeItem('supermemory-pending-login-timestamp')
|
||||
} catch { }
|
||||
localStorage.removeItem("supermemory-pending-login-method");
|
||||
localStorage.removeItem("supermemory-pending-login-timestamp");
|
||||
} catch {}
|
||||
}
|
||||
}, [params])
|
||||
|
||||
|
||||
}, [params]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setIsLoadingEmail(true)
|
||||
setError(null)
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setIsLoadingEmail(true);
|
||||
setError(null);
|
||||
|
||||
// Track login attempt
|
||||
posthog.capture("login_attempt", {
|
||||
method: "magic_link",
|
||||
email_domain: email.split("@")[1] || "unknown",
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
await signIn.magicLink({
|
||||
callbackURL: window.location.origin,
|
||||
callbackURL: getCallbackURL(),
|
||||
email,
|
||||
})
|
||||
setSubmittedEmail(email)
|
||||
setPendingLoginMethod('magic_link')
|
||||
});
|
||||
setSubmittedEmail(email);
|
||||
setPendingLoginMethod("magic_link");
|
||||
// Track successful magic link send
|
||||
posthog.capture("login_magic_link_sent", {
|
||||
email_domain: email.split("@")[1] || "unknown",
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
|
||||
// Track login failure
|
||||
posthog.capture("login_failed", {
|
||||
method: "magic_link",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
email_domain: email.split("@")[1] || "unknown",
|
||||
})
|
||||
});
|
||||
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to send login link. Please try again.",
|
||||
)
|
||||
setIsLoading(false)
|
||||
setIsLoadingEmail(false)
|
||||
return
|
||||
);
|
||||
setIsLoading(false);
|
||||
setIsLoadingEmail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
setIsLoadingEmail(false)
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsLoadingEmail(false);
|
||||
};
|
||||
|
||||
const handleSubmitToken = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setIsLoading(true)
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
const formData = new FormData(event.currentTarget)
|
||||
const token = formData.get("token") as string
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const token = formData.get("token") as string;
|
||||
const callbackURL = getCallbackURL();
|
||||
router.push(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(window.location.host)}`,
|
||||
)
|
||||
}
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(callbackURL)}`,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="min-h-screen flex flex-col lg:grid lg:grid-cols-12 items-center justify-center p-4 sm:p-6 md:p-8 lg:px-[5rem] lg:py-[3.125rem] gap-6 lg:gap-[5rem] max-w-[400rem] mx-auto">
|
||||
|
|
@ -225,8 +246,8 @@ export function LoginPage({
|
|||
disabled: isLoading,
|
||||
id: "email",
|
||||
onChange: (e) => {
|
||||
setEmail(e.target.value)
|
||||
error && setError(null)
|
||||
setEmail(e.target.value);
|
||||
error && setError(null);
|
||||
},
|
||||
required: true,
|
||||
value: email,
|
||||
|
|
@ -241,9 +262,11 @@ export function LoginPage({
|
|||
? "Sending login link..."
|
||||
: "Log in to supermemory"}
|
||||
</Button>
|
||||
{lastUsedMethod === 'magic_link' && (
|
||||
{lastUsedMethod === "magic_link" && (
|
||||
<div className="absolute -top-2 -right-2">
|
||||
<Badge variant="default" className="text-xs">Last used</Badge>
|
||||
<Badge variant="default" className="text-xs">
|
||||
Last used
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -251,14 +274,14 @@ export function LoginPage({
|
|||
</form>
|
||||
|
||||
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
|
||||
!process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ||
|
||||
!process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
|
||||
!process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ||
|
||||
!process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
|
||||
<TextSeparator text="OR" />
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col sm:flex-row flex-wrap gap-3 lg:gap-4">
|
||||
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
|
||||
!process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? (
|
||||
!process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? (
|
||||
<div className="relative flex-grow">
|
||||
<ExternalAuthButton
|
||||
authIcon={
|
||||
|
|
@ -293,32 +316,34 @@ export function LoginPage({
|
|||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
if (isLoading) return
|
||||
setIsLoading(true)
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
posthog.capture("login_attempt", {
|
||||
method: "social",
|
||||
provider: "google",
|
||||
})
|
||||
setPendingLoginMethod('google')
|
||||
});
|
||||
setPendingLoginMethod("google");
|
||||
signIn
|
||||
.social({
|
||||
callbackURL: window.location.origin,
|
||||
callbackURL: getCallbackURL(),
|
||||
provider: "google",
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
setIsLoading(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{lastUsedMethod === 'google' && (
|
||||
{lastUsedMethod === "google" && (
|
||||
<div className="absolute -top-2 -right-2">
|
||||
<Badge variant="default" className="text-xs">Last used</Badge>
|
||||
<Badge variant="default" className="text-xs">
|
||||
Last used
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
|
||||
!process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
|
||||
!process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
|
||||
<div className="relative flex-grow">
|
||||
<ExternalAuthButton
|
||||
authIcon={
|
||||
|
|
@ -355,26 +380,28 @@ export function LoginPage({
|
|||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
if (isLoading) return
|
||||
setIsLoading(true)
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
posthog.capture("login_attempt", {
|
||||
method: "social",
|
||||
provider: "github",
|
||||
})
|
||||
setPendingLoginMethod('github')
|
||||
});
|
||||
setPendingLoginMethod("github");
|
||||
signIn
|
||||
.social({
|
||||
callbackURL: window.location.origin,
|
||||
callbackURL: getCallbackURL(),
|
||||
provider: "github",
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
setIsLoading(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{lastUsedMethod === 'github' && (
|
||||
{lastUsedMethod === "github" && (
|
||||
<div className="absolute -top-2 -right-2">
|
||||
<Badge variant="default" className="text-xs">Last used</Badge>
|
||||
<Badge variant="default" className="text-xs">
|
||||
Last used
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -403,5 +430,5 @@ export function LoginPage({
|
|||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue