add more stuff

This commit is contained in:
Mahesh Sanikommmu 2026-01-13 00:24:09 -08:00
parent 0e6f158c1b
commit 9780635886
42 changed files with 1941 additions and 1287 deletions

View file

@ -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 (
<div className="fixed inset-0 z-0 overflow-hidden">
<motion.div
className="absolute top-[20%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-0.png')] bg-size-[150%_auto] bg-top bg-no-repeat"
initial={{ y: "100%" }}
animate={{
y: 0,
opacity: [1, 0, 1],
}}
transition={{
y: { duration: 0.75, ease: "easeOut" },
opacity: { duration: 8, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" },
}}
/>
<motion.div
className="absolute top-[20%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-1.png')] bg-size-[150%_auto] bg-top bg-no-repeat"
initial={{ y: "100%" }}
animate={{
y: 0,
opacity: [0, 1, 0],
}}
transition={{
y: { duration: 0.75, ease: "easeOut" },
opacity: { duration: 8, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" },
}}
/>
<motion.div
className="absolute top-0 left-0 right-0 bottom-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat"
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",
opacity: 0.4,
}}
/>
</div>
)
}
function LoginCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
className="flex py-8 px-11 flex-col items-start gap-2 rounded-[22px] bg-linear-to-b from-[#06101F] to-[#030912] shadow-[1.5px_1.5px_20px_0_rgba(0,0,0,0.65),1px_1.5px_2px_0_rgba(128,189,255,0.07)_inset,-0.5px_-1.5px_4px_0_rgba(0,35,73,0.40)_inset]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.75, ease: "easeOut" }}
>
{children}
</motion.div>
)
}
export default function LoginPage() {
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 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<HTMLFormElement>) => {
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<HTMLFormElement>) => {
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 (
<main className="relative h-screen overflow-hidden">
<AnimatedGradientBackground />
<div className="relative z-10">
<InitialHeader />
<section className="flex flex-col items-center justify-center p-4 space-y-12 sm:p-6 md:p-8 lg:px-20 lg:py-12.5 min-h-[calc(100vh-80px)]">
<div className="text-center">
<div className="text-5xl font-medium">
Never forget anything, anywhere
</div>
<div className="text-5xl font-medium">with supermemory</div>
</div>
{submittedEmail ? (
<LoginCard>
<div className="w-[360px] flex flex-col gap-4 lg:gap-6 min-h-2/3">
<div className="flex flex-col gap-2 text-center lg:text-left">
<Title1Bold className="text-foreground">
Almost there!
</Title1Bold>
<HeadingH3Medium className="text-muted-foreground">
Click the magic link we've sent to{" "}
<span className="text-foreground">{submittedEmail}</span>.
</HeadingH3Medium>
</div>
<TextSeparator text="OR" className={cn(dmSansClassName())} />
<form
className="flex flex-col gap-4 lg:gap-6"
onSubmit={handleSubmitToken}
>
<LabeledInput
inputPlaceholder="your temporary login code"
inputProps={{
name: "token",
required: true,
disabled: isLoading,
"aria-invalid": error ? "true" : "false",
}}
inputType="text"
label="Enter code"
/>
<Button disabled={isLoading} id="verify-token" type="submit">
Verify Token
</Button>
</form>
</div>
</LoginCard>
) : (
<LoginCard>
<div className="w-[360px] flex flex-col" style={{ gap: "12px" }}>
{params.get("error") && (
<div className="text-red-500">
Error: {params.get("error")}. Please try again!
</div>
)}
<div className="flex flex-col gap-3">
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
!process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? (
<div className="relative grow">
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
height="25"
viewBox="0 0 24 25"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Google</title>
<path
d="M21.8055 10.2563H21V10.2148H12V14.2148H17.6515C16.827 16.5433 14.6115 18.2148 12 18.2148C8.6865 18.2148 6 15.5283 6 12.2148C6 8.90134 8.6865 6.21484 12 6.21484C13.5295 6.21484 14.921 6.79184 15.9805 7.73434L18.809 4.90584C17.023 3.24134 14.634 2.21484 12 2.21484C6.4775 2.21484 2 6.69234 2 12.2148C2 17.7373 6.4775 22.2148 12 22.2148C17.5225 22.2148 22 17.7373 22 12.2148C22 11.5443 21.931 10.8898 21.8055 10.2563Z"
fill="#FFC107"
/>
<path
d="M3.15234 7.56034L6.43784 9.96984C7.32684 7.76884 9.47984 6.21484 11.9993 6.21484C13.5288 6.21484 14.9203 6.79184 15.9798 7.73434L18.8083 4.90584C17.0223 3.24134 14.6333 2.21484 11.9993 2.21484C8.15834 2.21484 4.82734 4.38334 3.15234 7.56034Z"
fill="#FF3D00"
/>
<path
d="M12.0002 22.2152C14.5832 22.2152 16.9302 21.2267 18.7047 19.6192L15.6097 17.0002C14.5721 17.7897 13.3039 18.2166 12.0002 18.2152C9.39916 18.2152 7.19066 16.5567 6.35866 14.2422L3.09766 16.7547C4.75266 19.9932 8.11366 22.2152 12.0002 22.2152Z"
fill="#4CAF50"
/>
<path
d="M21.8055 10.2563H21V10.2148H12V14.2148H17.6515C17.2571 15.3231 16.5467 16.2914 15.608 17.0003L15.6095 16.9993L18.7045 19.6183C18.4855 19.8173 22 17.2148 22 12.2148C22 11.5443 21.931 10.8898 21.8055 10.2563Z"
fill="#1976D2"
/>
</svg>
}
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" && (
<div className="absolute -top-2 -right-2">
<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 ? (
<div className="relative grow">
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5 text-foreground"
fill="none"
height="25"
viewBox="0 0 26 25"
width="26"
xmlns="http://www.w3.org/2000/svg"
>
<title>Github</title>
<g clipPath="url(#clip0_2579_3356)">
<path
clipRule="evenodd"
d="M12.9635 0.214844C6.20975 0.214844 0.75 5.71484 0.75 12.5191C0.75 17.9581 4.24825 22.5621 9.10125 24.1916C9.708 24.3141 9.93025 23.9268 9.93025 23.6011C9.93025 23.3158 9.91025 22.3381 9.91025 21.3193C6.51275 22.0528 5.80525 19.8526 5.80525 19.8526C5.25925 18.4266 4.45025 18.0601 4.45025 18.0601C3.33825 17.3063 4.53125 17.3063 4.53125 17.3063C5.76475 17.3878 6.412 18.5693 6.412 18.5693C7.50375 20.4433 9.263 19.9138 9.97075 19.5878C10.0718 18.7933 10.3955 18.2433 10.7393 17.9378C8.0295 17.6526 5.1785 16.5933 5.1785 11.8671C5.1785 10.5226 5.6635 9.42259 6.432 8.56709C6.31075 8.26159 5.886 6.99834 6.5535 5.30759C6.5535 5.30759 7.58475 4.98159 9.91 6.57059C10.9055 6.30126 11.9322 6.16425 12.9635 6.16309C13.9948 6.16309 15.046 6.30584 16.0168 6.57059C18.3423 4.98159 19.3735 5.30759 19.3735 5.30759C20.041 6.99834 19.616 8.26159 19.4948 8.56709C20.2835 9.42259 20.7485 10.5226 20.7485 11.8671C20.7485 16.5933 17.8975 17.6321 15.1675 17.9378C15.6125 18.3248 15.9965 19.0581 15.9965 20.2193C15.9965 21.8693 15.9765 23.1936 15.9765 23.6008C15.9765 23.9268 16.199 24.3141 16.8055 24.1918C21.6585 22.5618 25.1568 17.9581 25.1568 12.5191C25.1768 5.71484 19.697 0.214844 12.9635 0.214844Z"
fill="currentColor"
fillRule="evenodd"
/>
</g>
<defs>
<clipPath id="clip0_2579_3356">
<rect
fill="currentColor"
height="24"
transform="translate(0.75 0.214844)"
width="24.5"
/>
</clipPath>
</defs>
</svg>
}
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" && (
<div className="absolute -top-2 -right-2">
<Badge variant="default" className="text-xs">
Last used
</Badge>
</div>
)}
</div>
) : null}
</div>
<TextSeparator text="OR" className={cn(dmSansClassName())} />
<div className="flex flex-col gap-6">
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<LabeledInput
error={error}
inputPlaceholder="your@email.com"
inputProps={{
"aria-invalid": error ? "true" : "false",
disabled: isLoading,
id: "email",
onChange: (e) => {
setEmail(e.target.value)
error && setError(null)
},
required: true,
value: email,
}}
inputType="email"
/>
<div className="relative">
<Button
className="flex justify-center items-center w-full h-[44px] relative gap-3 p-2 rounded-xl"
style={{
background:
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
boxShadow:
"1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
}}
disabled={isLoading}
type="submit"
>
<Logo className="size-4" />
{isLoadingEmail
? "Sending login link..."
: "Log in with Supermemory"}
</Button>
{lastUsedMethod === "magic_link" && (
<div className="absolute -top-2 -right-2">
<Badge variant="default" className="text-xs">
Last used
</Badge>
</div>
)}
</div>
</form>
<Label1Regular
className={cn(
"text-center text-xs! text-[#737373B2]",
dmSansClassName(),
)}
>
By continuing, you agree to our{" "}
<span className="inline-block">
<a
className="underline"
href="https://supermemory.ai/terms-of-service"
>
Terms
</a>{" "}
and{" "}
<a
className="underline"
href="https://supermemory.ai/privacy-policy"
>
Privacy Policy
</a>
.
</span>
</Label1Regular>
</div>
</div>
</LoginCard>
)}
</section>
</div>
</main>
)
}

View file

@ -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 (

View file

@ -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}</>
}

View file

@ -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 (
<motion.div
className="absolute inset-0 top-[-34px] flex items-center justify-center z-10"
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 0 }}
transition={{ duration: 1, ease: "easeOut" }}
>
<Logo className="h-14 text-white" />
<div className="flex flex-col items-start justify-center ml-4">
<p className="text-white text-[25px] font-medium leading-none">
{name.split(" ")[0]}'s
</p>
<p className="text-white font-bold text-4xl leading-none -mt-2">
supermemory
</p>
</div>
</motion.div>
)
}
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 (
<InputStep
key="input"
name={name}
setName={setName}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
)
case "greeting":
return <GreetingStep key="greeting" name={name} />
case "welcome":
return <WelcomeStep key="welcome" />
case "username":
return <ContinueStep key="username" />
case "features":
return <FeaturesStep key="features" />
case "memories":
return <MemoriesStep key="memories" onSubmit={setMemoryFormData} />
default:
return null
}
}
const renderSetupStep = () => {
switch (currentStep) {
case "relatable":
return <RelatableQuestion key="relatable" />
case "integrations":
return <IntegrationsStep key="integrations" />
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 (
<div className="h-screen overflow-hidden bg-black">
{isWelcomeFlow && (
<InitialHeader
showUserSupermemory={
currentStep === "features" || currentStep === "memories"
}
name={name}
/>
)}
{isSetupFlow && <SetupHeader />}
{isSetupFlow && <AnimatedGradientBackground animateFromBottom={false} />}
{isWelcomeFlow && currentStep === "input" && (
<AnimatedGradientBackground animateFromBottom={true} />
)}
{isWelcomeFlow && showWelcomeContent && (
<div className="fixed inset-0 flex flex-col items-center justify-center overflow-y-auto">
<motion.div
className="absolute inset-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat pointer-events-none"
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",
opacity: 0.6,
}}
/>
<motion.div
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex flex-col items-center justify-center",
)}
animate={{
gap: minimizeNovaOrb ? 0 : 16,
}}
transition={{
duration: 0.6,
ease: "easeOut",
}}
>
<motion.div
animate={{
scale:
currentStep === "features"
? 0.7
: currentStep === "memories"
? 0.4
: 1,
padding: minimizeNovaOrb ? 0 : 48,
paddingTop: 0,
}}
transition={{
duration: 0.8,
ease: "easeOut",
delay: 0.2,
}}
className="relative"
>
<NovaOrb size={novaSize} />
{showUserSupermemory && <UserSupermemory name={name} />}
</motion.div>
<AnimatePresence mode="wait">{renderWelcomeStep()}</AnimatePresence>
</motion.div>
</div>
)}
{isSetupFlow && (
<main className="relative min-h-screen">
<div className="relative z-10">
<div className="flex flex-row h-[calc(100vh-90px)] relative">
<div className="flex-1 flex flex-col items-center justify-start p-8">
<AnimatePresence mode="wait">
{renderSetupStep()}
</AnimatePresence>
</div>
<AnimatePresence mode="popLayout">
<ChatSidebar formData={memoryFormData} />
</AnimatePresence>
</div>
</div>
</main>
)}
</div>
)
}

View file

@ -274,12 +274,12 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
>
<motion.button
onClick={toggleChat}
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border-[1px] border-[#17181A] text-white cursor-pointer"
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer"
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
<NovaOrb size={24} className="!blur-none z-10" />
<NovaOrb size={24} className="blur-none! z-10" />
Chat with Nova
</motion.button>
</motion.div>
@ -314,7 +314,7 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
>
{msg.type === "waiting" ? (
<div className="flex items-center gap-2 text-white/50">
<NovaOrb size={30} className="!blur-none" />
<NovaOrb size={30} className="blur-none!" />
<span className="text-sm">{msg.message}</span>
</div>
) : (
@ -328,7 +328,7 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
{i === 0 && (
<div className="w-3 h-3 bg-[#293952]/40 rounded-full mb-1" />
)}
<div className="w-[1px] flex-1 bg-[#293952]/40" />
<div className="w-px flex-1 bg-[#293952]/40" />
</div>
{msg.type === "memory" && (
<div className="space-y-2 w-full max-h-60 overflow-y-auto scrollbar-thin">
@ -376,13 +376,13 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
))}
{messages.length === 0 && !isLoading && !formData && (
<div className="flex items-center gap-2 text-white/50">
<NovaOrb size={28} className="!blur-none" />
<NovaOrb size={28} className="blur-none!" />
<span className="text-sm">Waiting for your input</span>
</div>
)}
{isLoading && (
<div className="flex items-center gap-2 text-foreground/50">
<NovaOrb size={28} className="!blur-none" />
<NovaOrb size={28} className="blur-none!" />
<span className="text-sm">Fetching your memories...</span>
</div>
)}

View file

@ -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() {
<Button
variant="link"
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
onClick={() => router.push("/onboarding?flow=setup&step=relatable")}
onClick={() => router.push("/new/onboarding?flow=setup&step=relatable")}
>
Back
</Button>

View file

@ -35,7 +35,7 @@ export function RelatableQuestion() {
const [selectedOptions, setSelectedOptions] = useState<number[]>([])
const handleContinueOrSkip = () => {
router.push("/onboarding?flow=setup&step=integrations")
router.push("/new/onboarding?flow=setup&step=integrations")
}
return (

View file

@ -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 (

View file

@ -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 (
<motion.div

View file

@ -288,7 +288,7 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
otherLinks: otherLinks.filter((l) => l.trim()),
}
onSubmit(formData)
router.push("/onboarding?flow=setup&step=relatable")
router.push("/new/onboarding?flow=setup&step=relatable")
}}
>
{isSubmitting ? "Fetching..." : "Remember this →"}

View file

@ -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 (
<HotkeysProvider>
@ -28,14 +29,16 @@ export default function NewPage() {
onOpenMCP={() => setIsMCPModalOpen(true)}
/>
<main className="relative">
<div className="relative z-10">
<div key={`main-container-${isChatOpen}`} className="relative z-10">
<div className="flex flex-row h-[calc(100vh-90px)] relative">
<div className="flex-1 flex flex-col justify-start p-6 pr-0">
<MemoriesGrid />
<MemoriesGrid isChatOpen={isChatOpen} />
</div>
<AnimatePresence mode="popLayout">
<ChatSidebar />
<ChatSidebar
isChatOpen={isChatOpen}
setIsChatOpen={setIsChatOpen}
/>
</AnimatePresence>
</div>
</div>

View file

@ -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 (
<OnboardingProvider>
<OnboardingProgressBar />
<OnboardingBackground>
<OnboardingForm />
</OnboardingBackground>
</OnboardingProvider>
)
}

View file

@ -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 (
<motion.div
className="absolute inset-0 top-[-34px] flex items-center justify-center z-10"
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 0 }}
transition={{ duration: 1, ease: "easeOut" }}
>
<Logo className="h-14 text-white" />
<div className="flex flex-col items-start justify-center ml-4">
<p className="text-white text-[25px] font-medium leading-none">
{name.split(" ")[0]}'s
</p>
<p className="text-white font-bold text-4xl leading-none -mt-2">
supermemory
</p>
</div>
</motion.div>
)
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 (
<InputStep
key="input"
name={name}
setName={setName}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
)
case "greeting":
return <GreetingStep key="greeting" name={name} />
case "welcome":
return <WelcomeStep key="welcome" />
case "username":
return <ContinueStep key="username" />
case "features":
return <FeaturesStep key="features" />
case "memories":
return <MemoriesStep key="memories" onSubmit={setMemoryFormData} />
default:
return null
}
}
const renderSetupStep = () => {
switch (currentStep) {
case "relatable":
return <RelatableQuestion key="relatable" />
case "integrations":
return <IntegrationsStep key="integrations" />
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 (
<div className="h-screen overflow-hidden bg-black">
{isWelcomeFlow && (
<InitialHeader
showUserSupermemory={
currentStep === "features" || currentStep === "memories"
}
name={name}
/>
)}
{isSetupFlow && <SetupHeader />}
{isSetupFlow && <AnimatedGradientBackground animateFromBottom={false} />}
{isWelcomeFlow && currentStep === "input" && (
<AnimatedGradientBackground animateFromBottom={true} />
)}
{isWelcomeFlow && showWelcomeContent && (
<div className="fixed inset-0 flex flex-col items-center justify-center overflow-y-auto">
<motion.div
className="absolute inset-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat pointer-events-none"
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",
opacity: 0.6,
}}
/>
<motion.div
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex flex-col items-center justify-center",
)}
animate={{
gap: minimizeNovaOrb ? 0 : 16,
}}
transition={{
duration: 0.6,
ease: "easeOut",
}}
>
<motion.div
animate={{
scale:
currentStep === "features"
? 0.7
: currentStep === "memories"
? 0.4
: 1,
padding: minimizeNovaOrb ? 0 : 48,
paddingTop: 0,
}}
transition={{
duration: 0.8,
ease: "easeOut",
delay: 0.2,
}}
className="relative"
>
<NovaOrb size={novaSize} />
{showUserSupermemory && <UserSupermemory name={name} />}
</motion.div>
<AnimatePresence mode="wait">{renderWelcomeStep()}</AnimatePresence>
</motion.div>
</div>
)}
{isSetupFlow && (
<main className="relative min-h-screen">
<div className="relative z-10">
<div className="flex flex-row h-[calc(100vh-90px)] relative">
<div className="flex-1 flex flex-col items-center justify-start p-8">
<AnimatePresence mode="wait">
{renderSetupStep()}
</AnimatePresence>
</div>
<AnimatePresence mode="popLayout">
<ChatSidebar formData={memoryFormData} />
</AnimatePresence>
</div>
</div>
</main>
)}
</div>
<OnboardingProvider>
<OnboardingProgressBar />
<OnboardingBackground>
<OnboardingForm />
</OnboardingBackground>
</OnboardingProvider>
)
}

View file

@ -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<string>("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 (
<div className="flex flex-col h-full p-8">
<div className="mb-6">
<Button
variant="link"
className="text-white hover:text-gray-300 p-0 hover:no-underline cursor-pointer"
onClick={onBack}
>
Back
</Button>
</div>
<div className="flex-1 flex flex-col items-start justify-start">
<h1 className="text-white text-xl font-medium mb-4 text-start">
Connect your AI to supermemory MCP
</h1>
<div className="mb-12 space-x-4 flex max-w-2xl">
<div
className={cn(
"flex items-start space-x-3 w-[200px]",
dmSansClassName(),
)}
>
<CircleCheckIcon className="size-4 text-green-500 shrink-0 mt-0.5" />
<p className="text-[#8B8B8B] text-sm">
MCP connects your AI apps to create and use memories directly
</p>
</div>
<div
className={cn(
"flex items-start space-x-3 w-[200px]",
dmSansClassName(),
)}
>
<CircleCheckIcon className="size-4 text-green-500 shrink-0 mt-0.5" />
<p className="text-[#8B8B8B] text-sm">
Auto-fetch the right context from anything you've saved
</p>
</div>
<div
className={cn(
"flex items-start space-x-3 w-[200px]",
dmSansClassName(),
)}
>
<CircleCheckIcon className="size-4 text-green-500 shrink-0 mt-0.5" />
<p className="text-[#8B8B8B] text-sm">
One-time setup, <br /> seamless integration across your workflow
</p>
</div>
</div>
<div className="w-full max-w-2xl relative">
<div
className="absolute left-4 top-0 w-px bg-[#1E293B] z-10"
style={{ height: activeStep === 3 ? "calc(100% - 4rem)" : "100%" }}
/>
<div className="flex items-start space-x-4 z-20">
<button
type="button"
className={cn(
"rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium shrink-0 z-20 text-white",
selectedClient && "cursor-pointer hover:bg-[#1a2530]",
!selectedClient && "cursor-default",
activeStep === 1
? "border border-[#15233C] bg-[#08142D]"
: "bg-[#161F2B] ",
)}
onClick={() => {
if (selectedClient) {
setSelectedClient(null)
setActiveStep(1)
}
}}
onKeyDown={(e) => {
if (selectedClient && (e.key === "Enter" || e.key === " ")) {
e.preventDefault()
setSelectedClient(null)
setActiveStep(1)
}
}}
disabled={!selectedClient}
>
<span
className="text-lg"
style={
activeStep === 1
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
1
</span>
</button>
<div className="flex-1 mb-4">
<div className="flex gap-4 mb-4">
<button
type="button"
className={cn(
"text-white text-lg font-medium text-center",
selectedClient && "cursor-pointer hover:opacity-80",
!selectedClient && "cursor-default",
)}
onClick={() => {
if (selectedClient) {
setSelectedClient(null)
setActiveStep(1)
}
}}
onKeyDown={(e) => {
if (
selectedClient &&
(e.key === "Enter" || e.key === " ")
) {
e.preventDefault()
setSelectedClient(null)
setActiveStep(1)
}
}}
disabled={!selectedClient}
style={
activeStep === 1
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
Select your AI client
</button>
{selectedClient && (
<Select
onValueChange={(value) => {
setSelectedClient(value as keyof typeof clients)
setActiveStep(2)
}}
value={selectedClient || undefined}
>
<SelectTrigger
className="max-w-md rounded-full border-[#242A33] text-white hover:border-gray-600 bg-transparent!"
style={{
background:
"linear-gradient(0deg, #0A0E14 0%, #080B0F 100%)",
}}
>
{selectedClient ? (
<div className="flex items-center gap-2">
<Image
alt={clients[selectedClient]}
height={20}
width={20}
unoptimized
src={
selectedClient === "mcp-url"
? "/mcp-icon.svg"
: `/mcp-supported-tools/${selectedClient === "claude-code" ? "claude" : selectedClient}.png`
}
/>
<span>{clients[selectedClient]}</span>
</div>
) : (
<SelectValue placeholder="Select a client" />
)}
</SelectTrigger>
<SelectContent className="bg-black border-none">
{Object.entries(clients)
.slice(0, 7)
.map(([key, clientName]) => (
<SelectItem
key={key}
value={key}
className="text-white hover:bg-[#080B0F]"
>
<div className="flex items-center gap-2">
<Image
alt={clientName}
height={20}
width={20}
unoptimized
src={
key === "mcp-url"
? "/mcp-icon.svg"
: `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`
}
/>
<span>{clientName}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div
className={cn(
"flex flex-wrap gap-2 mb-4",
selectedClient ? "hidden" : "",
)}
>
{Object.entries(clients)
.slice(0, 7)
.map(([key, clientName]) => (
<button
key={key}
type="button"
onClick={() => {
setSelectedClient(key as keyof typeof clients)
setActiveStep(2)
}}
className={`mcp-client-button-group py-[6px] pl-2 pr-3 rounded-full border transition-colors cursor-pointer duration-200 ${
selectedClient === key
? "border-blue-500 bg-blue-500/10"
: "border-[#242A33] bg-[#080B0F] hover:border-[#3273FC4D] hover:bg-[#08142D]"
}`}
>
<div className="flex items-center space-x-1">
<div className="w-5 h-5 flex items-center justify-center">
<Image
alt={clientName}
unoptimized
className="rounded object-contain"
height={20}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const parent = target.parentElement
if (
parent &&
!parent.querySelector(".fallback-text")
) {
const fallback = document.createElement("span")
fallback.className =
"fallback-text text-xs font-bold text-white"
fallback.textContent = clientName
.substring(0, 2)
.toUpperCase()
parent.appendChild(fallback)
}
}}
src={
key === "mcp-url"
? "/mcp-icon.svg"
: `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`
}
width={20}
/>
</div>
<span className="mcp-client-gradient-text text-sm font-medium text-white">
{clientName}
</span>
</div>
</button>
))}
</div>
{!selectedClient && (
<p
className={cn(
"text-[#8B8B8B] text-[14px]",
dmSansClassName(),
)}
>
*You can connect to all of these, setup is different for each
one
</p>
)}
</div>
</div>
<div className="flex items-start space-x-4">
<div
className={cn(
"rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium shrink-0 z-20 text-white",
activeStep === 2
? "border border-[#15233C] bg-[#08142D]"
: "bg-[#161F2B]",
)}
>
<span
className="text-lg"
style={
activeStep === 2
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
2
</span>
</div>
<div className="flex-1 mb-4">
<h3
className="text-white text-lg font-medium mb-4"
style={
activeStep === 2
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
Copy the installation command
</h3>
{selectedClient && (
<div className="space-y-3">
{selectedClient === "mcp-url" ? (
<div className="space-y-4">
<div className="flex justify-end">
<div className="flex bg-[#0D121A] rounded-full p-1 border border-gray-600">
<button
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${
mcpUrlTab === "oneClick"
? "bg-[#080B0F] text-white border border-gray-600"
: "text-gray-400 hover:text-white"
}`}
onClick={() => setMcpUrlTab("oneClick")}
type="button"
>
Quick Setup
</button>
<button
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${
mcpUrlTab === "manual"
? "bg-[#080B0F] text-white border border-gray-600"
: "text-gray-400 hover:text-white"
}`}
onClick={() => setMcpUrlTab("manual")}
type="button"
>
Manual Config
</button>
</div>
</div>
{mcpUrlTab === "oneClick" ? (
<div className="space-y-2">
<p className="text-sm text-gray-400">
Use this URL to quickly configure supermemory in
your AI assistant
</p>
<div className="relative">
<input
className="font-mono text-xs w-full pr-10 p-2 bg-black border border-gray-600 rounded text-green-400"
readOnly
value="https://api.supermemory.ai/mcp"
/>
<button
type="button"
className="absolute top-1 right-1 cursor-pointer p-1"
onClick={() => {
navigator.clipboard.writeText(
"https://api.supermemory.ai/mcp",
)
analytics.mcpInstallCmdCopied()
toast.success("Copied to clipboard!")
setActiveStep(3)
}}
>
<CopyIcon className="size-4 text-gray-400 hover:text-white" />
</button>
</div>
</div>
) : (
<div className="space-y-3">
<p className="text-sm text-gray-400">
Add this configuration to your MCP settings file
with authentication
</p>
<div className="relative">
<pre className="bg-black border border-gray-600 rounded-lg p-4 pr-12 text-xs overflow-x-auto max-w-full">
<code className="font-mono block whitespace-pre-wrap break-all text-green-400">
{`{
"supermemory-mcp": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"],
"env": {},
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}`}
</code>
</pre>
<button
type="button"
className="absolute top-2 right-2 cursor-pointer h-8 w-8 p-0 bg-[#0D121A] hover:bg-[#1a1a1a] rounded"
onClick={() => {
const config = `{
"supermemory-mcp": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"],
"env": {},
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}`
navigator.clipboard.writeText(config)
analytics.mcpInstallCmdCopied()
toast.success("Copied to clipboard!")
setIsCopied(true)
setActiveStep(3)
setTimeout(() => setIsCopied(false), 2000)
}}
>
{isCopied ? (
<span className="text-green-600"></span>
) : (
<CopyIcon className="size-3.5" />
)}
</button>
</div>
<p className="text-xs text-gray-400">
The API key is included as a Bearer token in the
Authorization header
</p>
</div>
)}
</div>
) : (
<div className="space-y-3">
<div className="relative">
<input
className={cn(
"text-xs w-full pr-24 py-4 bg-[#0D121A] rounded-xl text-white pl-3",
dmMonoClassName(),
)}
style={{
border: "1px solid rgba(61, 67, 77, 0.10)",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}}
readOnly
value={generateInstallCommand()}
/>
<button
type="button"
className={cn(
"absolute top-[5px] right-1 cursor-pointer p-1 flex items-center rounded-[10px] px-3 py-2 gap-2",
dmSansClassName(),
)}
style={{
background:
"linear-gradient(180deg, #267BF1 40.23%, #15468B 100%), linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
border: "1px solid #000",
}}
onClick={copyToClipboard}
>
{isCopied ? (
<>
<Check className="size-4 text-white" />
<span className="text-white">Copied</span>
</>
) : (
<>
<CopyIcon className="size-[20px] text-white stroke-[2px]" />
<span className="text-white">Copy</span>
</>
)}
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
<div className="flex items-start space-x-4">
<div
className={cn(
"rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium shrink-0 z-20 text-white",
activeStep === 3
? "border border-[#15233C] bg-[#08142D]"
: "bg-[#161F2B] ",
)}
>
<span
className="text-lg"
style={
activeStep === 3
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
3
</span>
</div>
<div className="flex-1 space-y-4">
<h3
className="text-white text-lg font-medium"
style={
activeStep === 3
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
Run command in your terminal
</h3>
{activeStep === 3 && (
<p
className={cn(
"font-mono text-xs w-full pr-10 p-4 px-2 bg-[#0D121A] rounded-xl text-white pl-3 flex items-center gap-2",
dmMonoClassName(),
)}
style={{
border: "1px solid rgba(61, 67, 77, 0.10)",
}}
>
<SyncLogoIcon className="size-4" />
Waiting for installation
</p>
)}
</div>
</div>
</div>
</div>
</div>
)
}

View file

@ -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 (
<div className="flex flex-col items-center justify-center h-full">
<div
id="chat-empty-state"
className="flex flex-col items-center justify-center h-full"
>
<div className="relative w-32 h-32">
<GradientLogo className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16" />
<LogoBgGradient className="w-full h-full" />
@ -50,7 +58,8 @@ function ChatEmptyStatePlaceholder() {
<Button
key={suggestion}
variant="default"
className="rounded-full text-base gap-1 h-10! border-[#2261CA33] bg-[#041127] border w-fit py-[4px] pl-[8px] pr-[12px] hover:bg-[#0A1A3A] hover:[&_span]:text-white hover:[&_svg]:text-white transition-colors"
className="rounded-full text-base gap-1 h-10! border-[#2261CA33] bg-[#041127] border w-fit py-[4px] pl-[8px] pr-[12px] hover:bg-[#0A1A3A] hover:[&_span]:text-white hover:[&_svg]:text-white transition-colors cursor-pointer"
onClick={() => onSuggestionClick(suggestion)}
>
<SearchIcon className="size-4 text-[#267BF1]" />
<span className="text-[#267BF1] text-[12px]">{suggestion}</span>
@ -62,9 +71,14 @@ function ChatEmptyStatePlaceholder() {
)
}
export function ChatSidebar() {
export function ChatSidebar({
isChatOpen,
setIsChatOpen,
}: {
isChatOpen: boolean
setIsChatOpen: (open: boolean) => void
}) {
const [input, setInput] = useState("")
const [isChatOpen, setIsChatOpen] = useState(true)
const [selectedModel, setSelectedModel] = useState<ModelId>("gemini-2.5-pro")
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null)
const [hoveredMessageId, setHoveredMessageId] = useState<string | null>(null)
@ -79,13 +93,15 @@ export function ChatSidebar() {
Record<string, boolean>
>({})
const [isInputExpanded, setIsInputExpanded] = useState(false)
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true)
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
const messagesContainerRef = useRef<HTMLDivElement>(null)
const { selectedProject } = useProject()
const { setCurrentChatId } = usePersistentChat()
const { messages, sendMessage, status, setMessages, stop } = useChat({
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
credentials: "include",
body: {
metadata: {
@ -200,11 +216,29 @@ export function ChatSidebar() {
}
}, [messages, followUpQuestions, loadingFollowUps, status])
const checkIfScrolledToBottom = useCallback(() => {
if (!messagesContainerRef.current) return
const container = messagesContainerRef.current
const { scrollTop, scrollHeight, clientHeight } = container
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
const isAtBottom = distanceFromBottom <= 20
setIsScrolledToBottom(isAtBottom)
}, [])
const scrollToBottom = useCallback(() => {
if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop =
messagesContainerRef.current.scrollHeight
setIsScrolledToBottom(true)
}
}, [])
const handleSend = () => {
if (!input.trim() || status === "submitted" || status === "streaming")
return
sendMessage({ text: input })
setInput("")
scrollToBottom()
}
const handleKeyDown = (e: React.KeyboardEvent) => {
@ -243,7 +277,6 @@ export function ChatSidebar() {
}, [])
const handleNewChat = useCallback(() => {
console.log("handleNewChat")
const newId = crypto.randomUUID()
setCurrentChatId(newId)
setMessages([])
@ -270,6 +303,49 @@ export function ChatSidebar() {
return () => window.removeEventListener("keydown", handleKeyDown)
}, [isChatOpen, handleNewChat])
// Scroll to bottom when a new user message is added
useEffect(() => {
const lastMessage = messages[messages.length - 1]
if (lastMessage?.role === "user" && messagesContainerRef.current) {
messagesContainerRef.current.scrollTop =
messagesContainerRef.current.scrollHeight
setIsScrolledToBottom(true)
}
// Always check scroll position when messages change
checkIfScrolledToBottom()
}, [messages, checkIfScrolledToBottom])
// Add scroll event listener to track scroll position
useEffect(() => {
const container = messagesContainerRef.current
if (!container) return
const handleScroll = () => {
requestAnimationFrame(() => {
checkIfScrolledToBottom()
})
}
container.addEventListener("scroll", handleScroll, { passive: true })
// Initial check with a small delay to ensure DOM is ready
setTimeout(() => {
checkIfScrolledToBottom()
}, 100)
// Also observe resize to detect content height changes
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
checkIfScrolledToBottom()
})
})
resizeObserver.observe(container)
return () => {
container.removeEventListener("scroll", handleScroll)
resizeObserver.disconnect()
}
}, [checkIfScrolledToBottom])
return (
<AnimatePresence mode="wait">
{!isChatOpen ? (
@ -305,7 +381,7 @@ export function ChatSidebar() {
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
>
<div
className="absolute top-0 left-0 right-0 flex items-center justify-between pt-4 px-4"
className="absolute top-0 left-0 right-0 flex items-center justify-between pt-4 px-4 rounded-t-2xl"
style={{
background:
"linear-gradient(180deg, #0A0E14 40.49%, rgba(10, 14, 20, 0.00) 100%)",
@ -354,6 +430,7 @@ export function ChatSidebar() {
</div>
</div>
<div
ref={messagesContainerRef}
className={cn(
"flex-1 overflow-y-auto px-4 scrollbar-thin",
dmSansClassName(),
@ -365,11 +442,17 @@ export function ChatSidebar() {
style={{ backgroundColor: "#000000E5" }}
/>
)}
{messages.length === 0 && <ChatEmptyStatePlaceholder />}
{messages.length === 0 && (
<ChatEmptyStatePlaceholder
onSuggestionClick={(suggestion) => {
sendMessage({ text: suggestion })
}}
/>
)}
<div
className={cn(
messages.length > 0
? "flex flex-col space-y-3 min-h-full justify-end"
? "flex flex-col space-y-3 min-h-full justify-end pt-14"
: "",
)}
>
@ -426,6 +509,20 @@ export function ChatSidebar() {
</div>
</div>
{!isScrolledToBottom && messages.length > 0 && (
<div className="absolute bottom-24 left-0 right-0 flex justify-center z-50 pointer-events-none">
<button
type="button"
className="cursor-pointer pointer-events-auto"
onClick={scrollToBottom}
>
<div className="rounded-full p-2 bg-[#0D121A] shadow-[1.5px_1.5px_4.5px_0_rgba(0,0,0,0.70)_inset] hover:bg-[#0F1620] transition-colors">
<ChevronDownIcon className="size-4 text-white" />
</div>
</button>
</div>
)}
<ChatInput
value={input}
onChange={(e) => setInput(e.target.value)}

View file

@ -0,0 +1,9 @@
import { Streamdown } from "streamdown"
export function NotionDoc({ content }: { content: string }) {
return (
<div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
<Streamdown>{content}</Streamdown>
</div>
)
}

View file

@ -82,7 +82,7 @@ const PDFIcon = ({ className }: { className: string }) => {
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
@ -160,7 +160,7 @@ const TextDocumentIcon = ({ className }: { className: string }) => {
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"

View file

@ -211,7 +211,9 @@ export function GraphListMemories({
/>
</g>
</svg>
<p className="group-hover:text-white group-data-[state=active]:text-white">Graph</p>
<p className="group-hover:text-white group-data-[state=active]:text-white">
Graph
</p>
</TabsTrigger>
<TabsTrigger
value="list"
@ -233,11 +235,13 @@ export function GraphListMemories({
className="fill-[#737373] group-hover:fill-white group-data-[state=active]:fill-white"
/>
</svg>
<p className="group-hover:text-white group-data-[state=active]:text-white">List</p>
<p className="group-hover:text-white group-data-[state=active]:text-white">
List
</p>
</TabsTrigger>
</TabsList>
</Tabs>
<div className="grid grid-cols-2 gap-2 pt-3 overflow-y-auto pr-1">
<div className="grid grid-cols-2 gap-2 pt-3 overflow-y-auto pr-1 scrollbar-thin">
{memoryEntries.map((memory, idx) => {
const isClickable =
memory.url &&

View file

@ -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)) && (
<div className="p-4 overflow-y-auto flex-1">
{_document.content}
</div>
)}
{_document?.type === "text" && (
<div className="p-4 overflow-y-auto flex-1">
{_document.content}
</div>
)}
{_document?.type === "pdf" && <PdfViewer url={_document.url} />}
{_document?.type === "notion_doc" && (
<NotionDoc content={_document.content ?? ""} />
)}
{_document?.url?.includes("youtube.com") && (
<YoutubeVideo url={_document.url} />
)}

View file

@ -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) {
</span>
</Button>
{user && (
<Avatar
className="border border-border h-8 w-8 md:h-10 md:w-10"
onClick={() => router.push("/new/settings")}
>
<AvatarImage src={user?.image ?? ""} />
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
</Avatar>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10 cursor-pointer">
<AvatarImage src={user?.image ?? ""} />
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => router.push("/new/settings")}>
<Settings className="h-4 w-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => authClient.signOut()}>
<LogOut className="h-4 w-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>

View file

@ -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({
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
className={cn(
"w-[80%]! max-w-[900px]! h-[80%]! max-h-[600px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-3 rounded-[22px]",
"w-[80%]! max-w-[900px]! h-[80%]! max-h-[375px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-3 rounded-[22px]",
dmSansClassName(),
)}
style={{
@ -44,8 +45,8 @@ export function MCPModal({
</DialogPrimitive.Close>
</div>
</div>
<div className="w-full h-full p-4 rounded-[14px] bg-[#14161A] shadow-inside-out resize-none">
MCP steps
<div className="w-full px-4 py-4 rounded-[14px] bg-[#14161A] shadow-inside-out overflow-y-auto">
<MCPSteps variant="embedded" />
</div>
<DialogFooter className="justify-between!">
<div className="flex items-center gap-2">
@ -59,7 +60,9 @@ export function MCPModal({
Migrate from MCP v1
</Button>
</div>
<Button variant="insideOut" className="px-6 py-[10px]">Done</Button>
<Button variant="insideOut" className="px-6 py-[10px]">
Done
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -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<string>("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 (
<div
className={cn(
"w-full relative",
isEmbedded ? "h-full overflow-y-auto" : "max-w-2xl",
)}
>
<div
className="absolute left-4 top-0 w-px bg-[#1E293B] z-10"
style={{ height: activeStep === 3 ? isEmbedded ? "100%" : "calc(100% - 4rem)" : "100%" }}
/>
<div className="flex items-start space-x-4 z-20">
<button
type="button"
className={cn(
"rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium shrink-0 z-20 text-white",
selectedClient && "cursor-pointer hover:bg-[#1a2530]",
!selectedClient && "cursor-default",
activeStep === 1
? "border border-[#15233C] bg-[#08142D]"
: "bg-[#161F2B] ",
)}
onClick={() => {
if (selectedClient) {
setSelectedClient(null)
setActiveStep(1)
}
}}
onKeyDown={(e) => {
if (selectedClient && (e.key === "Enter" || e.key === " ")) {
e.preventDefault()
setSelectedClient(null)
setActiveStep(1)
}
}}
disabled={!selectedClient}
>
<span
className="text-lg"
style={
activeStep === 1
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
1
</span>
</button>
<div className="flex-1 mb-4">
<div className="flex gap-4 mb-4">
<button
type="button"
className={cn(
"text-white text-lg font-medium text-center",
selectedClient && "cursor-pointer hover:opacity-80",
!selectedClient && "cursor-default",
)}
onClick={() => {
if (selectedClient) {
setSelectedClient(null)
setActiveStep(1)
}
}}
onKeyDown={(e) => {
if (selectedClient && (e.key === "Enter" || e.key === " ")) {
e.preventDefault()
setSelectedClient(null)
setActiveStep(1)
}
}}
disabled={!selectedClient}
style={
activeStep === 1
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
Select your AI client
</button>
{selectedClient && (
<Select
onValueChange={(value) => {
setSelectedClient(value as keyof typeof clients)
setActiveStep(2)
}}
value={selectedClient || undefined}
>
<SelectTrigger
className="max-w-md rounded-full border-[#242A33] text-white hover:border-gray-600 bg-transparent!"
style={{
background:
"linear-gradient(0deg, #0A0E14 0%, #080B0F 100%)",
}}
>
{selectedClient ? (
<div className="flex items-center gap-2">
<Image
alt={clients[selectedClient]}
height={20}
width={20}
unoptimized
src={
selectedClient === "mcp-url"
? "/mcp-icon.svg"
: `/mcp-supported-tools/${selectedClient === "claude-code" ? "claude" : selectedClient}.png`
}
/>
<span>{clients[selectedClient]}</span>
</div>
) : (
<SelectValue placeholder="Select a client" />
)}
</SelectTrigger>
<SelectContent className="bg-black border-none">
{Object.entries(clients)
.slice(0, 7)
.map(([key, clientName]) => (
<SelectItem
key={key}
value={key}
className="text-white hover:bg-[#080B0F]"
>
<div className="flex items-center gap-2">
<Image
alt={clientName}
height={20}
width={20}
unoptimized
src={
key === "mcp-url"
? "/mcp-icon.svg"
: `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`
}
/>
<span>{clientName}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div
className={cn(
"flex flex-wrap gap-2 mb-4",
selectedClient ? "hidden" : "",
)}
>
{Object.entries(clients)
.slice(0, 7)
.map(([key, clientName]) => (
<button
key={key}
type="button"
onClick={() => {
setSelectedClient(key as keyof typeof clients)
setActiveStep(2)
}}
className={`mcp-client-button-group py-[6px] pl-2 pr-3 rounded-full border transition-colors cursor-pointer duration-200 ${
selectedClient === key
? "border-blue-500 bg-blue-500/10"
: "border-[#242A33] bg-[#080B0F] hover:border-[#3273FC4D] hover:bg-[#08142D]"
}`}
>
<div className="flex items-center space-x-1">
<div className="w-5 h-5 flex items-center justify-center">
<Image
alt={clientName}
unoptimized
className="rounded object-contain"
height={20}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const parent = target.parentElement
if (
parent &&
!parent.querySelector(".fallback-text")
) {
const fallback = document.createElement("span")
fallback.className =
"fallback-text text-xs font-bold text-white"
fallback.textContent = clientName
.substring(0, 2)
.toUpperCase()
parent.appendChild(fallback)
}
}}
src={
key === "mcp-url"
? "/mcp-icon.svg"
: `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`
}
width={20}
/>
</div>
<span className="mcp-client-gradient-text text-sm font-medium text-white">
{clientName}
</span>
</div>
</button>
))}
</div>
{!selectedClient && (
<p className={cn("text-[#8B8B8B] text-[14px]", dmSansClassName())}>
*You can connect to all of these, setup is different for each one
</p>
)}
</div>
</div>
<div className="flex items-start space-x-4">
<div
className={cn(
"rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium shrink-0 z-20 text-white",
activeStep === 2
? "border border-[#15233C] bg-[#08142D]"
: "bg-[#161F2B]",
)}
>
<span
className="text-lg"
style={
activeStep === 2
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
2
</span>
</div>
<div className="flex-1 mb-4">
<h3
className="text-white text-lg font-medium mb-4"
style={
activeStep === 2
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
Copy the installation command
</h3>
{selectedClient && (
<div className="space-y-3">
{selectedClient === "mcp-url" ? (
<div className="space-y-4">
<div className="flex justify-end">
<div className="flex bg-[#0D121A] rounded-full p-1 border border-gray-600">
<button
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${
mcpUrlTab === "oneClick"
? "bg-[#080B0F] text-white border border-gray-600"
: "text-gray-400 hover:text-white"
}`}
onClick={() => setMcpUrlTab("oneClick")}
type="button"
>
Quick Setup
</button>
<button
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${
mcpUrlTab === "manual"
? "bg-[#080B0F] text-white border border-gray-600"
: "text-gray-400 hover:text-white"
}`}
onClick={() => setMcpUrlTab("manual")}
type="button"
>
Manual Config
</button>
</div>
</div>
{mcpUrlTab === "oneClick" ? (
<div className="space-y-2">
<p className="text-sm text-gray-400">
Use this URL to quickly configure supermemory in your AI
assistant
</p>
<div className="relative">
<input
className="font-mono text-xs w-full pr-10 p-2 bg-black border border-gray-600 rounded text-green-400"
readOnly
value="https://api.supermemory.ai/mcp"
/>
<button
type="button"
className="absolute top-1 right-1 cursor-pointer p-1"
onClick={() => {
navigator.clipboard.writeText(
"https://api.supermemory.ai/mcp",
)
analytics.mcpInstallCmdCopied()
toast.success("Copied to clipboard!")
setActiveStep(3)
}}
>
<CopyIcon className="size-4 text-gray-400 hover:text-white" />
</button>
</div>
</div>
) : (
<div className="space-y-3">
<p className="text-sm text-gray-400">
Add this configuration to your MCP settings file with
authentication
</p>
<div className="relative">
<pre className="bg-black border border-gray-600 rounded-lg p-4 pr-12 text-xs overflow-x-auto max-w-full">
<code className="font-mono block whitespace-pre-wrap break-all text-green-400">
{`{
"supermemory-mcp": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"],
"env": {},
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}`}
</code>
</pre>
<button
type="button"
className="absolute top-2 right-2 cursor-pointer h-8 w-8 p-0 bg-[#0D121A] hover:bg-[#1a1a1a] rounded"
onClick={() => {
const config = `{
"supermemory-mcp": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"],
"env": {},
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}`
navigator.clipboard.writeText(config)
analytics.mcpInstallCmdCopied()
toast.success("Copied to clipboard!")
setIsCopied(true)
setActiveStep(3)
setTimeout(() => setIsCopied(false), 2000)
}}
>
{isCopied ? (
<span className="text-green-600"></span>
) : (
<CopyIcon className="size-3.5" />
)}
</button>
</div>
<p className="text-xs text-gray-400">
The API key is included as a Bearer token in the
Authorization header
</p>
</div>
)}
</div>
) : (
<div className="space-y-3">
<div className="relative">
<input
className={cn(
"text-xs w-full pr-24 py-4 bg-[#0D121A] rounded-xl text-white pl-3",
dmMonoClassName(),
)}
style={{
border: "1px solid rgba(61, 67, 77, 0.10)",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}}
readOnly
value={generateInstallCommand()}
/>
<button
type="button"
className={cn(
"absolute top-[5px] right-1 cursor-pointer p-1 flex items-center rounded-[10px] px-3 py-2 gap-2",
dmSansClassName(),
)}
style={{
background:
"linear-gradient(180deg, #267BF1 40.23%, #15468B 100%), linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
border: "1px solid #000",
}}
onClick={copyToClipboard}
>
{isCopied ? (
<>
<Check className="size-4 text-white" />
<span className="text-white">Copied</span>
</>
) : (
<>
<CopyIcon className="size-[20px] text-white stroke-[2px]" />
<span className="text-white">Copy</span>
</>
)}
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
<div className="flex items-start space-x-4">
<div
className={cn(
"rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium shrink-0 z-20 text-white",
activeStep === 3
? "border border-[#15233C] bg-[#08142D]"
: "bg-[#161F2B] ",
)}
>
<span
className="text-lg"
style={
activeStep === 3
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
3
</span>
</div>
<div className="flex-1 space-y-4">
<h3
className="text-white text-lg font-medium"
style={
activeStep === 3
? {
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
: undefined
}
>
Run command in your terminal
</h3>
{activeStep === 3 && (
<p
className={cn(
"font-mono text-xs w-full pr-10 p-4 px-2 bg-[#0D121A] rounded-xl text-white pl-3 flex items-center gap-2",
dmMonoClassName(),
)}
style={{
border: "1px solid rgba(61, 67, 77, 0.10)",
}}
>
<SyncLogoIcon className="size-4" />
Waiting for installation
</p>
)}
</div>
</div>
</div>
)
}
interface MCPDetailViewProps {
onBack: () => void
}
export function MCPDetailView({ onBack }: MCPDetailViewProps) {
return (
<div className="flex flex-col h-full p-8">
<div className="mb-6">
<Button
variant="link"
className="text-white hover:text-gray-300 p-0 hover:no-underline cursor-pointer"
onClick={onBack}
>
Back
</Button>
</div>
<div className="flex-1 flex flex-col items-start justify-start">
<h1 className="text-white text-xl font-medium mb-4 text-start">
Connect your AI to supermemory MCP
</h1>
<div className="mb-12 space-x-4 flex max-w-2xl">
<div
className={cn(
"flex items-start space-x-3 w-[200px]",
dmSansClassName(),
)}
>
<CircleCheckIcon className="size-4 text-green-500 shrink-0 mt-0.5" />
<p className="text-[#8B8B8B] text-sm">
MCP connects your AI apps to create and use memories directly
</p>
</div>
<div
className={cn(
"flex items-start space-x-3 w-[200px]",
dmSansClassName(),
)}
>
<CircleCheckIcon className="size-4 text-green-500 shrink-0 mt-0.5" />
<p className="text-[#8B8B8B] text-sm">
Auto-fetch the right context from anything you've saved
</p>
</div>
<div
className={cn(
"flex items-start space-x-3 w-[200px]",
dmSansClassName(),
)}
>
<CircleCheckIcon className="size-4 text-green-500 shrink-0 mt-0.5" />
<p className="text-[#8B8B8B] text-sm">
One-time setup, <br /> seamless integration across your workflow
</p>
</div>
</div>
<MCPSteps variant="full" />
</div>
</div>
)
}

View file

@ -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<void> => {
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 (
<div className="h-full">
<div
className="h-full"
>
<Button
className={cn(
dmSansClassName(),
@ -180,7 +182,7 @@ export function MemoriesGrid() {
) : (
<div className="h-full overflow-auto scrollbar-thin">
<Masonry
key={`masonry-${documents.length}-${documents.map((d) => d.id).join(",")}`}
key={`masonry-${documents.length}-${documents.map((d) => d.id).join(",")}-${isChatOpen}`}
items={documents}
render={renderDocumentCard}
columnGutter={0}
@ -192,7 +194,7 @@ export function MemoriesGrid() {
onRender={maybeLoadMore}
/>
{isLoadingMore && (
{isFetchingNextPage && (
<div className="py-8 flex items-center justify-center">
<SuperLoader />
</div>

View file

@ -12,7 +12,7 @@ export default async function proxy(request: Request) {
console.debug("[PROXY] Session cookie exists:", !!sessionCookie)
// Always allow access to login and waitlist pages
const publicPaths = ["/login"]
const publicPaths = ["/login", "/login/new"]
if (publicPaths.includes(url.pathname)) {
console.debug("[PROXY] Public path, allowing access")
return NextResponse.next()

View file

@ -2,75 +2,33 @@
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 { InitialHeader } from "../../../apps/web/components/initial-header";
import Autoplay from "embla-carousel-autoplay";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { dmSansClassName } from "../../../apps/web/utils/fonts";
import { cn } from "@lib/utils";
import { Logo } from "@ui/assets/Logo";
function AnimatedGradientBackground() {
return (
<div className="fixed inset-0 z-0 overflow-hidden">
<motion.div
className="absolute top-[20%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-0.png')] bg-size-[150%_auto] bg-top bg-no-repeat"
initial={{ y: "100%" }}
animate={{
y: 0,
opacity: [1, 0, 1],
}}
transition={{
y: { duration: 0.75, ease: "easeOut" },
opacity: { duration: 8, repeat: Infinity, ease: "easeInOut" },
}}
/>
<motion.div
className="absolute top-[20%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-1.png')] bg-size-[150%_auto] bg-top bg-no-repeat"
initial={{ y: "100%" }}
animate={{
y: 0,
opacity: [0, 1, 0],
}}
transition={{
y: { duration: 0.75, ease: "easeOut" },
opacity: { duration: 8, repeat: Infinity, ease: "easeInOut" },
}}
/>
<motion.div
className="absolute top-0 left-0 right-0 bottom-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat"
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",
opacity: 0.4,
}}
/>
</div>
);
}
function LoginCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
className="flex py-8 px-11 flex-col items-start gap-2 rounded-[22px] bg-linear-to-b from-[#06101F] to-[#030912] shadow-[1.5px_1.5px_20px_0_rgba(0,0,0,0.65),1px_1.5px_2px_0_rgba(128,189,255,0.07)_inset,-0.5px_-1.5px_4px_0_rgba(0,35,73,0.40)_inset]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.75, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
export function LoginPage() {
export function LoginPage({
heroText = "The unified memory API for the AI era.",
texts = [
"Stop building retrieval from scratch.",
"Trusted by Open Source, enterprise and developers.",
],
}) {
const [email, setEmail] = useState("");
const [submittedEmail, setSubmittedEmail] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
@ -210,273 +168,316 @@ export function LoginPage() {
};
return (
<main className="relative h-screen overflow-hidden">
<AnimatedGradientBackground />
<div className="relative z-10">
<InitialHeader />
<section className="flex flex-col items-center justify-center p-4 space-y-12 sm:p-6 md:p-8 lg:px-20 lg:py-12.5 min-h-[calc(100vh-80px)]">
<div className="text-center">
<div className="text-5xl font-medium">
Never forget anything, anywhere
<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">
<Carousel
className="hidden lg:block lg:col-span-6"
opts={{
loop: true,
}}
plugins={[Autoplay({ delay: 5000 })]}
>
<CarouselContent>
<CarouselItem className="relative">
<Image
alt="supermemory abstract 2d"
height={600}
src="/images/login-carousel-1.png"
width={600}
/>
<div className="absolute inset-0 flex flex-col justify-end p-6 lg:p-12">
<Title1Bold className="text-white mb-2 leading-tight">
{texts[0]}
</Title1Bold>
</div>
<div className="text-5xl font-medium">with supermemory</div>
</CarouselItem>
<CarouselItem className="relative">
<Image
alt="supermemory abstract 3d"
height={600}
src="/images/login-carousel-2.png"
width={600}
/>
<div className="absolute inset-0 flex flex-col justify-end p-6 lg:p-12">
<Title1Bold className="text-white mb-2 leading-tight">
{texts[1]}
</Title1Bold>
</div>
</CarouselItem>
</CarouselContent>
</Carousel>
{submittedEmail ? (
<div className="w-full max-w-md lg:max-w-none lg:col-span-5 flex flex-col gap-4 lg:gap-6 min-h-2/3 ">
<div className="flex flex-col gap-2 text-center lg:text-left">
<Title1Bold className="text-foreground">Almost there!</Title1Bold>
<HeadingH3Medium className="text-muted-foreground">
Click the magic link we've sent to{" "}
<span className="text-foreground">{submittedEmail}</span>.
</HeadingH3Medium>
</div>
{submittedEmail ? (
<LoginCard>
<div className="w-[360px] flex flex-col gap-4 lg:gap-6 min-h-2/3">
<div className="flex flex-col gap-2 text-center lg:text-left">
<Title1Bold className="text-foreground">
Almost there!
</Title1Bold>
<HeadingH3Medium className="text-muted-foreground">
Click the magic link we've sent to{" "}
<span className="text-foreground">{submittedEmail}</span>.
</HeadingH3Medium>
</div>
<TextSeparator text="OR" className={cn(dmSansClassName())} />
<TextSeparator text="OR" />
<form
className="flex flex-col gap-4 lg:gap-6"
onSubmit={handleSubmitToken}
>
<LabeledInput
inputPlaceholder="your temporary login code"
inputProps={{
name: "token",
required: true,
disabled: isLoading,
"aria-invalid": error ? "true" : "false",
}}
inputType="text"
label="Enter code"
/>
<form
className="flex flex-col gap-4 lg:gap-6"
onSubmit={handleSubmitToken}
>
<LabeledInput
inputPlaceholder="your temporary login code"
inputProps={{
name: "token",
required: true,
disabled: isLoading,
"aria-invalid": error ? "true" : "false",
}}
inputType="text"
label="Enter code"
/>
<Button disabled={isLoading} id="verify-token" type="submit">
Verify Token
</Button>
</form>
</div>
</LoginCard>
) : (
<LoginCard>
<div
className="w-[360px] flex flex-col"
style={{ gap: "12px" }}
>
{params.get("error") && (
<div className="text-red-500">
Error: {params.get("error")}. Please try again!
<Button disabled={isLoading} id="verify-token" type="submit">
Verify Token
</Button>
</form>
</div>
) : (
<div className="w-full max-w-md lg:max-w-none lg:col-span-5 flex flex-col gap-4 lg:gap-6 min-h-2/3 ">
<div className="flex flex-col gap-2 text-center lg:text-left md:mb-12">
<Title1Bold className="text-foreground flex flex-col justify-center md:justify-start md:flex-row items-center gap-3">
<span className="block md:hidden">Welcome to </span>{" "}
<LogoFull className="h-8" />
</Title1Bold>
<HeadingH1Medium className="text-muted-foreground">
{heroText}
</HeadingH1Medium>
</div>
{params.get("error") && (
<div className="text-red-500">
Error: {params.get("error")}. Please try again!
</div>
)}
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-4 lg:gap-6">
<LabeledInput
error={error}
inputPlaceholder="your@email.com"
inputProps={{
"aria-invalid": error ? "true" : "false",
disabled: isLoading,
id: "email",
onChange: (e) => {
setEmail(e.target.value);
error && setError(null);
},
required: true,
value: email,
}}
inputType="email"
label="Email"
/>
<div className="relative">
<Button className="w-full" disabled={isLoading} type="submit">
{isLoadingEmail
? "Sending login link..."
: "Log in to supermemory"}
</Button>
{lastUsedMethod === "magic_link" && (
<div className="absolute -top-2 -right-2">
<Badge variant="default" className="text-xs">
Last used
</Badge>
</div>
)}
<div className="flex flex-col gap-3">
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
!process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? (
<div className="relative grow">
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
height="25"
viewBox="0 0 24 25"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Google</title>
<path
d="M21.8055 10.2563H21V10.2148H12V14.2148H17.6515C16.827 16.5433 14.6115 18.2148 12 18.2148C8.6865 18.2148 6 15.5283 6 12.2148C6 8.90134 8.6865 6.21484 12 6.21484C13.5295 6.21484 14.921 6.79184 15.9805 7.73434L18.809 4.90584C17.023 3.24134 14.634 2.21484 12 2.21484C6.4775 2.21484 2 6.69234 2 12.2148C2 17.7373 6.4775 22.2148 12 22.2148C17.5225 22.2148 22 17.7373 22 12.2148C22 11.5443 21.931 10.8898 21.8055 10.2563Z"
fill="#FFC107"
/>
<path
d="M3.15234 7.56034L6.43784 9.96984C7.32684 7.76884 9.47984 6.21484 11.9993 6.21484C13.5288 6.21484 14.9203 6.79184 15.9798 7.73434L18.8083 4.90584C17.0223 3.24134 14.6333 2.21484 11.9993 2.21484C8.15834 2.21484 4.82734 4.38334 3.15234 7.56034Z"
fill="#FF3D00"
/>
<path
d="M12.0002 22.2152C14.5832 22.2152 16.9302 21.2267 18.7047 19.6192L15.6097 17.0002C14.5721 17.7897 13.3039 18.2166 12.0002 18.2152C9.39916 18.2152 7.19066 16.5567 6.35866 14.2422L3.09766 16.7547C4.75266 19.9932 8.11366 22.2152 12.0002 22.2152Z"
fill="#4CAF50"
/>
<path
d="M21.8055 10.2563H21V10.2148H12V14.2148H17.6515C17.2571 15.3231 16.5467 16.2914 15.608 17.0003L15.6095 16.9993L18.7045 19.6183C18.4855 19.8173 22 17.2148 22 12.2148C22 11.5443 21.931 10.8898 21.8055 10.2563Z"
fill="#1976D2"
/>
</svg>
}
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" && (
<div className="absolute -top-2 -right-2">
<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 ? (
<div className="relative grow">
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5 text-foreground"
fill="none"
height="25"
viewBox="0 0 26 25"
width="26"
xmlns="http://www.w3.org/2000/svg"
>
<title>Github</title>
<g clipPath="url(#clip0_2579_3356)">
<path
clipRule="evenodd"
d="M12.9635 0.214844C6.20975 0.214844 0.75 5.71484 0.75 12.5191C0.75 17.9581 4.24825 22.5621 9.10125 24.1916C9.708 24.3141 9.93025 23.9268 9.93025 23.6011C9.93025 23.3158 9.91025 22.3381 9.91025 21.3193C6.51275 22.0528 5.80525 19.8526 5.80525 19.8526C5.25925 18.4266 4.45025 18.0601 4.45025 18.0601C3.33825 17.3063 4.53125 17.3063 4.53125 17.3063C5.76475 17.3878 6.412 18.5693 6.412 18.5693C7.50375 20.4433 9.263 19.9138 9.97075 19.5878C10.0718 18.7933 10.3955 18.2433 10.7393 17.9378C8.0295 17.6526 5.1785 16.5933 5.1785 11.8671C5.1785 10.5226 5.6635 9.42259 6.432 8.56709C6.31075 8.26159 5.886 6.99834 6.5535 5.30759C6.5535 5.30759 7.58475 4.98159 9.91 6.57059C10.9055 6.30126 11.9322 6.16425 12.9635 6.16309C13.9948 6.16309 15.046 6.30584 16.0168 6.57059C18.3423 4.98159 19.3735 5.30759 19.3735 5.30759C20.041 6.99834 19.616 8.26159 19.4948 8.56709C20.2835 9.42259 20.7485 10.5226 20.7485 11.8671C20.7485 16.5933 17.8975 17.6321 15.1675 17.9378C15.6125 18.3248 15.9965 19.0581 15.9965 20.2193C15.9965 21.8693 15.9765 23.1936 15.9765 23.6008C15.9765 23.9268 16.199 24.3141 16.8055 24.1918C21.6585 22.5618 25.1568 17.9581 25.1568 12.5191C25.1768 5.71484 19.697 0.214844 12.9635 0.214844Z"
fill="currentColor"
fillRule="evenodd"
/>
</g>
<defs>
<clipPath id="clip0_2579_3356">
<rect
fill="currentColor"
height="24"
transform="translate(0.75 0.214844)"
width="24.5"
/>
</clipPath>
</defs>
</svg>
}
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" && (
<div className="absolute -top-2 -right-2">
<Badge variant="default" className="text-xs">
Last used
</Badge>
</div>
)}
</div>
) : null}
</div>
<TextSeparator text="OR" className={cn(dmSansClassName())} />
<div className="flex flex-col gap-6">
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<LabeledInput
error={error}
inputPlaceholder="your@email.com"
inputProps={{
"aria-invalid": error ? "true" : "false",
disabled: isLoading,
id: "email",
onChange: (e) => {
setEmail(e.target.value);
error && setError(null);
},
required: true,
value: email,
}}
inputType="email"
/>
<div className="relative">
<Button
className="flex justify-center items-center w-full h-[44px] relative gap-3 p-2 rounded-xl"
style={{
background:
"linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
boxShadow:
"1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
}}
disabled={isLoading}
type="submit"
>
<Logo className="size-4" />
{isLoadingEmail
? "Sending login link..."
: "Log in with Supermemory"}
</Button>
{lastUsedMethod === "magic_link" && (
<div className="absolute -top-2 -right-2">
<Badge variant="default" className="text-xs">
Last used
</Badge>
</div>
)}
</div>
</form>
<Label1Regular
className={cn(
"text-center text-xs! text-[#737373B2]",
dmSansClassName(),
)}
>
By continuing, you agree to our{" "}
<span className="inline-block">
<a
className="underline"
href="https://supermemory.ai/terms-of-service"
>
Terms
</a>{" "}
and{" "}
<a
className="underline"
href="https://supermemory.ai/privacy-policy"
>
Privacy Policy
</a>
.
</span>
</Label1Regular>
</div>
</div>
</LoginCard>
)}
</section>
</div>
</main>
</div>
</form>
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
!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 ? (
<div className="relative flex-grow">
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
height="25"
viewBox="0 0 24 25"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Google</title>
<path
d="M21.8055 10.2563H21V10.2148H12V14.2148H17.6515C16.827 16.5433 14.6115 18.2148 12 18.2148C8.6865 18.2148 6 15.5283 6 12.2148C6 8.90134 8.6865 6.21484 12 6.21484C13.5295 6.21484 14.921 6.79184 15.9805 7.73434L18.809 4.90584C17.023 3.24134 14.634 2.21484 12 2.21484C6.4775 2.21484 2 6.69234 2 12.2148C2 17.7373 6.4775 22.2148 12 22.2148C17.5225 22.2148 22 17.7373 22 12.2148C22 11.5443 21.931 10.8898 21.8055 10.2563Z"
fill="#FFC107"
/>
<path
d="M3.15234 7.56034L6.43784 9.96984C7.32684 7.76884 9.47984 6.21484 11.9993 6.21484C13.5288 6.21484 14.9203 6.79184 15.9798 7.73434L18.8083 4.90584C17.0223 3.24134 14.6333 2.21484 11.9993 2.21484C8.15834 2.21484 4.82734 4.38334 3.15234 7.56034Z"
fill="#FF3D00"
/>
<path
d="M12.0002 22.2152C14.5832 22.2152 16.9302 21.2267 18.7047 19.6192L15.6097 17.0002C14.5721 17.7897 13.3039 18.2166 12.0002 18.2152C9.39916 18.2152 7.19066 16.5567 6.35866 14.2422L3.09766 16.7547C4.75266 19.9932 8.11366 22.2152 12.0002 22.2152Z"
fill="#4CAF50"
/>
<path
d="M21.8055 10.2563H21V10.2148H12V14.2148H17.6515C17.2571 15.3231 16.5467 16.2914 15.608 17.0003L15.6095 16.9993L18.7045 19.6183C18.4855 19.8173 22 17.2148 22 12.2148C22 11.5443 21.931 10.8898 21.8055 10.2563Z"
fill="#1976D2"
/>
</svg>
}
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" && (
<div className="absolute -top-2 -right-2">
<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 ? (
<div className="relative flex-grow">
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5 text-foreground"
fill="none"
height="25"
viewBox="0 0 26 25"
width="26"
xmlns="http://www.w3.org/2000/svg"
>
<title>Github</title>
<g clipPath="url(#clip0_2579_3356)">
<path
clipRule="evenodd"
d="M12.9635 0.214844C6.20975 0.214844 0.75 5.71484 0.75 12.5191C0.75 17.9581 4.24825 22.5621 9.10125 24.1916C9.708 24.3141 9.93025 23.9268 9.93025 23.6011C9.93025 23.3158 9.91025 22.3381 9.91025 21.3193C6.51275 22.0528 5.80525 19.8526 5.80525 19.8526C5.25925 18.4266 4.45025 18.0601 4.45025 18.0601C3.33825 17.3063 4.53125 17.3063 4.53125 17.3063C5.76475 17.3878 6.412 18.5693 6.412 18.5693C7.50375 20.4433 9.263 19.9138 9.97075 19.5878C10.0718 18.7933 10.3955 18.2433 10.7393 17.9378C8.0295 17.6526 5.1785 16.5933 5.1785 11.8671C5.1785 10.5226 5.6635 9.42259 6.432 8.56709C6.31075 8.26159 5.886 6.99834 6.5535 5.30759C6.5535 5.30759 7.58475 4.98159 9.91 6.57059C10.9055 6.30126 11.9322 6.16425 12.9635 6.16309C13.9948 6.16309 15.046 6.30584 16.0168 6.57059C18.3423 4.98159 19.3735 5.30759 19.3735 5.30759C20.041 6.99834 19.616 8.26159 19.4948 8.56709C20.2835 9.42259 20.7485 10.5226 20.7485 11.8671C20.7485 16.5933 17.8975 17.6321 15.1675 17.9378C15.6125 18.3248 15.9965 19.0581 15.9965 20.2193C15.9965 21.8693 15.9765 23.1936 15.9765 23.6008C15.9765 23.9268 16.199 24.3141 16.8055 24.1918C21.6585 22.5618 25.1568 17.9581 25.1568 12.5191C25.1768 5.71484 19.697 0.214844 12.9635 0.214844Z"
fill="currentColor"
fillRule="evenodd"
/>
</g>
<defs>
<clipPath id="clip0_2579_3356">
<rect
fill="currentColor"
height="24"
transform="translate(0.75 0.214844)"
width="24.5"
/>
</clipPath>
</defs>
</svg>
}
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" && (
<div className="absolute -top-2 -right-2">
<Badge variant="default" className="text-xs">
Last used
</Badge>
</div>
)}
</div>
) : null}
</div>
<Label1Regular className="text-muted-foreground text-center text-xs sm:text-sm">
By continuing, you agree to our{" "}
<span className="inline-block">
<a
className="text-foreground hover:underline"
href="https://supermemory.ai/terms-of-service"
>
Terms
</a>{" "}
and{" "}
<a
className="text-foreground hover:underline"
href="https://supermemory.ai/privacy-policy"
>
Privacy Policy
</a>
.
</span>
</Label1Regular>
</div>
)}
</section>
);
}
}