remove unused old onboarding flow (#904)

This commit is contained in:
Ishaan Gupta 2026-05-06 22:35:22 +05:30 committed by GitHub
parent f850a0e8a7
commit 253d16b4b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 0 additions and 2423 deletions

View file

@ -1,109 +0,0 @@
"use client"
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react"
import { useAuth } from "@lib/auth-context"
export type MemoryFormData = {
twitter: string
linkedin: string
description: string
otherLinks: string[]
} | null
interface OnboardingContextValue {
name: string
setName: (name: string) => void
memoryFormData: MemoryFormData
setMemoryFormData: (data: MemoryFormData) => void
resetOnboarding: () => void
}
const OnboardingContext = createContext<OnboardingContextValue | null>(null)
export function useOnboardingContext() {
const ctx = useContext(OnboardingContext)
if (!ctx) {
throw new Error("useOnboardingContext must be used within OnboardingLayout")
}
return ctx
}
export default function OnboardingLayout({
children,
}: {
children: ReactNode
}) {
const { user } = useAuth()
const [name, setNameState] = useState<string>("")
const [memoryFormData, setMemoryFormDataState] =
useState<MemoryFormData>(null)
useEffect(() => {
const storedName = localStorage.getItem("onboarding_name")
const storedMemoryFormData = localStorage.getItem(
"onboarding_memoryFormData",
)
if (storedName) {
setNameState(storedName)
} else if (user?.displayUsername) {
setNameState(user.displayUsername)
localStorage.setItem("onboarding_name", user.displayUsername)
} else if (user?.name) {
setNameState(user.name)
localStorage.setItem("onboarding_name", user.name)
}
if (storedMemoryFormData) {
try {
setMemoryFormDataState(JSON.parse(storedMemoryFormData))
} catch {
// ignore parse errors
}
}
}, [user?.displayUsername, user?.name])
const setName = useCallback((newName: string) => {
setNameState(newName)
localStorage.setItem("onboarding_name", newName)
localStorage.setItem("username", newName)
}, [])
const setMemoryFormData = useCallback((data: MemoryFormData) => {
setMemoryFormDataState(data)
if (data) {
localStorage.setItem("onboarding_memoryFormData", JSON.stringify(data))
} else {
localStorage.removeItem("onboarding_memoryFormData")
}
}, [])
const resetOnboarding = useCallback(() => {
localStorage.removeItem("onboarding_name")
localStorage.removeItem("onboarding_memoryFormData")
setNameState("")
setMemoryFormDataState(null)
}, [])
const contextValue: OnboardingContextValue = {
name,
setName,
memoryFormData,
setMemoryFormData,
resetOnboarding,
}
return (
<OnboardingContext.Provider value={contextValue}>
{children}
</OnboardingContext.Provider>
)
}

View file

@ -1,18 +0,0 @@
"use client"
import { useEffect } from "react"
import { useRouter } from "next/navigation"
export default function OnboardingPage() {
const router = useRouter()
useEffect(() => {
router.replace("/old/onboarding/welcome?step=input")
}, [router])
return (
<div className="h-screen overflow-hidden bg-black flex items-center justify-center">
<div className="text-white/50 text-sm">Loading...</div>
</div>
)
}

View file

@ -1,87 +0,0 @@
"use client"
import {
createContext,
useContext,
useCallback,
useEffect,
useRef,
type ReactNode,
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { analytics } from "@/lib/analytics"
export const SETUP_STEPS = ["integrations"] as const
export type SetupStep = (typeof SETUP_STEPS)[number]
interface SetupContextValue {
memoryFormData: MemoryFormData
currentStep: SetupStep
goToStep: (step: SetupStep) => void
goToWelcome: (step?: string) => void
finishOnboarding: () => void
}
const SetupContext = createContext<SetupContextValue | null>(null)
export function useSetupContext() {
const ctx = useContext(SetupContext)
if (!ctx) {
throw new Error("useSetupContext must be used within SetupLayout")
}
return ctx
}
export default function SetupLayout({ children }: { children: ReactNode }) {
const router = useRouter()
const searchParams = useSearchParams()
const { memoryFormData, resetOnboarding } = useOnboardingContext()
const stepParam = searchParams.get("step")
const currentStep: SetupStep = SETUP_STEPS.includes(stepParam as SetupStep)
? (stepParam as SetupStep)
: "integrations"
const hasTrackedInitialStep = useRef(false)
const goToStep = useCallback(
(step: SetupStep) => {
analytics.onboardingStepViewed({ step, trigger: "user" })
router.push(`/onboarding/setup?step=${step}`)
},
[router],
)
const goToWelcome = useCallback(
(step = "input") => {
router.push(`/onboarding/welcome?step=${step}`)
},
[router],
)
const finishOnboarding = useCallback(() => {
resetOnboarding()
router.push("/")
}, [router, resetOnboarding])
useEffect(() => {
if (!hasTrackedInitialStep.current) {
analytics.onboardingStepViewed({ step: currentStep, trigger: "user" })
hasTrackedInitialStep.current = true
}
}, [currentStep])
const contextValue: SetupContextValue = {
memoryFormData,
currentStep,
goToStep,
goToWelcome,
finishOnboarding,
}
return (
<SetupContext.Provider value={contextValue}>
{children}
</SetupContext.Provider>
)
}

View file

@ -1,45 +0,0 @@
"use client"
import { AnimatePresence } from "motion/react"
import { IntegrationsStep } from "@/components/onboarding/setup/integrations-step"
import { SetupHeader } from "@/components/onboarding/setup/header"
import { ChatSidebar } from "@/components/onboarding/setup/chat-sidebar"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import { useIsMobile } from "@hooks/use-mobile"
import { useSetupContext } from "./layout"
export default function SetupPage() {
const { memoryFormData } = useSetupContext()
const isMobile = useIsMobile()
return (
<div className="relative h-screen overflow-hidden bg-black">
<SetupHeader />
<AnimatedGradientBackground animateFromBottom={false} />
<main className="relative min-h-screen">
<div className="relative z-10">
<div className="flex flex-col lg:flex-row h-[calc(100vh-90px)] relative">
<div className="flex-1 flex flex-col items-center justify-start p-4 md:p-8">
<AnimatePresence mode="wait">
<IntegrationsStep key="integrations" />
</AnimatePresence>
</div>
{!isMobile && (
<AnimatePresence mode="popLayout">
<ChatSidebar formData={memoryFormData} />
</AnimatePresence>
)}
</div>
</div>
</main>
{isMobile && <ChatSidebar formData={memoryFormData} />}
</div>
)
}

View file

@ -1,166 +0,0 @@
"use client"
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
useRef,
type ReactNode,
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { useAuth } from "@lib/auth-context"
import { analytics } from "@/lib/analytics"
export const WELCOME_STEPS = [
"input",
"greeting",
"welcome",
"username",
"features",
"memories",
] as const
export type WelcomeStep = (typeof WELCOME_STEPS)[number]
interface WelcomeContextValue {
name: string
setName: (name: string) => void
isSubmitting: boolean
setIsSubmitting: (value: boolean) => void
showWelcomeContent: boolean
memoryFormData: MemoryFormData
setMemoryFormData: (data: MemoryFormData) => void
currentStep: WelcomeStep
goToStep: (step: WelcomeStep) => void
goToSetup: (step?: string) => void
}
const WelcomeContext = createContext<WelcomeContextValue | null>(null)
export function useWelcomeContext() {
const ctx = useContext(WelcomeContext)
if (!ctx) {
throw new Error("useWelcomeContext must be used within WelcomeLayout")
}
return ctx
}
export default function WelcomeLayout({ children }: { children: ReactNode }) {
const router = useRouter()
const searchParams = useSearchParams()
const { name, setName, memoryFormData, setMemoryFormData } =
useOnboardingContext()
const { organizations } = useAuth()
const hasOrgs = Array.isArray(organizations) && organizations.length > 0
const stepParam = searchParams.get("step")
const resolvedStep: WelcomeStep = WELCOME_STEPS.includes(
stepParam as WelcomeStep,
)
? (stepParam as WelcomeStep)
: "input"
const currentStep: WelcomeStep =
resolvedStep === "input" && hasOrgs ? "greeting" : resolvedStep
const [isSubmitting, setIsSubmitting] = useState(false)
const [showWelcomeContent, setShowWelcomeContent] = useState(false)
const isMountedRef = useRef(true)
const hasTrackedInitialStep = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
useEffect(() => {
if (currentStep === "input") {
setShowWelcomeContent(false)
const timer = setTimeout(() => {
if (isMountedRef.current) {
setShowWelcomeContent(true)
}
}, 400)
return () => clearTimeout(timer)
}
setShowWelcomeContent(true)
}, [currentStep])
useEffect(() => {
const timers: NodeJS.Timeout[] = []
if (currentStep === "greeting") {
timers.push(
setTimeout(() => {
if (isMountedRef.current) {
analytics.onboardingStepViewed({ step: "welcome", trigger: "auto" })
router.replace("/old/onboarding/welcome?step=welcome")
}
}, 2000),
)
} else if (currentStep === "welcome") {
timers.push(
setTimeout(() => {
if (isMountedRef.current) {
analytics.onboardingStepViewed({
step: "username",
trigger: "auto",
})
router.replace("/old/onboarding/welcome?step=username")
}
}, 2000),
)
}
return () => {
timers.forEach(clearTimeout)
}
}, [currentStep, router])
useEffect(() => {
if (!hasTrackedInitialStep.current) {
analytics.onboardingStepViewed({
step: currentStep,
trigger: "user",
})
hasTrackedInitialStep.current = true
}
}, [currentStep])
const goToStep = useCallback(
(step: WelcomeStep) => {
analytics.onboardingStepViewed({ step, trigger: "user" })
router.push(`/old/onboarding/welcome?step=${step}`)
},
[router],
)
const goToSetup = useCallback(
(step = "integrations") => {
router.push(`/old/onboarding/setup?step=${step}`)
},
[router],
)
const contextValue: WelcomeContextValue = {
name,
setName,
isSubmitting,
setIsSubmitting,
showWelcomeContent,
memoryFormData,
setMemoryFormData,
currentStep,
goToStep,
goToSetup,
}
return (
<WelcomeContext.Provider value={contextValue}>
{children}
</WelcomeContext.Provider>
)
}

View file

@ -1,265 +0,0 @@
"use client"
import { useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
import { cn } from "@lib/utils"
import { InputStep } from "@/components/onboarding/welcome/input-step"
import { GreetingStep } from "@/components/onboarding/welcome/greeting-step"
import { WelcomeStep } from "@/components/onboarding/welcome/welcome-step"
import { OnboardingContentStep } from "@/components/onboarding/welcome/continue-step"
import { InitialHeader } from "@/components/initial-header"
import { Logo } from "@ui/assets/Logo"
import NovaOrb from "@/components/nova/nova-orb"
import {
useWelcomeContext,
type WelcomeStep as WelcomeStepType,
} from "./layout"
import { gapVariants, orbVariants } from "@/lib/variants"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { analytics } from "@/lib/analytics"
import { toast } from "sonner"
function generateSlugFromName(value: string) {
return (
value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "") || "org"
)
}
function generateOrgSlug(name: string) {
const base = generateSlugFromName(name.trim())
const randomNum = Math.floor(100000 + Math.random() * 900000)
return `${base}-${randomNum}`
}
function generateUsername(name: string) {
const base = generateSlugFromName(name.trim()).replace(/-/g, "_")
const randomNum = Math.floor(100000 + Math.random() * 900000)
return `${base}${randomNum}`
}
function UserSupermemory({ name }: { name: string }) {
return (
<motion.div
className="absolute inset-0 top-[-34px] flex items-center justify-center z-10"
initial={{ opacity: 0, y: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 0 }}
transition={{ duration: 1, ease: "easeOut" }}
>
<Logo className="h-14 text-white" />
<div className="flex flex-col items-start justify-center ml-4">
<p className="text-white text-[25px] font-medium leading-none">
{name.split(" ")[0]}'s
</p>
<p className="text-white font-bold text-4xl leading-none -mt-2">
supermemory
</p>
</div>
</motion.div>
)
}
function StepNotFound({
goToStep,
}: {
goToStep: (step: WelcomeStepType) => void
}) {
return (
<motion.div
className="text-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<h2 className="text-white text-2xl mb-4">Unknown step</h2>
<button
type="button"
onClick={() => goToStep("input")}
className="text-blue-400 underline"
>
Start from beginning
</button>
</motion.div>
)
}
export default function WelcomePage() {
const {
name,
setName,
isSubmitting,
setIsSubmitting,
showWelcomeContent,
setMemoryFormData,
currentStep,
goToStep,
} = useWelcomeContext()
const { refetchOrganizations, setActiveOrg } = useAuth()
const submitLockRef = useRef(false)
const handleSubmit = async () => {
const trimmed = name.trim()
if (!trimmed) return
if (submitLockRef.current) return
submitLockRef.current = true
localStorage.setItem("username", trimmed)
setIsSubmitting(true)
try {
await authClient.updateUser({
displayUsername: trimmed,
username: generateUsername(trimmed),
})
const refetchResult = await refetchOrganizations()
const refetchData = (
refetchResult as { data?: unknown[] | null | undefined }
)?.data
const existingOrgs = Array.isArray(refetchData) ? refetchData : []
if (existingOrgs.length > 0) {
analytics.onboardingNameSubmitted({
name_length: trimmed.length,
})
goToStep("greeting")
return
}
const uniqueSlug = generateOrgSlug(trimmed)
const completedAt = new Date().toISOString()
const newOrg = await authClient.organization.create({
name: trimmed,
slug: uniqueSlug,
metadata: {
signupSource: "consumer",
webOnboarding: {
completedAt: null,
steps: {
welcomeInput: {
startedAt: completedAt,
completedAt,
data: {},
},
},
},
},
})
await setActiveOrg(newOrg.slug)
analytics.onboardingNameSubmitted({ name_length: trimmed.length })
goToStep("greeting")
} catch (error) {
console.error("Onboarding submit failed:", error)
toast.error(
error instanceof Error
? error.message
: "Could not set up your workspace. Please try again.",
)
} finally {
submitLockRef.current = false
setIsSubmitting(false)
}
}
const renderStep = () => {
switch (currentStep) {
case "input":
return (
<InputStep
key="input"
name={name}
setName={setName}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
)
case "greeting":
return <GreetingStep key="greeting" name={name} />
case "welcome":
return <WelcomeStep key="welcome" />
case "username":
case "features":
case "memories":
return (
<OnboardingContentStep
key="onboarding-content"
currentView={
currentStep === "username"
? "continue"
: currentStep === "features"
? "features"
: "memories"
}
onSubmit={setMemoryFormData}
/>
)
default:
return <StepNotFound key="not-found" goToStep={goToStep} />
}
}
const minimizeNovaOrb = ["features", "memories"].includes(currentStep)
const novaSize = currentStep === "memories" ? 150 : 300
const showUserSupermemory = currentStep === "username"
return (
<div className="relative h-screen overflow-hidden bg-black">
<InitialHeader
showUserSupermemory={
currentStep === "features" || currentStep === "memories"
}
showSkipOnboarding={currentStep !== "input"}
name={name}
/>
{showWelcomeContent && (
<div className="fixed inset-0 flex flex-col items-center justify-center overflow-y-auto">
<motion.div
className="absolute inset-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat pointer-events-none"
transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
style={{
mixBlendMode: "soft-light",
opacity: 0.6,
}}
/>
<motion.div
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex flex-col items-center justify-center",
)}
variants={gapVariants}
animate={minimizeNovaOrb ? "minimized" : "default"}
>
<motion.div
variants={orbVariants}
animate={
currentStep === "features"
? "features"
: currentStep === "memories"
? "memories"
: "default"
}
initial={{
padding: 0,
paddingTop: 0,
}}
className="relative"
>
<NovaOrb size={novaSize} />
{showUserSupermemory && <UserSupermemory name={name} />}
</motion.div>
<AnimatePresence mode="wait">{renderStep()}</AnimatePresence>
</motion.div>
</div>
)}
</div>
)
}

View file

@ -1,870 +0,0 @@
"use client"
import { useState, useEffect, useCallback, useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
import { useAgent } from "agents/react"
import { useAgentChat } from "@cloudflare/ai-chat/react"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
import {
PanelRightCloseIcon,
SendIcon,
CheckIcon,
XIcon,
Loader2,
} from "lucide-react"
import { collectValidUrls } from "@/lib/url-helpers"
import { $fetch } from "@lib/api"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useAuth } from "@lib/auth-context"
import { useProject } from "@/stores"
import { Streamdown } from "streamdown"
import { useIsMobile } from "@hooks/use-mobile"
interface ChatSidebarProps {
formData: {
twitter: string
linkedin: string
description: string
otherLinks: string[]
} | null
}
interface DraftDoc {
kind: "likes" | "link" | "x_research"
content: string
metadata: Record<string, string>
title?: string
url?: string
}
export function ChatSidebar({ formData }: ChatSidebarProps) {
const { user } = useAuth()
const { selectedProject } = useProject()
const isMobile = useIsMobile()
const [message, setMessage] = useState("")
const [isChatOpen, setIsChatOpen] = useState(!isMobile)
const [timelineMessages, setTimelineMessages] = useState<
{
message: string
type?: "formData" | "exa" | "memory" | "waiting"
memories?: {
url: string
title: string
description: string
fullContent: string
}[]
url?: string
title?: string
description?: string
}[]
>([])
const [isLoading, setIsLoading] = useState(false)
const [isFetchingDrafts, setIsFetchingDrafts] = useState(false)
const [draftDocs, setDraftDocs] = useState<DraftDoc[]>([])
const [xResearchStatus, setXResearchStatus] = useState<
"correct" | "incorrect" | null
>(null)
const [isConfirmed, setIsConfirmed] = useState(false)
const [processingByUrl, setProcessingByUrl] = useState<
Record<string, boolean>
>({})
const displayedMemoriesRef = useRef<Set<string>>(new Set())
const contextInjectedRef = useRef(false)
const draftsBuiltRef = useRef(false)
const isProcessingRef = useRef(false)
const draftRequestIdRef = useRef(0)
const backendUrl = new URL(process.env.NEXT_PUBLIC_BACKEND_URL!)
const agent = useAgent({
agent: "chat-agent",
name: user?.id ?? "anonymous",
host: backendUrl.host,
})
useEffect(() => {
agent.setState({
model: "claude-sonnet-4.6" as const,
projectId: selectedProject,
})
}, [agent, selectedProject])
const {
messages: chatMessages,
sendMessage,
status,
} = useAgentChat({
agent,
getInitialMessages: null,
credentials: "include",
})
const buildOnboardingContext = useCallback(() => {
if (!formData) return ""
const contextParts: string[] = []
if (formData.description?.trim()) {
contextParts.push(`User's interests/likes: ${formData.description}`)
}
if (formData.twitter) {
contextParts.push(`X/Twitter profile: ${formData.twitter}`)
}
if (formData.linkedin) {
contextParts.push(`LinkedIn profile: ${formData.linkedin}`)
}
if (formData.otherLinks.length > 0) {
contextParts.push(`Other links: ${formData.otherLinks.join(", ")}`)
}
const memoryTexts = timelineMessages
.filter((msg) => msg.type === "memory" && msg.memories)
.flatMap(
(msg) => msg.memories?.map((m) => `${m.title}: ${m.description}`) || [],
)
if (memoryTexts.length > 0) {
contextParts.push(`Extracted memories:\n${memoryTexts.join("\n")}`)
}
return contextParts.join("\n\n")
}, [formData, timelineMessages])
const handleSend = () => {
if (!message.trim() || status === "submitted" || status === "streaming")
return
let messageToSend = message
const context = buildOnboardingContext()
if (context && !contextInjectedRef.current && chatMessages.length === 0) {
messageToSend = `${context}\n\nUser question: ${message}`
contextInjectedRef.current = true
}
sendMessage({ text: messageToSend })
setMessage("")
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
const toggleChat = () => {
setIsChatOpen(!isChatOpen)
}
const pollForMemories = useCallback(
async (documentIds: string[]) => {
const maxAttempts = 30 // 30 attempts * 3 seconds = 90 seconds max
const pollInterval = 3000 // 3 seconds
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await $fetch("@get/documents/:id", {
params: { id: documentIds[0] ?? "" },
disableValidation: true,
})
console.log("response", response)
if (response.data) {
const document = response.data
if (document.memories && document.memories.length > 0) {
const newMemories: {
url: string
title: string
description: string
fullContent: string
}[] = []
document.memories.forEach(
(memory: { memory: string; title?: string }) => {
if (!displayedMemoriesRef.current.has(memory.memory)) {
displayedMemoriesRef.current.add(memory.memory)
newMemories.push({
url: document.url || "",
title: memory.title || document.title || "Memory",
description: memory.memory || "",
fullContent: memory.memory || "",
})
}
},
)
if (newMemories.length > 0 && timelineMessages.length < 10) {
setTimelineMessages((prev) => [
...prev,
{
message: newMemories
.map((memory) => memory.description)
.join("\n"),
type: "memory" as const,
memories: newMemories,
},
])
}
}
if (document.memories && document.memories.length > 0) {
break
}
}
await new Promise((resolve) => setTimeout(resolve, pollInterval))
} catch (error) {
console.warn("Error polling for memories:", error)
await new Promise((resolve) => setTimeout(resolve, pollInterval))
}
}
},
[timelineMessages.length],
)
const buildDraftDocs = useCallback(async () => {
if (!formData || draftsBuiltRef.current) return
draftsBuiltRef.current = true
const hasContent =
formData.twitter ||
formData.linkedin ||
formData.otherLinks.length > 0 ||
formData.description?.trim()
if (!hasContent) return
const requestId = ++draftRequestIdRef.current
setIsFetchingDrafts(true)
const drafts: DraftDoc[] = []
const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
const allProcessingUrls: string[] = [...urls]
if (formData.twitter) {
allProcessingUrls.push(formData.twitter)
}
if (allProcessingUrls.length > 0) {
setProcessingByUrl((prev) => {
const next = { ...prev }
for (const url of allProcessingUrls) {
next[url] = true
}
return next
})
}
try {
if (formData.description?.trim()) {
drafts.push({
kind: "likes",
content: formData.description,
metadata: {
sm_source: "consumer",
description_source: "user_input",
},
title: "Your Interests",
})
}
// Fetch each URL separately for per-link loading state
const linkPromises = urls.map(async (url) => {
try {
const response = await fetch("/api/onboarding/extract-content", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ urls: [url] }),
})
const data = await response.json()
return data.results?.[0] || null
} catch {
return null
} finally {
// Clear this URL's processing state
if (draftRequestIdRef.current === requestId) {
setProcessingByUrl((prev) => ({ ...prev, [url]: false }))
}
}
})
// Fetch X/Twitter research
const xResearchPromise = formData.twitter
? (async () => {
try {
const response = await fetch("/api/onboarding/research", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
xUrl: formData.twitter,
name: user?.name,
email: user?.email,
}),
})
if (!response.ok) return null
const data = await response.json()
return data?.text?.trim() || null
} catch {
return null
} finally {
// Clear twitter URL's processing state
if (draftRequestIdRef.current === requestId) {
setProcessingByUrl((prev) => ({
...prev,
[formData.twitter]: false,
}))
}
}
})()
: Promise.resolve(null)
const [exaResults, xResearchResult] = await Promise.all([
Promise.all(linkPromises),
xResearchPromise,
])
// Guard against stale request completing after a newer one
if (draftRequestIdRef.current !== requestId) return
for (const result of exaResults) {
if (result && (result.text || result.description)) {
drafts.push({
kind: "link",
content: result.text || result.description || "",
metadata: {
sm_source: "consumer",
exa_url: result.url,
exa_title: result.title,
},
title: result.title || "Extracted Content",
url: result.url,
})
}
}
if (xResearchResult) {
drafts.push({
kind: "x_research",
content: xResearchResult,
metadata: {
sm_source: "consumer",
onboarding_source: "x_research",
x_url: formData.twitter,
},
title: "X/Twitter Profile Research",
url: formData.twitter,
})
}
setDraftDocs(drafts)
} catch (error) {
console.warn("Error building draft docs:", error)
} finally {
if (draftRequestIdRef.current === requestId) {
setIsFetchingDrafts(false)
}
}
}, [formData, user])
const handleConfirmDocs = useCallback(async () => {
if (isConfirmed || isProcessingRef.current) return
isProcessingRef.current = true
setIsConfirmed(true)
setIsLoading(true)
try {
const promises = draftDocs.map(async (draft) => {
if (draft.kind === "x_research" && xResearchStatus !== "correct") {
return null
}
try {
const docResponse = await $fetch("@post/documents", {
body: {
content: draft.content,
containerTags: ["sm_project_default"],
metadata: draft.metadata,
},
})
return docResponse.data?.id
} catch (error) {
console.warn("Error creating document:", error)
return null
}
})
const results = await Promise.all(promises)
const documentIds = results.filter(
(id): id is string => id !== null && id !== undefined,
)
if (documentIds.length > 0) {
await pollForMemories(documentIds)
}
} catch (error) {
console.warn("Error confirming documents:", error)
setIsConfirmed(false)
} finally {
setIsLoading(false)
isProcessingRef.current = false
}
}, [draftDocs, xResearchStatus, isConfirmed, pollForMemories])
useEffect(() => {
if (!formData) return
const formDataMessages: typeof timelineMessages = []
if (formData.twitter) {
formDataMessages.push({
message: formData.twitter,
url: formData.twitter,
title: "X/Twitter",
description: formData.twitter,
type: "formData" as const,
})
}
if (formData.linkedin) {
formDataMessages.push({
message: formData.linkedin,
url: formData.linkedin,
title: "LinkedIn",
description: formData.linkedin,
type: "formData" as const,
})
}
if (formData.otherLinks.length > 0) {
formData.otherLinks.forEach((link) => {
formDataMessages.push({
message: link,
url: link,
title: "Link",
description: link,
type: "formData" as const,
})
})
}
if (formData.description?.trim()) {
formDataMessages.push({
message: formData.description,
title: "Likes",
description: formData.description,
type: "formData" as const,
})
}
setTimelineMessages(formDataMessages)
buildDraftDocs()
}, [formData, buildDraftDocs])
return (
<AnimatePresence mode="wait">
{!isChatOpen ? (
<motion.div
key="closed"
className={cn(
"flex items-start justify-start",
isMobile
? "fixed bottom-4 right-4 z-50"
: "absolute top-0 right-0 m-4",
dmSansClassName(),
)}
layoutId="chat-toggle-button"
>
<motion.button
onClick={toggleChat}
className={cn(
"flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer shadow-lg",
isMobile && "px-4 py-2",
)}
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
<NovaOrb size={24} className="blur-none! z-10" />
{!isMobile && "Chat with Nova"}
</motion.button>
</motion.div>
) : (
<motion.div
key="open"
className={cn(
"bg-[#0A0E14] backdrop-blur-md flex flex-col",
isMobile
? "fixed inset-0 z-50 w-full h-dvh rounded-none m-0"
: "w-[450px] h-[calc(100vh-110px)] rounded-2xl m-4",
dmSansClassName(),
)}
initial={
isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
}
animate={{ x: 0, y: 0, opacity: 1 }}
exit={
isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
}
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
>
<motion.button
onClick={toggleChat}
className={cn(
"absolute top-4 right-4 flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer",
isMobile && "bg-[#0D121A] border border-[#73737333]",
)}
style={
isMobile
? {
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
}
: {
background:
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}
}
layoutId="chat-toggle-button"
>
{isMobile ? (
<XIcon className="size-4" />
) : (
<>
<PanelRightCloseIcon className="size-4" />
Close chat
</>
)}
</motion.button>
<div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end overflow-y-auto scrollbar-thin">
{timelineMessages.map((msg, i) => (
<div
key={`message-${i}-${msg.message}`}
className="flex items-start gap-2"
>
{msg.type === "waiting" ? (
<div className="flex items-center gap-2 text-white/50">
<NovaOrb size={30} className="blur-none!" />
<span className="text-sm">{msg.message}</span>
</div>
) : (
<>
<div
className={cn(
"flex flex-col items-center justify-center w-[30px] h-full",
i !== 0 && "",
)}
>
{i === 0 && (
<div className="w-3 h-3 bg-[#293952]/40 rounded-full mb-1" />
)}
<div className="w-px flex-1 bg-[#293952]/40" />
</div>
{msg.type === "formData" && (
<div className="bg-[#293952]/40 rounded-lg p-2 px-3 space-y-1 flex-1">
{msg.title && (
<div className="flex items-center gap-2">
<h3
className="text-sm font-medium"
style={{
background:
"linear-gradient(90deg, #369BFD 0%, #36FDFD 30%, #36FDB5 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
{msg.title}
</h3>
{msg.url && processingByUrl[msg.url] && (
<Loader2 className="h-3 w-3 animate-spin text-blue-400" />
)}
</div>
)}
{msg.url && (
<a
href={msg.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:underline break-all block"
>
{msg.url}
</a>
)}
{msg.title === "Likes" && msg.description && (
<p className="text-xs text-white/70 mt-1">
{msg.description}
</p>
)}
</div>
)}
{msg.type === "memory" && (
<div className="space-y-2 w-full max-h-60 overflow-y-auto scrollbar-thin">
{msg.memories?.map((memory) => (
<div
key={memory.url + memory.title}
className="bg-[#293952]/40 rounded-lg p-2 px-3 space-y-2"
>
{memory.title && (
<h3
className="text-sm font-medium"
style={{
background:
"linear-gradient(90deg, #369BFD 0%, #36FDFD 30%, #36FDB5 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
{memory.title}
</h3>
)}
{memory.url && (
<a
href={memory.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:underline break-all"
>
{memory.url}
</a>
)}
{memory.description && (
<p className="text-xs text-white/50 mt-1">
{memory.description}
</p>
)}
</div>
))}
</div>
)}
</>
)}
</div>
))}
{chatMessages.map((msg) => {
if (msg.role === "user") {
const text = msg.parts
.filter((part) => part.type === "text")
.map((part) => part.text)
.join(" ")
return (
<div
key={msg.id}
className="flex items-start gap-2 justify-end"
>
<div className="bg-[#1B1F24] rounded-[12px] p-3 px-[14px] max-w-[80%]">
<p className="text-sm text-white">{text}</p>
</div>
</div>
)
}
if (msg.role === "assistant") {
return (
<div key={msg.id} className="flex items-start gap-2">
<NovaOrb size={30} className="blur-none!" />
<div className="flex-1">
{msg.parts.map((part, partIndex) => {
if (part.type === "text") {
return (
<div
key={`${msg.id}-${partIndex}`}
className="text-sm text-white/90 chat-markdown-content"
>
<Streamdown>{part.text}</Streamdown>
</div>
)
}
if (part.type === "tool-searchMemories") {
if (
part.state === "input-available" ||
part.state === "input-streaming"
) {
return (
<div
key={`${msg.id}-${partIndex}`}
className="text-xs text-white/50 italic"
>
Searching memories...
</div>
)
}
}
return null
})}
</div>
</div>
)
}
return null
})}
{(status === "submitted" || status === "streaming") &&
chatMessages[chatMessages.length - 1]?.role === "user" && (
<div className="flex items-start gap-2">
<NovaOrb size={30} className="blur-none!" />
<span className="text-sm text-white/50">Thinking...</span>
</div>
)}
{timelineMessages.length === 0 &&
chatMessages.length === 0 &&
!isLoading &&
!formData && (
<div className="flex items-center gap-2 text-white/50">
<NovaOrb size={28} className="blur-none!" />
<span className="text-sm">Waiting for your input</span>
</div>
)}
{isLoading && (
<div className="flex items-center gap-2 text-foreground/50">
<NovaOrb size={28} className="blur-none!" />
<span className="text-sm">Extracting memories...</span>
</div>
)}
</div>
{draftDocs.some((d) => d.kind === "x_research") && !isConfirmed && (
<div className="px-4 pb-2 space-y-3">
<div className="bg-[#293952]/40 rounded-lg p-3 space-y-2">
<h3
className="text-sm font-medium"
style={{
background:
"linear-gradient(90deg, #369BFD 0%, #36FDFD 30%, #36FDB5 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
Your Profile Summary
</h3>
<div className="overflow-y-auto scrollbar-thin max-h-32">
<p className="text-xs text-white/70">
{draftDocs.find((d) => d.kind === "x_research")?.content}
</p>
</div>
<div className="flex items-center gap-2 pt-2">
<span className="text-xs text-white/50">
Is this accurate?
</span>
<button
type="button"
onClick={() => {
setXResearchStatus("correct")
handleConfirmDocs()
}}
disabled={isConfirmed || isLoading}
className={cn(
"flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors cursor-pointer",
xResearchStatus === "correct"
? "bg-green-500/20 text-green-400 border border-green-500/40"
: "bg-[#1B1F24] text-white/50 hover:text-white/70",
(isConfirmed || isLoading) &&
"opacity-50 cursor-not-allowed",
)}
>
<CheckIcon className="size-3" />
Correct
</button>
<button
type="button"
onClick={() => setXResearchStatus("incorrect")}
className={cn(
"flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-colors cursor-pointer",
xResearchStatus === "incorrect"
? "bg-red-500/20 text-red-400 border border-red-500/40"
: "bg-[#1B1F24] text-white/50 hover:text-white/70",
)}
>
<XIcon className="size-3" />
Incorrect
</button>
</div>
{xResearchStatus === "incorrect" && (
<>
<p className="text-xs text-white/40 pt-1">
If incorrect, share your info in the input below, or you
can add memories later as well.
</p>
<Button
type="button"
onClick={handleConfirmDocs}
disabled={isConfirmed || isLoading}
className="w-full bg-[#267BF1] hover:bg-[#1E6AD9] text-white rounded-lg py-2 text-sm cursor-pointer mt-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
Continue
</Button>
</>
)}
</div>
</div>
)}
{!draftDocs.some((d) => d.kind === "x_research") &&
draftDocs.length > 0 &&
!isConfirmed && (
<div className="px-4 pb-2">
<Button
type="button"
onClick={handleConfirmDocs}
disabled={isConfirmed || isLoading}
className="w-full bg-[#267BF1] hover:bg-[#1E6AD9] text-white rounded-lg py-2 text-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
Continue
</Button>
</div>
)}
<div className="p-4 space-y-2">
{isFetchingDrafts && (
<div className="flex items-center gap-2 text-white/50 px-2">
<NovaOrb size={20} className="blur-none!" />
<span className="text-sm">
Getting all relevant info about you...
</span>
</div>
)}
<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-white placeholder:text-white/20 rounded-sm outline-none resize-none text-base leading-relaxed bg-transparent px-2 h-10"
disabled={status === "submitted" || status === "streaming"}
/>
<div className="flex justify-end absolute bottom-3 right-2">
<Button
type="submit"
disabled={
!message.trim() ||
status === "submitted" ||
status === "streaming"
}
className="text-white/20 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all"
size="icon"
>
<SendIcon className="size-4" />
</Button>
</div>
</form>
</div>
</motion.div>
)}
</AnimatePresence>
)
}

View file

@ -1,88 +0,0 @@
"use client"
import { motion } from "motion/react"
import { Logo } from "@ui/assets/Logo"
import { useAuth } from "@lib/auth-context"
import { UserProfileMenu } from "@/components/user-profile-menu"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"
export function SetupHeader() {
const { user } = useAuth()
const router = useRouter()
const localStorageUsername = useLocalStorageUsername()
const { markOrgOnboarded, isLoading: isOrgLoading } = useOrgOnboarding()
const handleSkip = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
router.push("/")
}
const displayName =
user?.displayUsername || localStorageUsername || user?.name || ""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
return (
<motion.div
className="relative z-20 flex p-6 justify-between items-center"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<nav
className={cn(
"flex items-center gap-2 sm:gap-3 min-w-0 z-10! text-sm",
dmSansClassName(),
)}
aria-label="Breadcrumb"
>
<button
type="button"
onClick={() => router.push("/")}
className={cn(
"flex items-center min-w-0 rounded-lg py-1 pr-2 -ml-1 pl-1",
"hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors cursor-pointer text-left",
)}
>
<Logo className="h-7 shrink-0" />
{displayName ? (
<div className="flex flex-col items-start justify-center ml-2 min-w-0">
<p className="text-[#8B8B8B] text-[11px] leading-tight">
{userName}
</p>
<p className="text-white font-bold text-xl leading-none -mt-1">
supermemory
</p>
</div>
) : (
<span className="ml-2 font-medium text-white/90">supermemory</span>
)}
</button>
<span className="text-white/35 shrink-0" aria-hidden>
/
</span>
<span className="text-white/50 font-medium shrink-0">Setup</span>
</nav>
<div className="flex items-center gap-3 z-10">
{!isOrgLoading && (
<button
type="button"
onClick={handleSkip}
className={cn(
"text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer",
dmSansClassName(),
)}
>
Skip Onboarding
</button>
)}
{user && <UserProfileMenu avatarClassName="border-border" />}
</div>
</motion.div>
)
}

View file

@ -1,185 +0,0 @@
"use client"
import { CHROME_EXTENSION_URL } from "@repo/lib/constants"
import { useState } from "react"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"
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="/onboarding/mcp.png" alt="MCP" className="size-28 h-auto" />
</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>
),
},
]
export function IntegrationsStep() {
const router = useRouter()
const [selectedCard, setSelectedCard] = useState<string | null>(null)
const { markOrgOnboarded } = useOrgOnboarding()
const handleContinue = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
router.push("/")
}
if (selectedCard === "Connect to AI") {
return <MCPDetailView onBack={() => setSelectedCard(null)} />
}
if (selectedCard === "Import") {
return <XBookmarksDetailView 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-[32px] font-medium">
Build your personal memory
</h1>
<p
className={cn(
"text-white text-sm opacity-60 max-w-xs",
dmSansClassName(),
)}
>
Your supermemory comes alive when you <br /> capture and connect
what's important
</p>
</div>
<div className="grid grid-cols-2 gap-3 max-w-lg w-full mb-12">
{integrationCards.map((card) => {
const isClickable =
card.title === "Connect to AI" ||
card.title === "Capture" ||
card.title === "Import"
if (isClickable) {
return (
<button
key={card.title}
type="button"
className={cn(
"bg-[#080B0F] relative rounded-lg p-3 hover:border-[#3374FF] hover:border-[0.1px] transition-colors duration-300 border-[0.1px] border-[#0D121A] cursor-pointer text-left w-full hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:175%_auto] hover:bg-[center_top_2rem] hover:bg-no-repeat",
"hover:border-b-0 border-b-0",
)}
onClick={() => {
if (card.title === "Capture") {
analytics.onboardingChromeExtensionClicked({
source: "onboarding",
})
window.open(CHROME_EXTENSION_URL, "_blank")
} else {
analytics.onboardingIntegrationClicked({
integration: card.title,
})
if (card.title === "Connect to AI") {
analytics.onboardingMcpDetailOpened()
} else if (card.title === "Import") {
analytics.onboardingXBookmarksDetailOpened()
}
setSelectedCard(card.title)
}
}}
>
<div className="flex-1 mt-10">
<h3 className="text-white text-sm font-medium">
{card.title}
</h3>
<p
className={cn(
"text-[#8B8B8B] text-xs leading-relaxed",
dmSansClassName(),
)}
>
{card.description}
</p>
</div>
<div className="absolute top-0 right-0">{card.icon}</div>
</button>
)
}
return (
<div
key={card.title}
className={cn(
"bg-[#080B0F] relative rounded-lg p-3 hover:border-[#3374FF] hover:border-[0.1px] transition-colors duration-300 border-[0.1px] border-[#0D121A] hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:175%_auto] hover:bg-[center_top_2rem] hover:bg-no-repeat",
"hover:border-b-0 border-b-0",
)}
>
<div className="flex-1 mt-10">
<h3 className="text-white text-sm font-medium">{card.title}</h3>
<p
className={cn(
"text-[#8B8B8B] text-xs leading-relaxed",
dmSansClassName(),
)}
>
{card.description}
</p>
</div>
<div className="absolute top-0 right-0">{card.icon}</div>
</div>
)
})}
</div>
<div className="flex justify-end w-full max-w-4xl">
<Button
variant="link"
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
onClick={handleContinue}
>
Continue
</Button>
</div>
</div>
)
}

View file

@ -1,195 +0,0 @@
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { motion, type Variants } from "motion/react"
import { useRouter } from "next/navigation"
import { ProfileStep } from "./profile-step"
import { continueVariants, contentVariants } from "@/lib/variants"
type OnboardingView = "continue" | "features" | "memories"
interface OnboardingContentStepProps {
currentView?: OnboardingView
onSubmit?: (data: {
twitter: string
linkedin: string
description: string
otherLinks: string[]
}) => void
}
const containerVariants: Variants = {
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: "easeOut",
},
},
hidden: {
opacity: 0,
transition: {
duration: 0,
},
},
}
export function OnboardingContentStep({
currentView = "continue",
onSubmit,
}: OnboardingContentStepProps) {
const router = useRouter()
const handleContinue = () => {
router.push("/old/onboarding/welcome?step=features")
}
const handleAddMemories = () => {
router.push("/old/onboarding/welcome?step=memories")
}
const isContinue = currentView === "continue"
const isFeatures = currentView === "features"
const isMemories = currentView === "memories"
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="text-center relative"
>
{/* Continue content */}
<motion.div
variants={continueVariants}
animate={isContinue ? "visible" : "hidden"}
initial="visible"
className={cn(
"flex flex-col items-center justify-center max-w-88",
!isContinue && "absolute inset-0 pointer-events-none",
)}
>
<p
className={cn(
"text-[#8A8A8A] text-sm mb-6 max-w-sm",
dmSansClassName(),
)}
>
I'm built with Supermemory's super fast memory API,
<br /> so you never have to worry about forgetting <br /> what matters
across your AI apps.
</p>
<Button
variant="onboarding"
onClick={handleContinue}
style={{
background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
width: "147px",
}}
>
Continue
</Button>
</motion.div>
{/* Features content */}
<motion.div
variants={contentVariants}
animate={isFeatures ? "visible" : "hiddenDown"}
initial="hiddenDown"
className={cn(
"space-y-6 max-w-88",
!isFeatures && "absolute inset-0 pointer-events-none",
)}
>
<h2 className="text-white text-[32px] font-medium leading-[110%]">
What I can do for you
</h2>
<div className={cn("space-y-4 mb-[24px] mx-4", dmSansClassName())}>
<div className="flex items-start space-x-2">
<div className="w-14 h-14 rounded-lg flex items-center justify-center shrink-0">
<img
src="/onboarding/human-brain.png"
alt="Brain icon"
className="w-14 h-14"
/>
</div>
<div className="text-left">
<p className="text-white font-light">Remember every context</p>
<p className="text-[#8A8A8A] text-[14px]">
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 shrink-0">
<img
src="/onboarding/search.png"
alt="Search icon"
className="w-14 h-14"
/>
</div>
<div className="text-left">
<p className="text-white font-light">Find when you need it</p>
<p className="text-[#8A8A8A] text-[14px]">
I surface the right memories inside <br /> 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 shrink-0">
<img
src="/onboarding/plant.png"
alt="Growth icon"
className="w-14 h-14"
/>
</div>
<div className="text-left">
<p className="text-white font-light">
Grow with your supermemory
</p>
<p className="text-[#8A8A8A] text-[14px]">
I learn and personalize over time, so every interaction feels
natural.
</p>
</div>
</div>
</div>
<Button
variant="onboarding"
style={{
background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
}}
onClick={handleAddMemories}
>
Add memories
</Button>
</motion.div>
{/* Memories/Profile content */}
<div
className={cn(
"w-full",
!isMemories && "absolute inset-0 pointer-events-none",
)}
>
{onSubmit && (
<motion.div
variants={contentVariants}
animate={isMemories ? "visible" : "hiddenDown"}
initial="hiddenDown"
>
<ProfileStep onSubmit={onSubmit} />
</motion.div>
)}
</div>
</motion.div>
)
}

View file

@ -1,23 +0,0 @@
import { motion } from "motion/react"
interface GreetingStepProps {
name: string
}
export function GreetingStep({ name }: GreetingStepProps) {
const userName = name ? `${name.split(" ")[0]}` : ""
return (
<motion.div
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-[32px] font-medium mb-2">
Hi {userName}, I'm Nova
</h2>
</motion.div>
)
}

View file

@ -1,89 +0,0 @@
import { motion } from "motion/react"
import { cn } from "@lib/utils"
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
className={cn(
"text-center min-w-[250px] flex flex-col",
isSubmitting && "pointer-events-none",
)}
style={{ gap: "24px" }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.3, ease: "easeOut" } }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
layout
>
<h2 className="text-white text-[32px] font-medium leading-[110%]">
What should I call you?
</h2>
<div className="flex items-center w-full relative">
<LabeledInput
inputType="text"
inputPlaceholder="your name"
className="w-full flex-1"
inputProps={{
defaultValue: name,
disabled: isSubmitting,
onKeyDown: (e) => {
if (e.key !== "Enter") return
e.preventDefault()
if (isSubmitting) return
handleSubmit()
},
className: "!text-white placeholder:!text-[#525966] !h-[40px] pl-4",
}}
onChange={(e) => {
if (isSubmitting) return
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
type="button"
disabled={isSubmitting}
className={`rounded-[8px] w-8 h-8 p-2 absolute right-1 border-[0.5px] border-[#161F2C] hover:cursor-pointer hover:scale-[0.95] active:scale-[0.95] transition-transform ${
isSubmitting ? "scale-[0.90]" : ""
}`}
size="icon"
onClick={handleSubmit}
style={{
background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
}}
>
<svg
width="12"
height="9"
viewBox="0 0 12 9"
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

@ -1,267 +0,0 @@
import { motion } from "motion/react"
import { Button } from "@ui/components/button"
import { useState } from "react"
import { useRouter } from "next/navigation"
import {
parseXHandle,
parseLinkedInHandle,
toXProfileUrl,
toLinkedInProfileUrl,
normalizeUrl,
} from "@/lib/url-helpers"
import { analytics } from "@/lib/analytics"
interface ProfileStepProps {
onSubmit: (data: {
twitter: string
linkedin: string
description: string
otherLinks: string[]
}) => void
}
type ValidationError = {
twitter: string | null
linkedin: string | null
}
export function ProfileStep({ onSubmit }: ProfileStepProps) {
const router = useRouter()
const [otherLinks, setOtherLinks] = useState([""])
const [twitterHandle, setTwitterHandle] = useState("")
const [linkedinProfile, setLinkedinProfile] = useState("")
const [description, setDescription] = useState("")
const [isSubmitting] = useState(false)
const [errors, setErrors] = useState<ValidationError>({
twitter: null,
linkedin: null,
})
const addOtherLink = () => {
if (otherLinks.length < 3) {
setOtherLinks([...otherLinks, ""])
}
}
const updateOtherLink = (index: number, value: string) => {
const updated = [...otherLinks]
updated[index] = value
setOtherLinks(updated)
}
const validateTwitterHandle = (handle: string): string | null => {
if (!handle.trim()) return null
// Basic validation: handle should be alphanumeric, underscore, or hyphen
// X/Twitter handles can contain letters, numbers, and underscores, max 15 chars
const handlePattern = /^[a-zA-Z0-9_]{1,15}$/
if (!handlePattern.test(handle.trim())) {
return "Enter your handle or profile link"
}
return null
}
const validateLinkedInHandle = (handle: string): string | null => {
if (!handle.trim()) return null
// Basic validation: LinkedIn handles are typically alphanumeric with hyphens
// They can be quite long, so we'll be lenient
const handlePattern = /^[a-zA-Z0-9-]+$/
if (!handlePattern.test(handle.trim())) {
return "Enter your handle or profile link"
}
return null
}
const handleTwitterChange = (value: string) => {
setTwitterHandle(value)
setErrors((prev) => ({ ...prev, twitter: null }))
}
const handleTwitterBlur = () => {
if (!twitterHandle.trim()) return
const parsed = parseXHandle(twitterHandle)
setTwitterHandle(parsed)
const error = validateTwitterHandle(parsed)
setErrors((prev) => ({ ...prev, twitter: error }))
}
const handleLinkedInChange = (value: string) => {
setLinkedinProfile(value)
setErrors((prev) => ({ ...prev, linkedin: null }))
}
const handleLinkedInBlur = () => {
if (!linkedinProfile.trim()) return
const parsed = parseLinkedInHandle(linkedinProfile)
setLinkedinProfile(parsed)
const error = validateLinkedInHandle(parsed)
setErrors((prev) => ({ ...prev, linkedin: error }))
}
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 w-full "
>
<h2 className="text-white text-[32px] font-medium mb-4 mt-[-36px]">
Let's add your memories
</h2>
<div className="space-y-4 max-w-[329px] mx-auto overflow-visible gap-4">
<div className="text-left gap-[6px] flex flex-col" id="x-twitter-field">
<label
htmlFor="twitter-handle"
className="text-white text-sm font-medium block pl-2"
>
X/Twitter
</label>
<input
id="twitter-handle"
type="text"
placeholder="x.com/handle or @handle"
value={twitterHandle}
onChange={(e) => handleTwitterChange(e.target.value)}
onBlur={handleTwitterBlur}
className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none focus:border-[#4A4A4A] transition-colors h-[40px] ${
errors.twitter
? "border-[#52596633] bg-[#290F0A]"
: "border-onboarding/20"
}`}
/>
</div>
<div className="text-left gap-[6px] flex flex-col" id="linkedin-field">
<label
htmlFor="linkedin-profile"
className="text-white text-sm font-medium block pl-2"
>
LinkedIn
</label>
<input
id="linkedin-profile"
type="text"
placeholder="linkedin.com/in/username or username"
value={linkedinProfile}
onChange={(e) => handleLinkedInChange(e.target.value)}
onBlur={handleLinkedInBlur}
className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none focus:border-[#4A4A4A] transition-colors h-[40px] ${
errors.linkedin
? "border-[#52596633] bg-[#290F0A]"
: "border-onboarding/20"
}`}
/>
</div>
<div
className="text-left gap-[6px] flex flex-col"
id="other-links-field"
>
<div className="flex items-center justify-between">
<label
htmlFor="other-links"
className="text-white text-sm font-medium pl-2"
>
Other links
</label>
<span className="text-onboarding text-[10px]">Upto 3</span>
</div>
<div className="flex flex-col gap-1.5">
{otherLinks.map((link, index) => (
<div
key={`other-link-${index}`}
className="flex items-center relative"
>
<input
id={`other-links-${index}`}
type="text"
placeholder="Add your website, GitHub, Notion..."
value={link}
onChange={(e) => updateOtherLink(index, e.target.value)}
className="flex-1 px-4 py-2 bg-[#070E1B] border border-onboarding/20 rounded-xl text-white placeholder-onboarding focus:outline-none focus:border-[#4A4A4A] transition-colors h-[40px]"
/>
{index === otherLinks.length - 1 && otherLinks.length < 3 && (
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
addOtherLink()
}}
className="size-8 m-1 absolute right-0 top-0 bg-black border border-[#161F2C] rounded-lg flex items-center justify-center text-white hover:bg-[#161F2C] transition-colors text-xl"
>
+
</button>
)}
</div>
))}
</div>
</div>
<div
className="text-left gap-[6px] flex flex-col"
id="description-field"
>
<label
htmlFor="description"
className="text-white text-sm font-medium block pl-2"
>
What do you do? What do you like?
</label>
<textarea
id="description"
placeholder="Tell me the basics in your words. A few lines about your work, interests, etc."
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={2}
className="w-full px-4 py-2 bg-[#070E1B] border border-onboarding/20 rounded-xl text-white placeholder-onboarding focus:outline-none focus:border-[#4A4A4A] transition-colors min-h-16"
/>
</div>
</div>
<motion.div
animate={{
opacity: 1,
y: 0,
}}
transition={{ duration: 1, ease: "easeOut", delay: 1 }}
initial={{ opacity: 0, y: 10 }}
className="mt-[24px] pb-30"
>
<Button
variant="onboarding"
disabled={isSubmitting}
style={{
background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
}}
onClick={() => {
const formData = {
twitter: toXProfileUrl(parseXHandle(twitterHandle)),
linkedin: toLinkedInProfileUrl(
parseLinkedInHandle(linkedinProfile),
),
description: description,
otherLinks: otherLinks
.filter((l) => l.trim())
.map((l) => normalizeUrl(l.trim())),
}
analytics.onboardingProfileSubmitted({
has_twitter: !!twitterHandle.trim(),
has_linkedin: !!linkedinProfile.trim(),
other_links_count: otherLinks.filter((l) => l.trim()).length,
description_length: description.trim().length,
})
onSubmit(formData)
router.push("/old/onboarding/setup?step=integrations")
}}
>
{isSubmitting ? "Fetching..." : "Remember this →"}
</Button>
</motion.div>
</motion.div>
)
}

View file

@ -1,16 +0,0 @@
import { motion } from "motion/react"
export function WelcomeStep() {
return (
<motion.div
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-[32px] font-medium mb-2">Welcome to...</h2>
</motion.div>
)
}