feat: new design, new vibes

This commit is contained in:
Mahesh Sanikommmu 2025-10-14 21:57:59 -07:00
parent 6efac9f4f5
commit 38d83dcbce
45 changed files with 1760 additions and 332 deletions

View file

@ -42,7 +42,7 @@ export default function Page() {
useEffect(() => {
if (user && !onboardingLoading && shouldShowOnboarding()) {
router.push("/onboarding")
router.push("/onboarding/welcome")
}
}, [user, shouldShowOnboarding, onboardingLoading, router])

View file

@ -0,0 +1,117 @@
"use client"
import { useState } from "react"
import { motion } from "motion/react"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
import { PanelRightCloseIcon, SendIcon } from "lucide-react"
export function ChatSidebar() {
const [message, setMessage] = useState("")
const [isChatOpen, setIsChatOpen] = useState(true)
const handleSend = () => {
console.log("Message:", message)
setMessage("")
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
const toggleChat = () => {
setIsChatOpen(!isChatOpen)
}
if (!isChatOpen) {
return (
<motion.div
className="absolute top-0 right-0 flex items-start justify-start m-4"
initial={{ x: "100%" }}
animate={{ x: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<motion.button
onClick={toggleChat}
className="flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium border-[1px] border-[#17181A]"
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
whileTap={{ scale: 0.95 }}
layoutId="chat-toggle-button"
>
Chat with Nova
</motion.button>
</motion.div>
)
}
return (
<motion.div
className="w-[450px] h-[calc(100vh-110px)] bg-[#0A0E14] backdrop-blur-md flex flex-col rounded-2xl m-4"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%", opacity: 0, transition: { duration: 1, ease: "easeOut" } }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<motion.button
onClick={toggleChat}
className="absolute top-4 right-4 flex items-center gap-2 rounded-full p-2 text-xs"
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
layoutId="chat-toggle-button"
>
<PanelRightCloseIcon className="size-4" />
Close chat
</motion.button>
<div className="flex-1 flex items-end justify-start px-4">
<div className="flex flex-col items-center gap-4">
<motion.div
className="flex items-center gap-2 text-foreground/70"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<NovaOrb size={24} />
<span className="text-sm">Waiting for your input</span>
</motion.div>
</div>
</div>
<div className="p-4">
<form
className="flex flex-col gap-3 bg-[#0D121A] rounded-xl p-2 relative"
onSubmit={(e) => {
e.preventDefault()
if (message.trim()) {
handleSend()
}
}}
>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Chat with your Supermemory"
className="w-full text-foreground placeholder:text-foreground/20 rounded-sm outline-none resize-none text-base leading-relaxed bg-transparent px-2 h-10"
/>
<div className="flex justify-end absolute bottom-3 right-2">
<Button
type="submit"
disabled={!message.trim()}
className="text-foreground/20 hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all"
size="icon"
>
<SendIcon className="size-4" />
</Button>
</div>
</form>
</div>
</motion.div>
)
}

View file

@ -0,0 +1,43 @@
import { motion } from "motion/react"
import { Logo } from "@ui/assets/Logo"
import { useAuth } from "@lib/auth-context"
import { useEffect, useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
export function SetupHeader() {
const { user } = useAuth()
const [name, setName] = useState<string>("")
useEffect(() => {
const storedName =
localStorage.getItem("username") || localStorage.getItem("userName") || ""
setName(storedName)
}, [])
return (
<motion.div
className="flex p-6 justify-between items-center"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<div className="flex items-center">
<Logo className="h-7" />
{name && (
<div className="flex flex-col items-start justify-center ml-2">
<p className="text-[#8B8B8B] text-xs leading-tight">{name}'s</p>
<p className="text-white font-bold text-xl leading-none -mt-1">
supermemory
</p>
</div>
)}
</div>
{user && (
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10">
<AvatarImage src={user?.image ?? ""} />
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
</Avatar>
)}
</motion.div>
)
}

View file

@ -0,0 +1,133 @@
"use client"
import { useState } from "react"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/mcp-detail-view"
const integrationCards = [
{
title: "Capture",
description: "Add the Chrome extension for one-click saves",
icon: (
<div className="rounded-full flex items-center justify-center">
<img
src="/onboarding/chrome.png"
alt="Chrome"
className="w-20 h-auto"
/>
</div>
),
},
{
title: "Connect to AI",
description: "Set up once and use your memory in Cursor, Claude, etc",
icon: (
<div className="rounded flex items-center justify-center">
<img src="/mcp-icon.svg" alt="MCP" className="size-14" />
</div>
),
},
{
title: "Connect",
description: "Link Notion, Google Drive, or OneDrive to import your docs",
icon: (
<div className="rounded flex items-center justify-center">
<img
src="/onboarding/connectors.png"
alt="Connectors"
className="w-20 h-auto"
/>
</div>
),
},
{
title: "Import",
description:
"Bring in X/Twitter bookmarks, and turn them into useful memories",
icon: (
<div className="rounded flex items-center justify-center">
<img src="/onboarding/x.png" alt="X" className="size-14" />
</div>
),
},
]
interface IntegrationsStepProps {
onBack: () => void
}
export function IntegrationsStep({ onBack }: IntegrationsStepProps) {
const [selectedCard, setSelectedCard] = useState<string | null>(null)
if (selectedCard === "Connect to AI") {
return <MCPDetailView onBack={() => setSelectedCard(null)} />
}
return (
<div className="flex flex-col items-center justify-center h-full p-8">
<div className="text-center mb-6 flex flex-col items-center justify-center space-y-2">
<h1 className="text-white text-3xl font-medium">
Build your personal memory
</h1>
<p className="text-white text-sm opacity-60 max-w-xs">
Your supermemory comes alive when you capture and connect what's
important
</p>
</div>
<div className="grid grid-cols-2 gap-2 max-w-lg w-full mb-12">
{integrationCards.map((card) => {
const isClickable = card.title === "Connect to AI"
if (isClickable) {
return (
<button
key={card.title}
type="button"
className="bg-[#080B0F] relative rounded-lg p-4 hover:border-gray-600 transition-colors duration-300 border-2 border-[#0D121A] cursor-pointer text-left w-full hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:250%_auto] hover:bg-[center_top_3rem] hover:bg-no-repeat"
onClick={() => setSelectedCard("Connect to AI")}
>
<div className="flex-1 mt-10">
<h3 className="text-white text-sm font-medium">
{card.title}
</h3>
<p className="text-gray-400 text-xs leading-relaxed">
{card.description}
</p>
</div>
<div className="absolute top-0 right-0">{card.icon}</div>
</button>
)
}
return (
<div
key={card.title}
className="bg-[#080B0F] relative rounded-lg p-4 hover:border-gray-600 transition-colors duration-300 border-2 border-[#0D121A] hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:250%_auto] hover:bg-[center_top_3rem] hover:bg-no-repeat"
>
<div className="flex-1 mt-10">
<h3 className="text-white text-sm font-medium">{card.title}</h3>
<p className="text-gray-400 text-xs leading-relaxed">
{card.description}
</p>
</div>
<div className="absolute top-0 right-0">{card.icon}</div>
</div>
)
})}
</div>
<div className="flex justify-between w-full max-w-4xl">
<Button
variant="link"
className="text-white hover:text-gray-300"
onClick={onBack}
>
Back
</Button>
<Button variant="link" className="text-white hover:text-gray-300">
Continue
</Button>
</div>
</div>
)
}

View file

@ -0,0 +1,94 @@
"use client"
import { SetupHeader } from "./header"
import { AnimatePresence, motion } from "motion/react"
import { RelatableQuestion } from "./relatable-question"
import { IntegrationsStep } from "./integrations-step"
import { ChatSidebar } from "./chat-sidebar"
import { useState } from "react"
function AnimatedGradientBackground() {
return (
<div className="fixed inset-0 z-0 overflow-hidden">
<motion.div
className="absolute top-[40%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-0.png')] bg-[length: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-[40%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-1.png')] bg-[length: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"
initial={{ y: "100%" }}
animate={{ y: 0 }}
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",
opacity: 0.6,
}}
/>
</div>
)
}
export default function SetupPage() {
const [currentStep, setCurrentStep] = useState<"relatable" | "integrations">(
"relatable",
)
const handleContinueOrSkip = () => {
setCurrentStep("integrations")
}
const handleBack = () => {
setCurrentStep("relatable")
}
return (
<main className="relative min-h-screen bg-[#080A0D]">
<AnimatedGradientBackground />
<div className="relative z-10">
<SetupHeader />
<div className="flex flex-row h-[calc(100vh-90px)] relative">
<div className="flex-1 flex flex-col items-center justify-start p-8">
{currentStep === "relatable" && (
<RelatableQuestion onContinueOrSkip={handleContinueOrSkip} />
)}
{currentStep === "integrations" && (
<IntegrationsStep onBack={handleBack} />
)}
</div>
<AnimatePresence mode="popLayout">
<ChatSidebar />
</AnimatePresence>
</div>
</div>
</main>
)
}

View file

@ -0,0 +1,110 @@
"use client"
import { useState } from "react"
import { motion } from "motion/react"
import { Button } from "@ui/components/button"
const relatableOptions = [
{
emoji: "😔",
text: "I always forget what I save in my twitter bookmarks",
},
{
emoji: "😭",
text: "Going through e-books manually is so tedious",
},
{
emoji: "🥲",
text: "I always have to feed every AI app with my data",
},
{
emoji: "😵‍💫",
text: "Referring meeting notes makes my AI chat hallucinate",
},
{
emoji: "🫤",
text: "I save nothing on my browser, it's just useless",
},
]
interface RelatableQuestionProps {
onContinueOrSkip: () => void
}
export function RelatableQuestion({
onContinueOrSkip,
}: RelatableQuestionProps) {
const [selectedOptions, setSelectedOptions] = useState<number[]>([])
return (
<motion.div
className="flex flex-col items-center justify-center h-full"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.6 }}
>
<motion.h1
className="text-white text-3xl font-medium mb-6 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
Which of these sound most relatable?
</motion.h1>
<div className="flex flex-wrap justify-center gap-4 max-w-7xl">
{relatableOptions.map((option, index) => (
<button
key={option.text}
className={`
relative rounded-lg p-2 cursor-pointer transition-all duration-300 opacity-50 hover:opacity-100 border-[#0D121A]
${
selectedOptions.includes(index)
? "border-[#3374FF] border-[0.1px] opacity-100 bg-[url('/onboarding/bg-gradient-1.png')] bg-[length:250%_auto] bg-[center_top_3rem] bg-no-repeat"
: "border-2 bg-[#080B0F] hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:250%_auto] hover:bg-[center_top_3rem] hover:bg-no-repeat"
}
`}
onClick={() => {
setSelectedOptions((prev) =>
prev.includes(index)
? prev.filter((i) => i !== index)
: [...prev, index],
)
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
setSelectedOptions((prev) =>
prev.includes(index)
? prev.filter((i) => i !== index)
: [...prev, index],
)
}
}}
type="button"
>
<div className="flex flex-col items-start justify-between space-y-8">
<span className="text-2xl">{option.emoji}</span>
<p className="text-white text-xs leading-relaxed max-w-[115px] align-bottom text-left">
{option.text}
</p>
</div>
</button>
))}
</div>
<div className="flex gap-4 my-8">
<div key={selectedOptions.length === 0 ? "skip" : "continue"}>
<Button
className="text-foreground font-medium"
variant="link"
size="lg"
onClick={onContinueOrSkip}
>
{selectedOptions.length === 0 ? "Skip for now →" : "Continue →"}
</Button>
</div>
</div>
</motion.div>
)
}

View file

@ -0,0 +1,32 @@
import { Button } from "@ui/components/button"
import { motion } from "motion/react"
interface ContinueStepProps {
setCurrentStep: (
step: "input" | "greeting" | "welcome" | "username" | "features",
) => void
}
export function ContinueStep({ setCurrentStep }: ContinueStepProps) {
return (
<motion.div
className="text-center"
initial={{ opacity: 0, y: 0, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 0, scale: 0.9 }}
transition={{ duration: 1, ease: "easeOut" }}
layout
>
<p className="text-white text-sm opacity-80 mb-6 max-w-xs">
I'm built with Supermemory's super fast memory API, so you never have to
worry about forgetting what matters across your AI apps.
</p>
<Button
className="rounded-xl px-6 py-3 bg-black border border-gray-800 hover:bg-gray-900"
onClick={() => setCurrentStep("features")}
>
Continue
</Button>
</motion.div>
)
}

View file

@ -0,0 +1,97 @@
import { motion } from "framer-motion"
import { Button } from "@ui/components/button"
import Link from "next/link"
export function FeaturesStep() {
return (
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.3 }}
className="text-center max-w-[22rem]"
layout
>
<h2 className="text-white text-3xl font-medium mb-2">
What I can do for you
</h2>
<div className="space-y-6 mb-8">
<div className="flex items-start space-x-2">
<div className="w-14 h-14 rounded-lg flex items-center justify-center flex-shrink-0">
<img
src="/onboarding/human-brain.png"
alt="Brain icon"
className="w-14 h-14"
/>
</div>
<div className="text-left">
<h3 className="text-white font-light text-lg mb-1">
Remember every context
</h3>
<p className="text-[#8A8A8A] text-xs">
I keep track of what you've saved and shared with your
supermemory.
</p>
</div>
</div>
<div className="flex items-start space-x-2">
<div className="w-14 h-14 rounded-lg flex items-center justify-center flex-shrink-0">
<img
src="/onboarding/search.png"
alt="Search icon"
className="w-14 h-14"
/>
</div>
<div className="text-left">
<h3 className="text-white font-light text-lg mb-1">
Find when you need it
</h3>
<p className="text-[#8A8A8A] text-xs">
I surface the right memories inside your supermemory, superfast.
</p>
</div>
</div>
<div className="flex items-start space-x-2">
<div className="w-14 h-14 rounded-lg flex items-center justify-center flex-shrink-0">
<img
src="/onboarding/plant.png"
alt="Growth icon"
className="w-14 h-14"
/>
</div>
<div className="text-left">
<h3 className="text-white font-light text-lg mb-1">
Grow with your supermemory
</h3>
<p className="text-[#8A8A8A] text-xs">
I learn and personalize over time, so every interaction feels
natural.
</p>
</div>
</div>
</div>
<motion.div
animate={{
opacity: 1,
y: 0,
}}
transition={{ duration: 1, ease: "easeOut", delay: 1 }}
initial={{ opacity: 0, y: 10 }}
>
<Button
className="rounded-xl px-6 py-3 bg-black border border-gray-800 hover:bg-gray-900 max-w-[10rem]"
asChild
>
<Link href="/onboarding/setup">
<motion.button whileTap={{ scale: 0.95 }} className="w-full">
Continue setup
</motion.button>
</Link>
</Button>
</motion.div>
</motion.div>
)
}

View file

@ -0,0 +1,23 @@
import { motion } from "framer-motion"
interface GreetingStepProps {
name: string
}
export function GreetingStep({ name }: GreetingStepProps) {
return (
<motion.div
key="greeting"
className="text-center"
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 0 }}
transition={{ duration: 1, ease: "easeOut" }}
layout
>
<h2 className="text-white text-2xl font-medium mb-2">
Hi {name}, I'm Nova
</h2>
</motion.div>
)
}

View file

@ -0,0 +1,86 @@
import { motion } from "motion/react"
import { LabeledInput } from "@ui/input/labeled-input"
import { Button } from "@ui/components/button"
interface InputStepProps {
name: string
setName: (name: string) => void
handleSubmit: () => void
isSubmitting: boolean
}
export function InputStep({
name,
setName,
handleSubmit,
isSubmitting,
}: InputStepProps) {
return (
<motion.div
key="input"
className="text-center min-w-[250px]"
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
}}
transition={{
duration: 0.8,
ease: "easeOut",
delay: 1,
}}
layout
>
<h2 className="text-white text-3xl font-medium mb-2">
What should I call you?
</h2>
<div className="flex items-center space-x-3 w-full relative">
<LabeledInput
inputType="text"
inputPlaceholder="your name"
className="w-full flex-1"
inputProps={{
value: name,
onKeyDown: (e) => {
if (e.key === "Enter") {
handleSubmit()
}
},
}}
onChange={(e) => setName((e.target as HTMLInputElement).value)}
style={{
background:
"linear-gradient(0deg, rgba(91, 126, 245, 0.04) 0%, rgba(91, 126, 245, 0.04) 100%)",
}}
/>
<Button
className={`rounded-xl p-2 !bg-black absolute right-4 border-[0.5px] border-gray-800 hover:cursor-pointer hover:scale-[0.95] active:scale-[0.95] transition-transform ${
isSubmitting ? "scale-[0.90]" : ""
}`}
size="icon"
onClick={handleSubmit}
>
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Next</title>
<path
d="M8.05099 9.60156L6.93234 8.49987L9.00014 6.44902L9.62726 6.04224L9.54251 5.788L8.79675 5.90665H0.0170898V4.31343H8.79675L9.54251 4.43207L9.62726 4.17783L9.00014 3.77105L6.93234 1.72021L8.05099 0.601562L11.9832 4.53377V5.68631L8.05099 9.60156Z"
fill="#FAFAFA"
/>
</svg>
</Button>
</div>
</motion.div>
)
}

View file

@ -0,0 +1,147 @@
"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "motion/react"
import { InitialHeader } from "@/components/initial-header"
import { useAuth } from "@lib/auth-context"
import { InputStep } from "./input-step"
import { GreetingStep } from "./greeting-step"
import { WelcomeStep } from "./welcome-step"
import { ContinueStep } from "./continue-step"
import { FeaturesStep } from "./features-step"
import { Logo } from "@ui/assets/Logo"
import NovaOrb from "@/components/nova/nova-orb"
import { cn } from "@lib/utils"
type OnboardStep = "input" | "greeting" | "welcome" | "username" | "features"
function UserSupermemory({ name }: { name: string }) {
return (
<motion.div
className="absolute inset-0 flex items-center justify-center"
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-xl leading-none">{name}'s</p>
<p className="text-white font-bold text-4xl leading-none -mt-2">
supermemory
</p>
</div>
</motion.div>
)
}
export default function OnboardPage() {
const { user } = useAuth()
const [name, setName] = useState(user?.name ?? "")
const [currentStep, setCurrentStep] = useState<OnboardStep>("input")
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = () => {
localStorage.setItem("username", name)
if (name.trim()) {
setIsSubmitting(true)
setCurrentStep("greeting")
setIsSubmitting(false)
}
}
useEffect(() => {
if (user?.name) {
setName(user.name)
localStorage.setItem("username", user.name)
}
}, [user?.name])
useEffect(() => {
const timers: NodeJS.Timeout[] = []
switch (currentStep) {
case "greeting":
timers.push(setTimeout(() => setCurrentStep("welcome"), 2000))
break
case "welcome":
timers.push(setTimeout(() => setCurrentStep("username"), 2000))
break
}
return () => {
timers.forEach(clearTimeout)
}
}, [currentStep])
return (
<div className="min-h-screen bg-black">
<InitialHeader
showUserSupermemory={currentStep === "features"}
name={name}
/>
<div className="flex flex-col items-center justify-start h-[calc(100vh-86px)] relative">
<motion.div
className="absolute inset-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.6,
}}
/>
<motion.div
className={cn("relative z-10 flex flex-col items-center justify-center", currentStep === "features" ? "mt-0" : "mt-16")}
animate={{
gap: currentStep === "features" ? 0 : 16,
}}
transition={{
duration: 0.6,
ease: "easeOut",
}}
layout
>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: currentStep === "features" ? 0.6 : 1,
padding: currentStep === "features" ? 0 : 32,
}}
transition={{
duration: 0.8,
ease: "easeOut",
delay: 0.2,
}}
layout
className="relative"
>
<NovaOrb size={300} />
{currentStep === "username" && <UserSupermemory name={name} />}
</motion.div>
<AnimatePresence mode="wait">
{currentStep === "input" && (
<InputStep
name={name}
setName={setName}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
)}
{currentStep === "greeting" && <GreetingStep name={name} />}
{currentStep === "welcome" && <WelcomeStep />}
{currentStep === "username" && (
<ContinueStep
setCurrentStep={(step) => setCurrentStep(step as OnboardStep)}
/>
)}
{currentStep === "features" && <FeaturesStep />}
</AnimatePresence>
</motion.div>
</div>
</div>
)
}

View file

@ -0,0 +1,17 @@
import { motion } from "motion/react"
export function WelcomeStep() {
return (
<motion.div
key="welcome"
className="text-center"
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 0 }}
transition={{ duration: 1, ease: "easeOut" }}
layout
>
<h2 className="text-white text-2xl font-medium mb-2">Welcome to</h2>
</motion.div>
)
}

View file

@ -0,0 +1,33 @@
import { Logo } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
export function InitialHeader({
showUserSupermemory,
name,
}: {
showUserSupermemory?: boolean
name?: string
}) {
return (
<div className="flex p-6 justify-between items-center">
<div className="flex items-center">
<Logo className="h-7" />
{showUserSupermemory && (
<div className="flex flex-col items-start justify-center ml-2">
<p className="text-[#8B8B8B] text-sm leading-tight">{name}'s</p>
<p className="text-white font-bold text-xl leading-none -mt-1">
supermemory
</p>
</div>
)}
</div>
<Button
variant="newDefault"
className="rounded-2xl text-base gap-1 !h-11"
size={"lg"}
>
Memory API <span className="text-xs mt-[4px]"></span>
</Button>
</div>
)
}

View file

@ -0,0 +1,306 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@ui/components/button"
import { CircleCheckIcon, CopyIcon } from "lucide-react"
import { toast } from "sonner"
import { analytics } from "@/lib/analytics"
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)
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()
toast.success("Copied to clipboard!")
}
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"
onClick={onBack}
>
Back
</Button>
</div>
<div className="flex-1 flex flex-col items-center justify-start">
<h1 className="text-white text-3xl font-medium mb-6 text-center">
Connect your AI to supermemory MCP
</h1>
<div className="mb-8 space-x-4 flex max-w-2xl">
<div className="flex items-start space-x-3">
<CircleCheckIcon className="size-10 text-green-500" />
<p className="text-foreground/40 text-sm">
MCP connects your AI apps to create and use memories directly
</p>
</div>
<div className="flex items-start space-x-3">
<CircleCheckIcon className="size-10 text-green-500" />
<p className="text-foreground/40 text-sm">
Auto-fetch the right context from anything you've saved
</p>
</div>
<div className="flex items-start space-x-3">
<CircleCheckIcon className="size-10 text-green-500" />
<p className="text-foreground/40 text-sm">
One-time setup, seamless integration across your workflow
</p>
</div>
</div>
<div className="w-full max-w-2xl space-y-6">
<div className="flex items-start space-x-4">
<div className="bg-blue-500 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium flex-shrink-0">
1
</div>
<div className="flex-1">
<h3 className="text-white text-lg font-medium mb-4">
Select your AI client
</h3>
<div className="flex flex-wrap gap-2 mb-4">
{Object.entries(clients)
.slice(0, 7)
.map(([key, clientName]) => (
<button
key={key}
type="button"
onClick={() =>
setSelectedClient(key as keyof typeof clients)
}
className={`px-3 py-2 rounded-full border-2 transition-colors duration-200 ${
selectedClient === key
? "border-blue-500 bg-blue-500/10"
: "border-[#0D121A] bg-[#080B0F] hover:border-gray-600"
}`}
>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-white">
{clientName}
</span>
</div>
</button>
))}
</div>
<p className="text-gray-400 text-xs">
*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="bg-gray-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium flex-shrink-0">
2
</div>
<div className="flex-1">
<h3 className="text-white text-lg font-medium mb-2">
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!")
}}
>
<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)
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="font-mono text-xs w-full pr-10 p-2 bg-black border border-gray-600 rounded text-green-400"
readOnly
value={generateInstallCommand()}
/>
<button
type="button"
className="absolute top-1 right-1 cursor-pointer p-1"
onClick={copyToClipboard}
>
<CopyIcon className="size-4 text-gray-400 hover:text-white" />
</button>
</div>
<p className="text-gray-400 text-sm">
Copy and run this command in your terminal to install
the MCP server.
</p>
</div>
)}
</div>
) : (
<p className="text-gray-400 text-sm">
The command will be provided once you select an AI client
above.
</p>
)}
</div>
</div>
<div className="flex items-start space-x-4">
<div className="bg-gray-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-medium flex-shrink-0">
3
</div>
<div className="flex-1">
<h3 className="text-white text-lg font-medium mb-2">
Run command in your terminal
</h3>
<p className="text-gray-400 text-sm">
Execute the copied command in your terminal to complete the
setup.
</p>
</div>
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,150 @@
"use client"
interface BgGradProps {
size?: number
className?: string
}
function BgGrad({ size = 400, className = "" }: BgGradProps) {
return (
<div
className={`relative ${className}`}
data-name="bg-grad"
style={{ width: `${size}px`, height: `${size}px` }}
>
{/* Large blue orb - right */}
<div
className="absolute bg-[#1410ff] filter rounded-full z-0"
style={{
height: `${(341.891 * size) / 400}px`,
left: `${(161.86 * size) / 400}px`,
top: `${(13.1 * size) / 400}px`,
width: `${(204.976 * size) / 400}px`,
filter: `blur(${(22.836 * size) / 400}px)`,
}}
data-name="4"
/>
{/* Large blue orb - left */}
<div
className="absolute bg-[#1410ff] filter rounded-full z-0"
style={{
height: `${(341.891 * size) / 400}px`,
left: `${(17.16 * size) / 400}px`,
top: `${(-0.01 * size) / 400}px`,
width: `${(204.976 * size) / 400}px`,
filter: `blur(${(22.836 * size) / 400}px)`,
}}
data-name="4"
/>
{/* Rotated blue orb - top left */}
<div
className="absolute flex items-center justify-center z-10"
style={{
height: `${(163.918 * size) / 400}px`,
left: `${(12.75 * size) / 400}px`,
top: `${(70.19 * size) / 400}px`,
width: `${(345.985 * size) / 400}px`,
}}
>
<div className="flex-none rotate-[11.06deg]">
<div
className="bg-[#0090ff] filter rounded-full"
style={{
height: `${(163.918 * size) / 400}px`,
width: `${(345.985 * size) / 400}px`,
filter: `blur(${(17.127 * size) / 400}px)`,
}}
data-name="3"
/>
</div>
</div>
{/* Rotated blue orb - top right */}
<div
className="absolute flex items-center justify-center z-20"
style={{
height: `${(103.099 * size) / 400}px`,
left: `${(195 * size) / 400}px`,
top: `${(84.54 * size) / 400}px`,
width: `${(159.018 * size) / 400}px`,
}}
>
<div className="flex-none rotate-[32.89deg]">
<div
className="bg-[#0099ff] filter rounded-full"
style={{
height: `${(103.099 * size) / 400}px`,
width: `${(159.018 * size) / 400}px`,
filter: `blur(${(14.273 * size) / 400}px)`,
}}
data-name="2"
/>
</div>
</div>
{/* Rotated blue orb - bottom left */}
<div
className="absolute flex items-center justify-center z-20"
style={{
height: `${(103.099 * size) / 400}px`,
left: "0px",
top: `${(81.05 * size) / 400}px`,
width: `${(159.018 * size) / 400}px`,
}}
>
<div className="flex-none rotate-[147.11deg]">
<div
className="bg-[#0099ff] filter rounded-full"
style={{
height: `${(103.099 * size) / 400}px`,
width: `${(159.018 * size) / 400}px`,
filter: `blur(${(14.273 * size) / 400}px)`,
}}
data-name="2"
/>
</div>
</div>
{/* Central rotated orb */}
<div
className="absolute flex items-center justify-center z-30"
style={{
height: `${(220.17 * size) / 400}px`,
left: `${(85.76 * size) / 400}px`,
top: `${(110.88 * size) / 400}px`,
width: `${(216.8 * size) / 400}px`,
}}
>
<div className="flex-none rotate-[180deg]">
<div
className="filter rounded-full"
style={{
height: `${(220.17 * size) / 400}px`,
width: `${(216.8 * size) / 400}px`,
filter: `blur(${(8.564 * size) / 400}px)`,
background: "linear-gradient(186deg, #FFF 4.91%, #124DFF 61.9%)",
}}
data-name="1"
/>
</div>
</div>
{/* Bottom blue orb */}
<div
className="absolute bg-[#47a8fd] filter rounded-full z-40"
style={{
height: `${(74.371 * size) / 400}px`,
left: `${(103.05 * size) / 400}px`,
top: `${(228.82 * size) / 400}px`,
width: `${(153.608 * size) / 400}px`,
filter: `blur(${(17.127 * size) / 400}px)`,
}}
data-name="0"
/>
</div>
)
}
export default BgGrad

View file

@ -0,0 +1,42 @@
"use client"
import BgGrad from "./bg-grad"
import { motion } from "motion/react"
interface NovaOrbProps {
size?: number
className?: string
}
function NovaOrb({ size = 200, className = "" }: NovaOrbProps) {
return (
<div
className={`flex items-center justify-center ${className} blur-[6px]`}
style={{ width: `${size}px`, height: `${size}px` }}
>
<div
className="rounded-full relative overflow-hidden border-[black] border-[1px]"
style={{
width: size,
height: size,
boxShadow: `${(1 * size) / 30}px ${(2 * size) / 30}px ${(4 * size) / 30}px 0 #0A0E14 inset, 0 ${(18.462 * size) / 30}px ${(5.192 * size) / 30}px 0 rgba(41, 95, 255, 0.00), 0 ${(12.115 * size) / 30}px ${(4.615 * size) / 30}px 0 rgba(41, 95, 255, 0.01), 0 ${(6.923 * size) / 30}px ${(4.038 * size) / 30}px 0 rgba(41, 95, 255, 0.05), 0 ${(2.885 * size) / 30}px ${(2.885 * size) / 30}px 0 rgba(41, 95, 255, 0.09), 0 ${(0.577 * size) / 30}px ${(1.731 * size) / 30}px 0 rgba(41, 95, 255, 0.10)`,
}}
>
<div className="rotate-[30.76deg] z-[-1] absolute top-[-30%] left-[-60%]">
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 12,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
}}
>
<BgGrad size={size * 1.8} />
</motion.div>
</div>
</div>
</div>
)
}
export default NovaOrb

View file

@ -54,6 +54,6 @@ export default async function proxy(request: Request) {
export const config = {
matcher: [
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|ingest|api|login|api/emails).*)",
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|bg-rectangle.png|onboarding|ingest|api|login|api/emails).*)",
],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -15,9 +15,14 @@ export function ExternalAuthButton({
return (
<Button
className={cn(
"flex flex-grow cursor-pointer max-w-full bg-background items-center justify-center gap-[0.625rem] rounded-xl border-[1.5px] border-border px-6 py-5 hover:bg-accent",
"flex flex-grow cursor-pointer max-w-full items-center justify-center gap-[0.625rem] rounded-xl px-6 py-5 hover:opacity-75",
className,
)}
style={{
borderRadius: "12px",
background: "linear-gradient(180deg, #00264F 0%, #001933 100%), linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
boxShadow: "0 1px 2px 0 rgba(0, 43, 87, 0.10), 1px 1px 1px 1px #002B57 inset",
}}
{...props}
>
<span className="aspect-square">{authIcon}</span>

View file

@ -9,7 +9,9 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
" text-primary-foreground shadow-xs hover:bg-primary/90",
newDefault:
"bg-gradient-to-b from-[#1C2026] to-[#12161C] text-white shadow-[inset_-2px_-2px_6px_0_rgba(0,0,0,0.15),inset_2px_2px_4px_0_rgba(255,255,255,0.05)] hover:from-[#1C2026]/90 hover:to-[#12161C]/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:

View file

@ -14,11 +14,9 @@ export function TextSeparator({
className={cn("flex gap-4 items-center justify-center", className)}
{...props}
>
<div className="w-full h-px bg-border" />
<span className="text-muted-foreground text-[0.75rem] uppercase tracking-[-0.2px] leading-[0.875rem]">
<span className="text-foreground font-sans text-[0.75rem] uppercase tracking-[-0.2px] leading-[0.875rem]">
{text}
</span>
<div className="w-full h-px bg-border" />
</div>
);
}

View file

@ -3,7 +3,7 @@ import { Input } from "@ui/components/input";
import { Label1Regular } from "@ui/text/label/label-1-regular";
interface LabeledInputProps extends React.ComponentProps<"div"> {
label: string;
label?: string;
inputType: string;
inputPlaceholder: string;
error?: string | null;
@ -11,23 +11,28 @@ interface LabeledInputProps extends React.ComponentProps<"div"> {
}
export function LabeledInput({
label,
inputType,
inputPlaceholder,
className,
error,
inputProps,
label,
...props
}: LabeledInputProps) {
return (
<div className={cn("flex flex-col gap-2", className)} {...props}>
<Label1Regular className="text-foreground">{label}</Label1Regular>
{label && <Label1Regular className="text-foreground">{label}</Label1Regular>}
<Input
className={cn(
"w-full leading-[1.375rem] tracking-[-0.4px] rounded-2xl p-5 placeholder:text-muted-foreground text-foreground border-[1.5px] border-border disabled:cursor-not-allowed disabled:opacity-50",
"w-full leading-[1.375rem] tracking-[-0.4px] rounded-xl p-5 placeholder:text-muted-foreground/50 text-foreground disabled:cursor-not-allowed disabled:opacity-50",
inputProps?.className,
)}
style={{
borderRadius: "12px",
border: "1px solid rgba(82, 89, 102, 0.20)",
background: "rgba(91, 126, 245, 0.04)",
boxShadow: "0 1px 2px 0 rgba(0, 43, 87, 0.10), 0 0 0 1px rgba(43, 49, 67, 0.08) inset, 0 1px 1px 0 rgba(0, 0, 0, 0.08) inset, 0 2px 4px 0 rgba(0, 0, 0, 0.02) inset",
}}
placeholder={inputPlaceholder}
type={inputType}
{...inputProps}

View file

@ -2,33 +2,74 @@
import { signIn } from "@lib/auth";
import { usePostHog } from "@lib/posthog";
import { LogoFull } from "@repo/ui/assets/Logo";
import { TextSeparator } from "@repo/ui/components/text-separator";
import { ExternalAuthButton } from "@ui/button/external-auth";
import { Button } from "@ui/components/button";
import { Badge } from "@ui/components/badge";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@ui/components/carousel";
import { LabeledInput } from "@ui/input/labeled-input";
import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium";
import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium";
import { Label1Regular } from "@ui/text/label/label-1-regular";
import { Title1Bold } from "@ui/text/title/title-1-bold";
import Autoplay from "embla-carousel-autoplay";
import Image from "next/image";
import { InitialHeader } from "../../../apps/web/components/initial-header";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
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.",
],
}) {
function AnimatedGradientBackground() {
return (
<div className="fixed inset-0 z-0 overflow-hidden">
<motion.div
className="absolute top-[40%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-0.png')] bg-[length: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-[40%] left-0 right-0 bottom-0 bg-[url('/onboarding/bg-gradient-1.png')] bg-[length: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"
initial={{ y: "100%" }}
animate={{ y: 0 }}
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 w-[448px] p-8 flex-col items-start gap-2 rounded-[22px] bg-gradient-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() {
const [email, setEmail] = useState("");
const [submittedEmail, setSubmittedEmail] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
@ -168,316 +209,263 @@ export function LoginPage({
};
return (
<section className="min-h-screen flex flex-col lg:grid lg:grid-cols-12 items-center justify-center p-4 sm:p-6 md:p-8 lg:px-[5rem] lg:py-[3.125rem] gap-6 lg:gap-[5rem] max-w-[400rem] mx-auto">
<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>
<main className="relative min-h-screen">
<AnimatedGradientBackground />
<div className="relative z-10">
<InitialHeader />
<section className="flex flex-col items-center justify-center p-4 space-y-8 sm:p-6 md:p-8 lg:px-[5rem] lg:py-[3.125rem] min-h-[calc(100vh-80px)]">
<div className="text-center">
<div className="text-5xl font-medium">
Never forget anything, anywhere
</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 className="text-5xl font-medium">with supermemory</div>
</div>
{submittedEmail ? (
<LoginCard>
<div className="w-full 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" />
<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>
) : (
<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>
<Button disabled={isLoading} id="verify-token" type="submit">
Verify Token
</Button>
</form>
</div>
</LoginCard>
) : (
<LoginCard>
<div className="w-full flex flex-col gap-4">
{params.get("error") && (
<div className="text-red-500">
Error: {params.get("error")}. Please try again!
</div>
)}
{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 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);
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 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);
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" />
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<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="w-full h-11"
style={{
borderRadius: "12px",
background:
"linear-gradient(180deg, #0FF0D2 0%, #5349dd 1%, #1E0FF0 100%)",
boxShadow:
"1px 1px 2px 1px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
}}
disabled={isLoading}
type="submit"
>
{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="text-muted-foreground text-center !text-xs">
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>
</LoginCard>
)}
<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>
</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>
</section>
</div>
</main>
);
}