diff --git a/apps/web/app/(auth)/login/new/page.tsx b/apps/web/app/(auth)/login/new/page.tsx new file mode 100644 index 00000000..3589d6a1 --- /dev/null +++ b/apps/web/app/(auth)/login/new/page.tsx @@ -0,0 +1,479 @@ +"use client" + +import { signIn } from "@lib/auth" +import { usePostHog } from "@lib/posthog" +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 { LabeledInput } from "@ui/input/labeled-input" +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 { InitialHeader } from "@/components/initial-header" +import { useRouter, useSearchParams } from "next/navigation" +import { useState, useEffect } from "react" +import { motion } from "framer-motion" +import { dmSansClassName } from "@/utils/fonts" +import { cn } from "@lib/utils" +import { Logo } from "@ui/assets/Logo" + +function AnimatedGradientBackground() { + return ( +
+ + + +
+ ) +} + +function LoginCard({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +export default function LoginPage() { + const [email, setEmail] = useState("") + const [submittedEmail, setSubmittedEmail] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isLoadingEmail, setIsLoadingEmail] = useState(false) + const [error, setError] = useState(null) + const [lastUsedMethod, setLastUsedMethod] = useState(null) + const router = useRouter() + + const posthog = usePostHog() + + 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 + let finalUrl: URL + + if (redirectUrl) { + try { + finalUrl = new URL(redirectUrl, origin) + } catch { + finalUrl = new URL(origin) + } + } else { + finalUrl = new URL(origin) + } + + finalUrl.searchParams.set("extension-auth-success", "true") + return finalUrl.toString() + } + + // Load last used method from localStorage on mount + useEffect(() => { + 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 {} + } + + function isNetworkError(error: unknown): boolean { + if (!(error instanceof Error)) return false + const message = error.message.toLowerCase() + return ( + message.includes("load failed") || + message.includes("networkerror") || + message.includes("failed to fetch") || + message.includes("network request failed") + ) + } + + function getErrorMessage(error: unknown): string { + if (isNetworkError(error)) { + return "Network error. Please check your connection and try again." + } + if (error instanceof Error) { + return error.message + } + return "An unexpected error occurred. Please try again." + } + + // 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 {} + } + }, [params]) + + const handleSubmit = async (e: React.FormEvent) => { + 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: getCallbackURL(), + email, + }) + 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) + + // Track login failure + posthog.capture("login_failed", { + method: "magic_link", + error: error instanceof Error ? error.message : "Unknown error", + email_domain: email.split("@")[1] || "unknown", + is_network_error: isNetworkError(error), + }) + + setError(getErrorMessage(error)) + setIsLoading(false) + setIsLoadingEmail(false) + return + } + + setIsLoading(false) + setIsLoadingEmail(false) + } + + const handleSubmitToken = async (event: React.FormEvent) => { + event.preventDefault() + setIsLoading(true) + + 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(callbackURL)}`, + ) + } + + return ( +
+ +
+ +
+
+
+ Never forget anything, anywhere +
+
with supermemory
+
+ {submittedEmail ? ( + +
+
+ + Almost there! + + + Click the magic link we've sent to{" "} + {submittedEmail}. + +
+ + + +
+ + + + +
+
+ ) : ( + +
+ {params.get("error") && ( +
+ Error: {params.get("error")}. Please try again! +
+ )} + +
+ {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( +
+ + Google + + + + + + } + authProvider="Google" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", + provider: "google", + }) + setPendingLoginMethod("google") + signIn + .social({ + callbackURL: getCallbackURL(), + provider: "google", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === "google" && ( +
+ + Last used + +
+ )} +
+ ) : null} + {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( +
+ + Github + + + + + + + + + + } + authProvider="Github" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", + provider: "github", + }) + setPendingLoginMethod("github") + signIn + .social({ + callbackURL: getCallbackURL(), + provider: "github", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === "github" && ( +
+ + Last used + +
+ )} +
+ ) : null} +
+ + + +
+
+ { + setEmail(e.target.value) + error && setError(null) + }, + required: true, + value: email, + }} + inputType="email" + /> + +
+ + {lastUsedMethod === "magic_link" && ( +
+ + Last used + +
+ )} +
+ + + + By continuing, you agree to our{" "} + + + Terms + {" "} + and{" "} + + Privacy Policy + + . + + +
+
+
+ )} +
+
+
+ ) +} diff --git a/apps/web/app/(navigation)/page.tsx b/apps/web/app/(navigation)/page.tsx index 7ad84caf..73da4f3c 100644 --- a/apps/web/app/(navigation)/page.tsx +++ b/apps/web/app/(navigation)/page.tsx @@ -10,12 +10,14 @@ import { ChromeExtensionButton } from "@/components/chrome-extension-button" import { ChatInput } from "@/components/chat-input" import { BackgroundPlus } from "@ui/components/grid-plus" import { Memories } from "@/components/memories" +import { useFeatureFlagEnabled } from "posthog-js/react" export default function Page() { const { user, session } = useAuth() const { shouldShowOnboarding, isLoading: onboardingLoading } = useOnboardingStorage() const router = useRouter() + const flagEnabled = useFeatureFlagEnabled("nova-alpha-access") useEffect(() => { const url = new URL(window.location.href) @@ -33,7 +35,10 @@ export default function Page() { if (sessionToken && userData?.email) { const encodedToken = encodeURIComponent(sessionToken) - window.postMessage({ token: encodedToken, userData }, window.location.origin) + window.postMessage( + { token: encodedToken, userData }, + window.location.origin, + ) url.searchParams.delete("extension-auth-success") window.history.replaceState({}, "", url.toString()) } @@ -42,9 +47,13 @@ export default function Page() { useEffect(() => { if (user && !onboardingLoading && shouldShowOnboarding()) { - router.push("/onboarding?step=input&flow=welcome") + if (flagEnabled) { + router.push("/new/onboarding?step=input&flow=welcome") + } else { + router.push("/onboarding") + } } - }, [user, shouldShowOnboarding, onboardingLoading, router]) + }, [user, shouldShowOnboarding, onboardingLoading, router, flagEnabled]) if (!user || onboardingLoading) { return ( diff --git a/apps/web/app/new/layout.tsx b/apps/web/app/new/layout.tsx new file mode 100644 index 00000000..e761fea2 --- /dev/null +++ b/apps/web/app/new/layout.tsx @@ -0,0 +1,22 @@ +"use client" + +import { useEffect } from "react" +import { useFeatureFlagEnabled } from "posthog-js/react" +import { useRouter } from "next/navigation" + +export default function NewLayout({ children }: { children: React.ReactNode }) { + const router = useRouter() + const flagEnabled = useFeatureFlagEnabled("nova-alpha-access") + + useEffect(() => { + if (!flagEnabled) { + router.push("/") + } + }, [flagEnabled, router]) + + if (!flagEnabled) { + return null + } + + return <>{children} +} diff --git a/apps/web/app/new/onboarding/page.tsx b/apps/web/app/new/onboarding/page.tsx new file mode 100644 index 00000000..57b5b4fb --- /dev/null +++ b/apps/web/app/new/onboarding/page.tsx @@ -0,0 +1,267 @@ +"use client" + +import { useSearchParams } from "next/navigation" +import { motion, AnimatePresence } from "motion/react" +import { useState, useEffect } from "react" +import { useAuth } from "@lib/auth-context" +import { cn } from "@lib/utils" + +import { InputStep } from "./welcome/input-step" +import { GreetingStep } from "./welcome/greeting-step" +import { WelcomeStep } from "./welcome/welcome-step" +import { ContinueStep } from "./welcome/continue-step" +import { FeaturesStep } from "./welcome/features-step" +import { MemoriesStep } from "./welcome/memories-step" +import { RelatableQuestion } from "./setup/relatable-question" +import { IntegrationsStep } from "./setup/integrations-step" + +import { InitialHeader } from "@/components/initial-header" +import { SetupHeader } from "./setup/header" +import { ChatSidebar } from "./setup/chat-sidebar" +import { Logo } from "@ui/assets/Logo" +import NovaOrb from "@/components/nova/nova-orb" +import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background" + +function UserSupermemory({ name }: { name: string }) { + return ( + + +
+

+ {name.split(" ")[0]}'s +

+

+ supermemory +

+
+
+ ) +} + +export default function OnboardingPage() { + const searchParams = useSearchParams() + const { user } = useAuth() + + const flow = searchParams.get("flow") as "welcome" | "setup" | null + const step = searchParams.get("step") as string | null + + const [name, setName] = useState(user?.name ?? "") + const [isSubmitting, setIsSubmitting] = useState(false) + const [memoryFormData, setMemoryFormData] = useState<{ + twitter: string + linkedin: string + description: string + otherLinks: string[] + } | null>(null) + const [showWelcomeContent, setShowWelcomeContent] = useState(false) + + const currentFlow = flow || "welcome" + const currentStep = step || "input" + + useEffect(() => { + if (user?.name) { + setName(user.name) + localStorage.setItem("username", user.name) + } + }, [user?.name]) + + useEffect(() => { + if (currentFlow === "welcome" && currentStep === "input") { + setShowWelcomeContent(false) + const timer = setTimeout(() => { + setShowWelcomeContent(true) + }, 1250) + return () => clearTimeout(timer) + } + }, [currentFlow, currentStep]) + + useEffect(() => { + if (currentFlow !== "welcome") return + + const timers: NodeJS.Timeout[] = [] + + switch (currentStep) { + case "greeting": + timers.push( + setTimeout(() => { + // Auto-advance to welcome step + window.history.replaceState( + null, + "", + "/new/onboarding?flow=welcome&step=welcome", + ) + }, 2000), + ) + break + case "welcome": + timers.push( + setTimeout(() => { + // Auto-advance to username step + window.history.replaceState( + null, + "", + "/new/onboarding?flow=welcome&step=username", + ) + }, 2000), + ) + break + } + + return () => { + timers.forEach(clearTimeout) + } + }, [currentStep, currentFlow]) + + const handleSubmit = () => { + localStorage.setItem("username", name) + if (name.trim()) { + setIsSubmitting(true) + window.history.replaceState( + null, + "", + "/new/onboarding?flow=welcome&step=greeting", + ) + setIsSubmitting(false) + } + } + + const renderWelcomeStep = () => { + switch (currentStep) { + case "input": + return ( + + ) + case "greeting": + return + case "welcome": + return + case "username": + return + case "features": + return + case "memories": + return + default: + return null + } + } + + const renderSetupStep = () => { + switch (currentStep) { + case "relatable": + return + case "integrations": + return + default: + return null + } + } + + const isWelcomeFlow = currentFlow === "welcome" + const isSetupFlow = currentFlow === "setup" + + const minimizeNovaOrb = + isWelcomeFlow && ["features", "memories"].includes(currentStep) + const novaSize = currentStep === "memories" ? 150 : 300 + + const showUserSupermemory = isWelcomeFlow && currentStep === "username" + + return ( +
+ {isWelcomeFlow && ( + + )} + {isSetupFlow && } + + {isSetupFlow && } + + {isWelcomeFlow && currentStep === "input" && ( + + )} + + {isWelcomeFlow && showWelcomeContent && ( +
+ + + + + {showUserSupermemory && } + + + {renderWelcomeStep()} + +
+ )} + + {isSetupFlow && ( +
+
+
+
+ + {renderSetupStep()} + +
+ + + + +
+
+
+ )} +
+ ) +} diff --git a/apps/web/app/onboarding/setup/chat-sidebar.tsx b/apps/web/app/new/onboarding/setup/chat-sidebar.tsx similarity index 97% rename from apps/web/app/onboarding/setup/chat-sidebar.tsx rename to apps/web/app/new/onboarding/setup/chat-sidebar.tsx index 973d31ae..d35ce73d 100644 --- a/apps/web/app/onboarding/setup/chat-sidebar.tsx +++ b/apps/web/app/new/onboarding/setup/chat-sidebar.tsx @@ -274,12 +274,12 @@ export function ChatSidebar({ formData }: ChatSidebarProps) { > - + Chat with Nova @@ -314,7 +314,7 @@ export function ChatSidebar({ formData }: ChatSidebarProps) { > {msg.type === "waiting" ? (
- + {msg.message}
) : ( @@ -328,7 +328,7 @@ export function ChatSidebar({ formData }: ChatSidebarProps) { {i === 0 && (
)} -
+
{msg.type === "memory" && (
@@ -376,13 +376,13 @@ export function ChatSidebar({ formData }: ChatSidebarProps) { ))} {messages.length === 0 && !isLoading && !formData && (
- + Waiting for your input
)} {isLoading && (
- + Fetching your memories...
)} diff --git a/apps/web/app/onboarding/setup/header.tsx b/apps/web/app/new/onboarding/setup/header.tsx similarity index 100% rename from apps/web/app/onboarding/setup/header.tsx rename to apps/web/app/new/onboarding/setup/header.tsx diff --git a/apps/web/app/onboarding/setup/integrations-step.tsx b/apps/web/app/new/onboarding/setup/integrations-step.tsx similarity index 97% rename from apps/web/app/onboarding/setup/integrations-step.tsx rename to apps/web/app/new/onboarding/setup/integrations-step.tsx index c9d67861..ff1ce96f 100644 --- a/apps/web/app/onboarding/setup/integrations-step.tsx +++ b/apps/web/app/new/onboarding/setup/integrations-step.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { Button } from "@ui/components/button" -import { MCPDetailView } from "@/components/mcp-detail-view" +import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view" import { XBookmarksDetailView } from "@/components/x-bookmarks-detail-view" import { useRouter } from "next/navigation" import { cn } from "@lib/utils" @@ -164,7 +164,7 @@ export function IntegrationsStep() { diff --git a/apps/web/app/onboarding/setup/relatable-question.tsx b/apps/web/app/new/onboarding/setup/relatable-question.tsx similarity index 98% rename from apps/web/app/onboarding/setup/relatable-question.tsx rename to apps/web/app/new/onboarding/setup/relatable-question.tsx index 5a2d344d..c853985d 100644 --- a/apps/web/app/onboarding/setup/relatable-question.tsx +++ b/apps/web/app/new/onboarding/setup/relatable-question.tsx @@ -35,7 +35,7 @@ export function RelatableQuestion() { const [selectedOptions, setSelectedOptions] = useState([]) const handleContinueOrSkip = () => { - router.push("/onboarding?flow=setup&step=integrations") + router.push("/new/onboarding?flow=setup&step=integrations") } return ( diff --git a/apps/web/app/onboarding/welcome/continue-step.tsx b/apps/web/app/new/onboarding/welcome/continue-step.tsx similarity index 94% rename from apps/web/app/onboarding/welcome/continue-step.tsx rename to apps/web/app/new/onboarding/welcome/continue-step.tsx index 0f47dcbd..eefab753 100644 --- a/apps/web/app/onboarding/welcome/continue-step.tsx +++ b/apps/web/app/new/onboarding/welcome/continue-step.tsx @@ -8,7 +8,7 @@ export function ContinueStep() { const router = useRouter() const handleContinue = () => { - router.push("/onboarding?flow=welcome&step=features") + router.push("/new/onboarding?flow=welcome&step=features") } return ( diff --git a/apps/web/app/onboarding/welcome/features-step.tsx b/apps/web/app/new/onboarding/welcome/features-step.tsx similarity index 97% rename from apps/web/app/onboarding/welcome/features-step.tsx rename to apps/web/app/new/onboarding/welcome/features-step.tsx index 094afa4e..6d15e2f8 100644 --- a/apps/web/app/onboarding/welcome/features-step.tsx +++ b/apps/web/app/new/onboarding/welcome/features-step.tsx @@ -8,7 +8,7 @@ export function FeaturesStep() { const router = useRouter() const handleContinue = () => { - router.push("/onboarding?flow=welcome&step=memories") + router.push("/new/onboarding?flow=welcome&step=memories") } return ( l.trim()), } onSubmit(formData) - router.push("/onboarding?flow=setup&step=relatable") + router.push("/new/onboarding?flow=setup&step=relatable") }} > {isSubmitting ? "Fetching..." : "Remember this →"} diff --git a/apps/web/app/onboarding/welcome/welcome-step.tsx b/apps/web/app/new/onboarding/welcome/welcome-step.tsx similarity index 100% rename from apps/web/app/onboarding/welcome/welcome-step.tsx rename to apps/web/app/new/onboarding/welcome/welcome-step.tsx diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx index cd270d89..6e4f2cd2 100644 --- a/apps/web/app/new/page.tsx +++ b/apps/web/app/new/page.tsx @@ -3,18 +3,19 @@ import { useState } from "react" import { Header } from "@/components/new/header" import { ChatSidebar } from "@/components/new/chat" -import { AnimatePresence } from "motion/react" import { MemoriesGrid } from "@/components/new/memories-grid" import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background" import { AddDocumentModal } from "@/components/new/add-document" import { MCPModal } from "@/components/new/mcp-modal" import { HotkeysProvider } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook" +import { AnimatePresence } from "framer-motion" export default function NewPage() { const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false) const [isMCPModalOpen, setIsMCPModalOpen] = useState(false) useHotkeys("c", () => setIsAddDocumentOpen(true)) + const [isChatOpen, setIsChatOpen] = useState(true) return ( @@ -28,14 +29,16 @@ export default function NewPage() { onOpenMCP={() => setIsMCPModalOpen(true)} />
-
+
- +
- - +
diff --git a/apps/web/app/onboarding-old/page.tsx b/apps/web/app/onboarding-old/page.tsx deleted file mode 100644 index dcf64ad0..00000000 --- a/apps/web/app/onboarding-old/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { getSession } from "@lib/auth" -import { OnboardingForm } from "./onboarding-form" -import { OnboardingProvider } from "./onboarding-context" -import { OnboardingProgressBar } from "./progress-bar" -import { redirect } from "next/navigation" -import { OnboardingBackground } from "./onboarding-background" -import type { Metadata } from "next" -export const metadata: Metadata = { - title: "Welcome to Supermemory", - description: "We're excited to have you on board.", -} - -export default function OnboardingPage() { - const session = getSession() - - if (!session) redirect("/login") - - return ( - - - - - - - ) -} diff --git a/apps/web/app/onboarding-old/animated-text.tsx b/apps/web/app/onboarding/animated-text.tsx similarity index 100% rename from apps/web/app/onboarding-old/animated-text.tsx rename to apps/web/app/onboarding/animated-text.tsx diff --git a/apps/web/app/onboarding-old/bio-form.tsx b/apps/web/app/onboarding/bio-form.tsx similarity index 100% rename from apps/web/app/onboarding-old/bio-form.tsx rename to apps/web/app/onboarding/bio-form.tsx diff --git a/apps/web/app/onboarding-old/extension-form.tsx b/apps/web/app/onboarding/extension-form.tsx similarity index 100% rename from apps/web/app/onboarding-old/extension-form.tsx rename to apps/web/app/onboarding/extension-form.tsx diff --git a/apps/web/app/onboarding-old/floating-orbs.tsx b/apps/web/app/onboarding/floating-orbs.tsx similarity index 100% rename from apps/web/app/onboarding-old/floating-orbs.tsx rename to apps/web/app/onboarding/floating-orbs.tsx diff --git a/apps/web/app/onboarding-old/intro.tsx b/apps/web/app/onboarding/intro.tsx similarity index 100% rename from apps/web/app/onboarding-old/intro.tsx rename to apps/web/app/onboarding/intro.tsx diff --git a/apps/web/app/onboarding-old/mcp-form.tsx b/apps/web/app/onboarding/mcp-form.tsx similarity index 100% rename from apps/web/app/onboarding-old/mcp-form.tsx rename to apps/web/app/onboarding/mcp-form.tsx diff --git a/apps/web/app/onboarding-old/name-form.tsx b/apps/web/app/onboarding/name-form.tsx similarity index 100% rename from apps/web/app/onboarding-old/name-form.tsx rename to apps/web/app/onboarding/name-form.tsx diff --git a/apps/web/app/onboarding-old/nav-menu.tsx b/apps/web/app/onboarding/nav-menu.tsx similarity index 100% rename from apps/web/app/onboarding-old/nav-menu.tsx rename to apps/web/app/onboarding/nav-menu.tsx diff --git a/apps/web/app/onboarding-old/onboarding-background.tsx b/apps/web/app/onboarding/onboarding-background.tsx similarity index 100% rename from apps/web/app/onboarding-old/onboarding-background.tsx rename to apps/web/app/onboarding/onboarding-background.tsx diff --git a/apps/web/app/onboarding-old/onboarding-context.tsx b/apps/web/app/onboarding/onboarding-context.tsx similarity index 100% rename from apps/web/app/onboarding-old/onboarding-context.tsx rename to apps/web/app/onboarding/onboarding-context.tsx diff --git a/apps/web/app/onboarding-old/onboarding-form.tsx b/apps/web/app/onboarding/onboarding-form.tsx similarity index 100% rename from apps/web/app/onboarding-old/onboarding-form.tsx rename to apps/web/app/onboarding/onboarding-form.tsx diff --git a/apps/web/app/onboarding/page.tsx b/apps/web/app/onboarding/page.tsx index f3d8a769..dcf64ad0 100644 --- a/apps/web/app/onboarding/page.tsx +++ b/apps/web/app/onboarding/page.tsx @@ -1,267 +1,26 @@ -"use client" - -import { useSearchParams } from "next/navigation" -import { motion, AnimatePresence } from "motion/react" -import { useState, useEffect } from "react" -import { useAuth } from "@lib/auth-context" -import { cn } from "@lib/utils" - -import { InputStep } from "./welcome/input-step" -import { GreetingStep } from "./welcome/greeting-step" -import { WelcomeStep } from "./welcome/welcome-step" -import { ContinueStep } from "./welcome/continue-step" -import { FeaturesStep } from "./welcome/features-step" -import { MemoriesStep } from "./welcome/memories-step" -import { RelatableQuestion } from "./setup/relatable-question" -import { IntegrationsStep } from "./setup/integrations-step" - -import { InitialHeader } from "@/components/initial-header" -import { SetupHeader } from "./setup/header" -import { ChatSidebar } from "./setup/chat-sidebar" -import { Logo } from "@ui/assets/Logo" -import NovaOrb from "@/components/nova/nova-orb" -import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background" - -function UserSupermemory({ name }: { name: string }) { - return ( - - -
-

- {name.split(" ")[0]}'s -

-

- supermemory -

-
-
- ) +import { getSession } from "@lib/auth" +import { OnboardingForm } from "./onboarding-form" +import { OnboardingProvider } from "./onboarding-context" +import { OnboardingProgressBar } from "./progress-bar" +import { redirect } from "next/navigation" +import { OnboardingBackground } from "./onboarding-background" +import type { Metadata } from "next" +export const metadata: Metadata = { + title: "Welcome to Supermemory", + description: "We're excited to have you on board.", } export default function OnboardingPage() { - const searchParams = useSearchParams() - const { user } = useAuth() + const session = getSession() - const flow = searchParams.get("flow") as "welcome" | "setup" | null - const step = searchParams.get("step") as string | null - - const [name, setName] = useState(user?.name ?? "") - const [isSubmitting, setIsSubmitting] = useState(false) - const [memoryFormData, setMemoryFormData] = useState<{ - twitter: string - linkedin: string - description: string - otherLinks: string[] - } | null>(null) - const [showWelcomeContent, setShowWelcomeContent] = useState(false) - - const currentFlow = flow || "welcome" - const currentStep = step || "input" - - useEffect(() => { - if (user?.name) { - setName(user.name) - localStorage.setItem("username", user.name) - } - }, [user?.name]) - - useEffect(() => { - if (currentFlow === "welcome" && currentStep === "input") { - setShowWelcomeContent(false) - const timer = setTimeout(() => { - setShowWelcomeContent(true) - }, 1250) - return () => clearTimeout(timer) - } - }, [currentFlow, currentStep]) - - useEffect(() => { - if (currentFlow !== "welcome") return - - const timers: NodeJS.Timeout[] = [] - - switch (currentStep) { - case "greeting": - timers.push( - setTimeout(() => { - // Auto-advance to welcome step - window.history.replaceState( - null, - "", - "/onboarding?flow=welcome&step=welcome", - ) - }, 2000), - ) - break - case "welcome": - timers.push( - setTimeout(() => { - // Auto-advance to username step - window.history.replaceState( - null, - "", - "/onboarding?flow=welcome&step=username", - ) - }, 2000), - ) - break - } - - return () => { - timers.forEach(clearTimeout) - } - }, [currentStep, currentFlow]) - - const handleSubmit = () => { - localStorage.setItem("username", name) - if (name.trim()) { - setIsSubmitting(true) - window.history.replaceState( - null, - "", - "/onboarding?flow=welcome&step=greeting", - ) - setIsSubmitting(false) - } - } - - const renderWelcomeStep = () => { - switch (currentStep) { - case "input": - return ( - - ) - case "greeting": - return - case "welcome": - return - case "username": - return - case "features": - return - case "memories": - return - default: - return null - } - } - - const renderSetupStep = () => { - switch (currentStep) { - case "relatable": - return - case "integrations": - return - default: - return null - } - } - - const isWelcomeFlow = currentFlow === "welcome" - const isSetupFlow = currentFlow === "setup" - - const minimizeNovaOrb = - isWelcomeFlow && ["features", "memories"].includes(currentStep) - const novaSize = currentStep === "memories" ? 150 : 300 - - const showUserSupermemory = isWelcomeFlow && currentStep === "username" + if (!session) redirect("/login") return ( -
- {isWelcomeFlow && ( - - )} - {isSetupFlow && } - - {isSetupFlow && } - - {isWelcomeFlow && currentStep === "input" && ( - - )} - - {isWelcomeFlow && showWelcomeContent && ( -
- - - - - {showUserSupermemory && } - - - {renderWelcomeStep()} - -
- )} - - {isSetupFlow && ( -
-
-
-
- - {renderSetupStep()} - -
- - - - -
-
-
- )} -
+ + + + + + ) } diff --git a/apps/web/app/onboarding-old/progress-bar.tsx b/apps/web/app/onboarding/progress-bar.tsx similarity index 100% rename from apps/web/app/onboarding-old/progress-bar.tsx rename to apps/web/app/onboarding/progress-bar.tsx diff --git a/apps/web/app/onboarding-old/welcome.tsx b/apps/web/app/onboarding/welcome.tsx similarity index 100% rename from apps/web/app/onboarding-old/welcome.tsx rename to apps/web/app/onboarding/welcome.tsx diff --git a/apps/web/components/mcp-detail-view.tsx b/apps/web/components/mcp-detail-view.tsx deleted file mode 100644 index adaddf6f..00000000 --- a/apps/web/components/mcp-detail-view.tsx +++ /dev/null @@ -1,621 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Button } from "@ui/components/button" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@ui/components/select" -import { CircleCheckIcon, CopyIcon, Check } from "lucide-react" -import Image from "next/image" -import { toast } from "sonner" -import { analytics } from "@/lib/analytics" -import { cn } from "@lib/utils" -import { dmMonoClassName, dmSansClassName } from "@/utils/fonts" -import { SyncLogoIcon } from "@ui/assets/icons" - -const clients = { - cursor: "Cursor", - claude: "Claude Desktop", - vscode: "VSCode", - cline: "Cline", - "gemini-cli": "Gemini CLI", - "claude-code": "Claude Code", - "mcp-url": "MCP URL", - "roo-cline": "Roo Cline", - witsy: "Witsy", - enconvo: "Enconvo", -} as const - -interface MCPDetailViewProps { - onBack: () => void -} - -export function MCPDetailView({ onBack }: MCPDetailViewProps) { - const [selectedClient, setSelectedClient] = useState< - keyof typeof clients | null - >(null) - const [selectedProject] = useState("sm_project_default") - const [mcpUrlTab, setMcpUrlTab] = useState<"oneClick" | "manual">("oneClick") - const [isCopied, setIsCopied] = useState(false) - const [activeStep, setActiveStep] = useState<1 | 2 | 3>(1) - - useEffect(() => { - analytics.mcpViewOpened() - }, []) - - function generateInstallCommand() { - if (!selectedClient) return "" - - let command = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${selectedClient} --oauth=yes` - - const projectIdForCommand = selectedProject.replace(/^sm_project_/, "") - command += ` --project ${projectIdForCommand}` - - return command - } - - const copyToClipboard = () => { - const command = generateInstallCommand() - navigator.clipboard.writeText(command) - analytics.mcpInstallCmdCopied() - setIsCopied(true) - setActiveStep(3) - setTimeout(() => setIsCopied(false), 2000) - } - - return ( -
-
- -
- -
-

- Connect your AI to supermemory MCP -

- -
-
- -

- MCP connects your AI apps to create and use memories directly -

-
-
- -

- Auto-fetch the right context from anything you've saved -

-
-
- -

- One-time setup,
seamless integration across your workflow -

-
-
- -
-
-
- -
-
- - {selectedClient && ( - - )} -
-
- {Object.entries(clients) - .slice(0, 7) - .map(([key, clientName]) => ( - - ))} -
- {!selectedClient && ( -

- *You can connect to all of these, setup is different for each - one -

- )} -
-
- -
-
- - 2 - -
-
-

- Copy the installation command -

- {selectedClient && ( -
- {selectedClient === "mcp-url" ? ( -
-
-
- - -
-
- - {mcpUrlTab === "oneClick" ? ( -
-

- Use this URL to quickly configure supermemory in - your AI assistant -

-
- - -
-
- ) : ( -
-

- Add this configuration to your MCP settings file - with authentication -

-
-
-															
-																{`{
-  "supermemory-mcp": {
-    "command": "npx",
-    "args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"],
-    "env": {},
-    "headers": {
-      "Authorization": "Bearer your-api-key-here"
-    }
-  }
-}`}
-															
-														
- -
-

- The API key is included as a Bearer token in the - Authorization header -

-
- )} -
- ) : ( -
-
- - -
-
- )} -
- )} -
-
- -
-
- - 3 - -
-
-

- Run command in your terminal -

- {activeStep === 3 && ( -

- - Waiting for installation -

- )} -
-
-
-
-
- ) -} diff --git a/apps/web/components/new/chat/index.tsx b/apps/web/components/new/chat/index.tsx index f0777be7..3460c456 100644 --- a/apps/web/components/new/chat/index.tsx +++ b/apps/web/components/new/chat/index.tsx @@ -7,6 +7,7 @@ import { DefaultChatTransport } from "ai" import NovaOrb from "@/components/nova/nova-orb" import { Button } from "@ui/components/button" import { + ChevronDownIcon, HistoryIcon, PanelRightCloseIcon, SearchIcon, @@ -24,7 +25,11 @@ import { UserMessage } from "./message/user-message" import { AgentMessage } from "./message/agent-message" import { ChainOfThought } from "./input/chain-of-thought" -function ChatEmptyStatePlaceholder() { +function ChatEmptyStatePlaceholder({ + onSuggestionClick, +}: { + onSuggestionClick: (suggestion: string) => void +}) { const suggestions = [ "Show me all content related to Supermemory.", "Summarize the key ideas from My Gita.", @@ -33,7 +38,10 @@ function ChatEmptyStatePlaceholder() { ] return ( -
+
@@ -50,7 +58,8 @@ function ChatEmptyStatePlaceholder() { +
+ )} + setInput(e.target.value)} diff --git a/apps/web/components/new/document-modal/content/notion-doc.tsx b/apps/web/components/new/document-modal/content/notion-doc.tsx new file mode 100644 index 00000000..45a7dab8 --- /dev/null +++ b/apps/web/components/new/document-modal/content/notion-doc.tsx @@ -0,0 +1,9 @@ +import { Streamdown } from "streamdown" + +export function NotionDoc({ content }: { content: string }) { + return ( +
+ {content} +
+ ) +} \ No newline at end of file diff --git a/apps/web/components/new/document-modal/document-icon.tsx b/apps/web/components/new/document-modal/document-icon.tsx index ec174094..f86228eb 100644 --- a/apps/web/components/new/document-modal/document-icon.tsx +++ b/apps/web/components/new/document-modal/document-icon.tsx @@ -82,7 +82,7 @@ const PDFIcon = ({ className }: { className: string }) => { filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB" > - + { filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB" > - + -

Graph

+

+ Graph +

-

List

+

+ List +

-
+
{memoryEntries.map((memory, idx) => { const isClickable = memory.url && diff --git a/apps/web/components/new/document-modal/index.tsx b/apps/web/components/new/document-modal/index.tsx index 9c0add83..74d4e178 100644 --- a/apps/web/components/new/document-modal/index.tsx +++ b/apps/web/components/new/document-modal/index.tsx @@ -14,6 +14,7 @@ import { GraphListMemories, type MemoryEntry } from "./graph-list-memories" import { YoutubeVideo } from "./content/yt-video" import { TweetContent } from "./content/tweet" import { isTwitterUrl } from "@/utils/url-helpers" +import { NotionDoc } from "./content/notion-doc" // Dynamically importing to prevent DOMMatrix error const PdfViewer = dynamic( @@ -102,13 +103,15 @@ export function DocumentModal({ } /> )} - {_document?.type === "text" && - !(_document?.url && isTwitterUrl(_document.url)) && ( -
- {_document.content} -
- )} + {_document?.type === "text" && ( +
+ {_document.content} +
+ )} {_document?.type === "pdf" && } + {_document?.type === "notion_doc" && ( + + )} {_document?.url?.includes("youtube.com") && ( )} diff --git a/apps/web/components/new/header.tsx b/apps/web/components/new/header.tsx index 572cba9b..4ef344a3 100644 --- a/apps/web/components/new/header.tsx +++ b/apps/web/components/new/header.tsx @@ -10,6 +10,8 @@ import { Plus, SearchIcon, FolderIcon, + LogOut, + Settings, } from "lucide-react" import { Button } from "@ui/components/button" import { cn } from "@lib/utils" @@ -24,6 +26,7 @@ import { } from "@ui/components/dropdown-menu" import { useQuery } from "@tanstack/react-query" import { $fetch } from "@repo/lib/api" +import { authClient } from "@lib/auth" import { DEFAULT_PROJECT_ID } from "@repo/lib/constants" import { useProjectMutations } from "@/hooks/use-project-mutations" import { useProject } from "@/stores" @@ -197,13 +200,24 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) { {user && ( - router.push("/new/settings")} - > - - {user?.name?.charAt(0)} - + + + + + {user?.name?.charAt(0)} + + + + router.push("/new/settings")}> + + Settings + + authClient.signOut()}> + + Logout + + + )}
diff --git a/apps/web/components/new/mcp-modal/index.tsx b/apps/web/components/new/mcp-modal/index.tsx index ff008bbd..d555ae62 100644 --- a/apps/web/components/new/mcp-modal/index.tsx +++ b/apps/web/components/new/mcp-modal/index.tsx @@ -4,6 +4,7 @@ import { cn } from "@lib/utils" import * as DialogPrimitive from "@radix-ui/react-dialog" import { ChevronsUpDownIcon, XIcon } from "lucide-react" import { Button } from "@ui/components/button" +import { MCPSteps } from "./mcp-detail-view" export function MCPModal({ isOpen, @@ -16,7 +17,7 @@ export function MCPModal({ !open && onClose()}>
-
- MCP steps +
+
@@ -59,7 +60,9 @@ export function MCPModal({ Migrate from MCP v1
- +
diff --git a/apps/web/components/new/mcp-modal/mcp-detail-view.tsx b/apps/web/components/new/mcp-modal/mcp-detail-view.tsx new file mode 100644 index 00000000..f1254bcb --- /dev/null +++ b/apps/web/components/new/mcp-modal/mcp-detail-view.tsx @@ -0,0 +1,629 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@ui/components/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/components/select" +import { CircleCheckIcon, CopyIcon, Check } from "lucide-react" +import Image from "next/image" +import { toast } from "sonner" +import { analytics } from "@/lib/analytics" +import { cn } from "@lib/utils" +import { dmMonoClassName, dmSansClassName } from "@/utils/fonts" +import { SyncLogoIcon } from "@ui/assets/icons" + +const clients = { + cursor: "Cursor", + claude: "Claude Desktop", + vscode: "VSCode", + cline: "Cline", + "gemini-cli": "Gemini CLI", + "claude-code": "Claude Code", + "mcp-url": "MCP URL", + "roo-cline": "Roo Cline", + witsy: "Witsy", + enconvo: "Enconvo", +} as const + +interface MCPStepsProps { + variant?: "full" | "embedded" +} + +export function MCPSteps({ variant = "full" }: MCPStepsProps) { + const [selectedClient, setSelectedClient] = useState< + keyof typeof clients | null + >(null) + const [selectedProject] = useState("sm_project_default") + const [mcpUrlTab, setMcpUrlTab] = useState<"oneClick" | "manual">("oneClick") + const [isCopied, setIsCopied] = useState(false) + const [activeStep, setActiveStep] = useState<1 | 2 | 3>(1) + + useEffect(() => { + analytics.mcpViewOpened() + }, []) + + function generateInstallCommand() { + if (!selectedClient) return "" + + let command = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${selectedClient} --oauth=yes` + + const projectIdForCommand = selectedProject.replace(/^sm_project_/, "") + command += ` --project ${projectIdForCommand}` + + return command + } + + const copyToClipboard = () => { + const command = generateInstallCommand() + navigator.clipboard.writeText(command) + analytics.mcpInstallCmdCopied() + setIsCopied(true) + setActiveStep(3) + setTimeout(() => setIsCopied(false), 2000) + } + + const isEmbedded = variant === "embedded" + + return ( +
+
+
+ +
+
+ + {selectedClient && ( + + )} +
+
+ {Object.entries(clients) + .slice(0, 7) + .map(([key, clientName]) => ( + + ))} +
+ {!selectedClient && ( +

+ *You can connect to all of these, setup is different for each one +

+ )} +
+
+ +
+
+ + 2 + +
+
+

+ Copy the installation command +

+ {selectedClient && ( +
+ {selectedClient === "mcp-url" ? ( +
+
+
+ + +
+
+ + {mcpUrlTab === "oneClick" ? ( +
+

+ Use this URL to quickly configure supermemory in your AI + assistant +

+
+ + +
+
+ ) : ( +
+

+ Add this configuration to your MCP settings file with + authentication +

+
+
+													
+														{`{
+  "supermemory-mcp": {
+    "command": "npx",
+    "args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"],
+    "env": {},
+    "headers": {
+      "Authorization": "Bearer your-api-key-here"
+    }
+  }
+}`}
+													
+												
+ +
+

+ The API key is included as a Bearer token in the + Authorization header +

+
+ )} +
+ ) : ( +
+
+ + +
+
+ )} +
+ )} +
+
+ +
+
+ + 3 + +
+
+

+ Run command in your terminal +

+ {activeStep === 3 && ( +

+ + Waiting for installation +

+ )} +
+
+
+ ) +} + +interface MCPDetailViewProps { + onBack: () => void +} + +export function MCPDetailView({ onBack }: MCPDetailViewProps) { + return ( +
+
+ +
+ +
+

+ Connect your AI to supermemory MCP +

+ +
+
+ +

+ MCP connects your AI apps to create and use memories directly +

+
+
+ +

+ Auto-fetch the right context from anything you've saved +

+
+
+ +

+ One-time setup,
seamless integration across your workflow +

+
+
+ + +
+
+ ) +} diff --git a/apps/web/components/new/memories-grid.tsx b/apps/web/components/new/memories-grid.tsx index 9fa32459..e5972d51 100644 --- a/apps/web/components/new/memories-grid.tsx +++ b/apps/web/components/new/memories-grid.tsx @@ -6,7 +6,10 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import { useInfiniteQuery } from "@tanstack/react-query" import { useCallback, memo, useMemo, useState, useRef } from "react" import type { z } from "zod" -import { Masonry, useInfiniteLoader } from "masonic" +import { + Masonry, + useInfiniteLoader, +} from "masonic" import { dmSansClassName } from "@/utils/fonts" import { SuperLoader } from "@/components/superloader" import { cn } from "@lib/utils" @@ -32,7 +35,7 @@ const IS_DEV = process.env.NODE_ENV === "development" const PAGE_SIZE = IS_DEV ? 100 : 100 const MAX_TOTAL = 1000 -export function MemoriesGrid() { +export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) { const { user } = useAuth() const { selectedProject } = useProject() const isMobile = useIsMobile() @@ -91,9 +94,6 @@ export function MemoriesGrid() { ) }, [data]) - const hasMore = hasNextPage - const isLoadingMore = isFetchingNextPage - const loadMoreDocuments = useCallback(async (): Promise => { if (hasNextPage && !isFetchingNextPage) { await fetchNextPage() @@ -104,7 +104,7 @@ export function MemoriesGrid() { const maybeLoadMore = useInfiniteLoader( async (_startIndex, _stopIndex, _currentItems) => { - if (hasMore && !isLoadingMore) { + if (hasNextPage && !isFetchingNextPage) { await loadMoreDocuments() } }, @@ -151,7 +151,9 @@ export function MemoriesGrid() { } return ( -
+
- -
- - ) : ( - -
- {params.get("error") && ( -
- Error: {params.get("error")}. Please try again! + + +
+ ) : ( +
+
+ + Welcome to {" "} + + + + {heroText} + +
+ + {params.get("error") && ( +
+ Error: {params.get("error")}. Please try again! +
+ )} + +
+
+ { + setEmail(e.target.value); + error && setError(null); + }, + required: true, + value: email, + }} + inputType="email" + label="Email" + /> + +
+ + {lastUsedMethod === "magic_link" && ( +
+ + Last used +
)} - -
- {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( -
- - Google - - - - - - } - authProvider="Google" - className="w-full" - disabled={isLoading} - onClick={() => { - if (isLoading) return; - setIsLoading(true); - posthog.capture("login_attempt", { - method: "social", - provider: "google", - }); - setPendingLoginMethod("google"); - signIn - .social({ - callbackURL: getCallbackURL(), - provider: "google", - }) - .finally(() => { - setIsLoading(false); - }); - }} - /> - {lastUsedMethod === "google" && ( -
- - Last used - -
- )} -
- ) : null} - {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( -
- - Github - - - - - - - - - - } - authProvider="Github" - className="w-full" - disabled={isLoading} - onClick={() => { - if (isLoading) return; - setIsLoading(true); - posthog.capture("login_attempt", { - method: "social", - provider: "github", - }); - setPendingLoginMethod("github"); - signIn - .social({ - callbackURL: getCallbackURL(), - provider: "github", - }) - .finally(() => { - setIsLoading(false); - }); - }} - /> - {lastUsedMethod === "github" && ( -
- - Last used - -
- )} -
- ) : null} -
- - - -
- - { - setEmail(e.target.value); - error && setError(null); - }, - required: true, - value: email, - }} - inputType="email" - /> - -
- - {lastUsedMethod === "magic_link" && ( -
- - Last used - -
- )} -
- - - - By continuing, you agree to our{" "} - - - Terms - {" "} - and{" "} - - Privacy Policy - - . - - -
- - )} - -
-
+
+ + + {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED || + !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( + + ) : null} + +
+ {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( +
+ + Google + + + + + + } + authProvider="Google" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return; + setIsLoading(true); + setError(null); + posthog.capture("login_attempt", { + method: "social", + provider: "google", + }); + setPendingLoginMethod("google"); + signIn + .social({ + callbackURL: getCallbackURL(), + provider: "google", + }) + .catch((error) => { + console.error("Google login error:", error); + posthog.capture("login_failed", { + method: "social", + provider: "google", + error: + error instanceof Error + ? error.message + : "Unknown error", + is_network_error: isNetworkError(error), + }); + setError(getErrorMessage(error)); + }) + .finally(() => { + setIsLoading(false); + }); + }} + /> + {lastUsedMethod === "google" && ( +
+ + Last used + +
+ )} +
+ ) : null} + {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( +
+ + Github + + + + + + + + + + } + authProvider="Github" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return; + setIsLoading(true); + setError(null); + posthog.capture("login_attempt", { + method: "social", + provider: "github", + }); + setPendingLoginMethod("github"); + signIn + .social({ + callbackURL: getCallbackURL(), + provider: "github", + }) + .catch((error) => { + console.error("GitHub login error:", error); + posthog.capture("login_failed", { + method: "social", + provider: "github", + error: + error instanceof Error + ? error.message + : "Unknown error", + is_network_error: isNetworkError(error), + }); + setError(getErrorMessage(error)); + }) + .finally(() => { + setIsLoading(false); + }); + }} + /> + {lastUsedMethod === "github" && ( +
+ + Last used + +
+ )} +
+ ) : null} +
+ + + By continuing, you agree to our{" "} + + + Terms + {" "} + and{" "} + + Privacy Policy + + . + + +
+ )} + ); -} +} \ No newline at end of file