feat: new design, new vibes
|
|
@ -42,7 +42,7 @@ export default function Page() {
|
|||
|
||||
useEffect(() => {
|
||||
if (user && !onboardingLoading && shouldShowOnboarding()) {
|
||||
router.push("/onboarding")
|
||||
router.push("/onboarding/welcome")
|
||||
}
|
||||
}, [user, shouldShowOnboarding, onboardingLoading, router])
|
||||
|
||||
|
|
|
|||
117
apps/web/app/onboarding/setup/chat-sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
43
apps/web/app/onboarding/setup/header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
133
apps/web/app/onboarding/setup/integrations-step.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
94
apps/web/app/onboarding/setup/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
110
apps/web/app/onboarding/setup/relatable-question.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
apps/web/app/onboarding/welcome/continue-step.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
97
apps/web/app/onboarding/welcome/features-step.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
23
apps/web/app/onboarding/welcome/greeting-step.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
86
apps/web/app/onboarding/welcome/input-step.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
147
apps/web/app/onboarding/welcome/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
17
apps/web/app/onboarding/welcome/welcome-step.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
33
apps/web/components/initial-header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
306
apps/web/components/mcp-detail-view.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
150
apps/web/components/nova/bg-grad.tsx
Normal 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
|
||||
42
apps/web/components/nova/nova-orb.tsx
Normal 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
|
||||
|
|
@ -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).*)",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
BIN
apps/web/public/bg-rectangle.png
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
apps/web/public/onboarding/bg-gradient-0.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
apps/web/public/onboarding/bg-gradient-1.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
apps/web/public/onboarding/chrome.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
apps/web/public/onboarding/connectors.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/web/public/onboarding/human-brain.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
apps/web/public/onboarding/plant.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
apps/web/public/onboarding/search.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
apps/web/public/onboarding/x.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||