aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2025-10-01 21:59:53 +0000
committerMaheshtheDev <[email protected]>2025-10-01 21:59:53 +0000
commit60794f63e2bde8a4298fe7677f9cad4942fab3ef (patch)
tree66f13d6d153ccb9c60eb5cb2476326374bc1c315
parentfeat: new onboarding flow (#408) (diff)
downloadsupermemory-60794f63e2bde8a4298fe7677f9cad4942fab3ef.tar.xz
supermemory-60794f63e2bde8a4298fe7677f9cad4942fab3ef.zip
UI: onboarding improvements (#435)09-23-ui_onboarding_improvements
UI: onboarding improvements ui(onboarding): updated onboarding ui patterns
-rw-r--r--apps/web/app/layout.tsx48
-rw-r--r--apps/web/app/onboarding/bio-form.tsx173
-rw-r--r--apps/web/app/onboarding/extension-form.tsx163
-rw-r--r--apps/web/app/onboarding/intro.tsx6
-rw-r--r--apps/web/app/onboarding/mcp-form.tsx441
-rw-r--r--apps/web/app/onboarding/name-form.tsx177
-rw-r--r--apps/web/app/onboarding/nav-menu.tsx2
-rw-r--r--apps/web/app/onboarding/onboarding-background.tsx35
-rw-r--r--apps/web/app/onboarding/onboarding-context.tsx396
-rw-r--r--apps/web/app/onboarding/onboarding-form.tsx183
-rw-r--r--apps/web/app/onboarding/page.tsx44
-rw-r--r--apps/web/app/onboarding/progress-bar.tsx39
-rw-r--r--apps/web/app/onboarding/welcome.tsx45
-rw-r--r--apps/web/app/page.tsx209
-rw-r--r--apps/web/biome.json7
-rw-r--r--apps/web/components/menu.tsx21
-rw-r--r--apps/web/components/tour.tsx414
-rw-r--r--apps/web/lib/analytics.ts3
-rw-r--r--apps/web/lib/tour-constants.ts23
-rw-r--r--apps/web/public/onboarding-complete.pngbin0 -> 3984689 bytes
-rw-r--r--apps/web/public/onboarding.pngbin0 -> 5195205 bytes
-rw-r--r--packages/hooks/use-onboarding-storage.ts33
22 files changed, 1015 insertions, 1447 deletions
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 8b1adc85..70d4916f 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,47 +1,45 @@
-import type { Metadata } from "next";
-import { Inter, JetBrains_Mono, Instrument_Serif } from "next/font/google";
-import "../globals.css";
-import "@ui/globals.css";
-import { AuthProvider } from "@lib/auth-context";
-import { ErrorTrackingProvider } from "@lib/error-tracking";
-import { PostHogProvider } from "@lib/posthog";
-import { QueryProvider } from "@lib/query-client";
-import { AutumnProvider } from "autumn-js/react";
-import { Suspense } from "react";
-import { Toaster } from "sonner";
-import { TourProvider } from "@/components/tour";
-import { MobilePanelProvider } from "@/lib/mobile-panel-context";
-import { NuqsAdapter } from 'nuqs/adapters/next/app'
+import type { Metadata } from "next"
+import { Inter, JetBrains_Mono, Instrument_Serif } from "next/font/google"
+import "../globals.css"
+import "@ui/globals.css"
+import { AuthProvider } from "@lib/auth-context"
+import { ErrorTrackingProvider } from "@lib/error-tracking"
+import { PostHogProvider } from "@lib/posthog"
+import { QueryProvider } from "@lib/query-client"
+import { AutumnProvider } from "autumn-js/react"
+import { Suspense } from "react"
+import { Toaster } from "sonner"
+import { MobilePanelProvider } from "@/lib/mobile-panel-context"
+import { NuqsAdapter } from "nuqs/adapters/next/app"
-
-import { ViewModeProvider } from "@/lib/view-mode-context";
+import { ViewModeProvider } from "@/lib/view-mode-context"
const sans = Inter({
subsets: ["latin"],
variable: "--font-sans",
-});
+})
const mono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
-});
+})
const serif = Instrument_Serif({
subsets: ["latin"],
variable: "--font-serif",
weight: ["400"],
-});
+})
export const metadata: Metadata = {
metadataBase: new URL("https://app.supermemory.ai"),
description: "Your memories, wherever you are",
title: "supermemory app",
-};
+}
export default function RootLayout({
children,
}: Readonly<{
- children: React.ReactNode;
+ children: React.ReactNode
}>) {
return (
<html className="dark bg-sm-black" lang="en">
@@ -61,10 +59,8 @@ export default function RootLayout({
<PostHogProvider>
<ErrorTrackingProvider>
<NuqsAdapter>
- <TourProvider>
- <Suspense>{children}</Suspense>
- <Toaster richColors theme="dark" />
- </TourProvider>
+ <Suspense>{children}</Suspense>
+ <Toaster richColors theme="dark" />
</NuqsAdapter>
</ErrorTrackingProvider>
</PostHogProvider>
@@ -75,5 +71,5 @@ export default function RootLayout({
</AutumnProvider>
</body>
</html>
- );
+ )
}
diff --git a/apps/web/app/onboarding/bio-form.tsx b/apps/web/app/onboarding/bio-form.tsx
index 984309a9..d985a775 100644
--- a/apps/web/app/onboarding/bio-form.tsx
+++ b/apps/web/app/onboarding/bio-form.tsx
@@ -1,86 +1,97 @@
-"use client";
+"use client"
-import { Textarea } from "@ui/components/textarea";
-import { useOnboarding } from "./onboarding-context";
-import { useState } from "react";
-import { Button } from "@ui/components/button";
-import { AnimatePresence, motion } from "motion/react";
-import { NavMenu } from "./nav-menu";
-import { $fetch } from "@lib/api";
+import { Textarea } from "@ui/components/textarea"
+import { useOnboarding } from "./onboarding-context"
+import { useState } from "react"
+import { Button } from "@ui/components/button"
+import { AnimatePresence, motion } from "motion/react"
+import { NavMenu } from "./nav-menu"
+import { $fetch } from "@lib/api"
export function BioForm() {
- const [bio, setBio] = useState("");
- const { totalSteps, nextStep, getStepNumberFor } = useOnboarding();
+ const [bio, setBio] = useState("")
+ const { totalSteps, nextStep, getStepNumberFor } = useOnboarding()
- function handleNext() {
- const trimmed = bio.trim();
- if (!trimmed) {
- nextStep();
- return;
- }
+ function handleNext() {
+ const trimmed = bio.trim()
+ if (!trimmed) {
+ nextStep()
+ return
+ }
- nextStep();
- void $fetch("@post/memories", {
- body: {
- content: trimmed,
- containerTags: ["sm_project_default"],
- metadata: { sm_source: "consumer" },
- },
- }).catch((error) => {
- console.error("Failed to save onboarding bio memory:", error);
- });
- }
- return (
- <div className="relative">
- <div className="space-y-4">
- <NavMenu>
- <p className="text-base text-zinc-600">
- Step {getStepNumberFor("bio")} of {totalSteps}
- </p>
- </NavMenu>
- <h1 className="max-sm:text-4xl">Tell us about yourself</h1>
- <p className="text-zinc-600 text-2xl max-sm:text-lg">
- What should Supermemory know about you?
- </p>
- </div>
- <Textarea
- autoFocus
- className="font-sans mt-6 text-base! tracking-normal font-medium border bg-white! border-zinc-200 rounded-lg !field-sizing-normal !min-h-[calc(3*1.5rem+1rem)]"
- placeholder="I'm a software engineer from San Francisco..."
- rows={3}
- value={bio}
- onChange={(e) => setBio(e.target.value)}
- />
- <AnimatePresence
- mode="sync">
- {
- bio ? (
- <motion.div
- key="save"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.2, ease: "easeOut" }}
- className="flex justify-end mt-2 absolute -bottom-12 right-0">
-
- <Button variant="link" size="lg" className="text-zinc-900 font-medium! text-lg underline w-fit px-0! cursor-pointer" onClick={handleNext}>
- Save & Continue
- </Button>
- </motion.div>
- ) : <motion.div
- key="skip"
- initial={{ opacity: 0, filter: "blur(5px)", }}
- animate={{ opacity: 1, filter: "blur(0px)", }}
- exit={{ opacity: 0, filter: "blur(5px)", }}
- transition={{ duration: 0.2, ease: "easeOut" }}
- className="flex justify-end mt-2 absolute -bottom-12 right-0">
-
- <Button variant="link" size="lg" className="text-zinc-900 font-medium! text-lg underline w-fit px-0! cursor-pointer" onClick={handleNext}>
- Skip For Now
- </Button>
- </motion.div>
- }
- </AnimatePresence>
- </div>
- );
-} \ No newline at end of file
+ nextStep()
+ void $fetch("@post/memories", {
+ body: {
+ content: trimmed,
+ containerTags: ["sm_project_default"],
+ metadata: { sm_source: "consumer" },
+ },
+ }).catch((error) => {
+ console.error("Failed to save onboarding bio memory:", error)
+ })
+ }
+ return (
+ <div className="relative">
+ <div className="space-y-4">
+ <NavMenu>
+ <p className="text-base text-white/60">
+ Step {getStepNumberFor("bio")} of {totalSteps}
+ </p>
+ </NavMenu>
+ <h1 className="max-sm:text-4xl text-white font-medium">
+ Tell us about yourself
+ </h1>
+ <p className="text-2xl max-sm:text-lg text-white/80">
+ What should Supermemory know about you?
+ </p>
+ </div>
+ <Textarea
+ autoFocus
+ className="font-sans mt-6 text-base! text-white tracking-normal font-medium border bg-white/30 border-zinc-200 rounded-lg !field-sizing-normal !min-h-[calc(3*1.5rem+1rem)]"
+ placeholder="I'm a software engineer from San Francisco..."
+ rows={3}
+ value={bio}
+ onChange={(e) => setBio(e.target.value)}
+ />
+ <AnimatePresence mode="sync">
+ {bio ? (
+ <motion.div
+ key="save"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.2, ease: "easeOut" }}
+ className="flex justify-end mt-2 absolute -bottom-12 right-0"
+ >
+ <Button
+ variant="link"
+ size="lg"
+ className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer"
+ onClick={handleNext}
+ >
+ Save & Continue
+ </Button>
+ </motion.div>
+ ) : (
+ <motion.div
+ key="skip"
+ initial={{ opacity: 0, filter: "blur(5px)" }}
+ animate={{ opacity: 1, filter: "blur(0px)" }}
+ exit={{ opacity: 0, filter: "blur(5px)" }}
+ transition={{ duration: 0.2, ease: "easeOut" }}
+ className="flex justify-end mt-2 absolute -bottom-12 right-0"
+ >
+ <Button
+ variant="link"
+ size="lg"
+ className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer"
+ onClick={handleNext}
+ >
+ Skip For Now
+ </Button>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/app/onboarding/extension-form.tsx b/apps/web/app/onboarding/extension-form.tsx
index 8ce40b99..c94512e0 100644
--- a/apps/web/app/onboarding/extension-form.tsx
+++ b/apps/web/app/onboarding/extension-form.tsx
@@ -628,80 +628,91 @@ function TwitterDemo() {
export function ExtensionForm() {
const { totalSteps, nextStep, getStepNumberFor } = useOnboarding();
return (
- <div className="relative flex items-start flex-col gap-6">
- <div className="flex items-center justify-between w-full">
- <div className="flex flex-col items-start text-left gap-4">
- <NavMenu>
- <p className="text-base text-zinc-600">
- Step {getStepNumberFor("extension")} of {totalSteps}
- </p>
- </NavMenu>
- <h1>Install the Chrome extension</h1>
- <p className="text-zinc-600 text-2xl">
- {/* Install the Supermemory extension to start saving and organizing everything that matters. */}
- Bring Supermemory everywhere
- </p>
- </div>
- <div className="flex flex-col items-center text-center gap-4">
- <a href="https://supermemory.ai/extension" target="_blank" className="bg-zinc-50/80 backdrop-blur-lg border-2 hover:bg-zinc-100/80 transition-colors duration-100 border-zinc-200/80 shadow-xs rounded-full mt-6 pl-4.5 pr-6 py-4 text-2xl font-sans tracking-tight font-medium flex items-center gap-4">
- <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Google_Chrome_icon_%28February_2022%29.svg/2048px-Google_Chrome_icon_%28February_2022%29.svg.png" alt="Chrome" className="size-8" />
- Add to Chrome
- </a>
- </div>
- </div>
-
- <div className="grid grid-cols-3 w-fit gap-4 text-base font-sans font-medium tracking-normal">
- <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden">
- <div className="p-4 bg-white">
- <h2 className="text-lg">Remember anything, anywhere</h2>
- <p className="text-zinc-600 text-sm">
- Just right-click to save instantly.
- </p>
- </div>
- <div className="aspect-square bg-blue-500">
- <SnippetDemo />
- </div>
-
- </div>
- <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden">
-
- <div className="p-4 bg-white">
- <h2 className="text-lg">Integrate with your AI</h2>
- {/* Supercharge your AI with memory */}
- {/* Supercharge any AI with Supermemory. */}
- {/* ChatGPT is better with Supermemory. */}
- {/* Seamless integration with your workflow */}
- <p className="text-zinc-600 text-sm">
- {/* Integrates with ChatGPT and Claude. */}
- {/* Integrates with your chat apps */}
- Enhance any prompt with Supermemory.
- {/* Seamlessly */}
- </p>
- </div>
- <div className="aspect-square bg-blue-500">
- <ChatGPTDemo />
- </div>
- </div>
- <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden">
-
- <div className="p-4 bg-white">
- <h2 className="text-lg">Import Twitter bookmarks</h2>
- <p className="text-zinc-600 text-sm">
- Search semantically and effortlessly.
- {/* Import instantly and search effortlessly. */}
- </p>
- </div><div className="aspect-square bg-blue-500">
- <TwitterDemo />
- </div>
-
- </div>
- </div>
- <div className="flex w-full justify-end">
- <Button variant="link" size="lg" className="text-zinc-900 font-medium! text-lg underline w-fit px-0! cursor-pointer" onClick={nextStep}>
- Continue
- <ChevronRightIcon className="size-4" />
- </Button>
- </div>
- </div>
- );
+ <div className="relative flex items-start flex-col gap-6">
+ <div className="flex items-center justify-between w-full">
+ <div className="flex flex-col items-start text-left gap-4">
+ <NavMenu>
+ <p className="text-base text-white/60">
+ Step {getStepNumberFor("extension")} of {totalSteps}
+ </p>
+ </NavMenu>
+ <h1 className="text-white">Install the Chrome extension</h1>
+ <p className="text-white/80 text-2xl">
+ {/* Install the Supermemory extension to start saving and organizing everything that matters. */}
+ Bring Supermemory everywhere
+ </p>
+ </div>
+ <div className="flex flex-col items-center text-center gap-4">
+ <a
+ href="https://chromewebstore.google.com/detail/afpgkkipfdpeaflnpoaffkcankadgjfc?utm_source=item-share-cb"
+ rel="noopener noreferrer"
+ target="_blank"
+ className="bg-zinc-50/80 backdrop-blur-lg border-2 hover:bg-zinc-100/80 transition-colors duration-100 border-zinc-200/80 shadow-xs rounded-full mt-6 pl-4.5 pr-6 py-4 text-2xl font-sans tracking-tight font-medium flex items-center gap-4"
+ >
+ <img
+ src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Google_Chrome_icon_%28February_2022%29.svg/2048px-Google_Chrome_icon_%28February_2022%29.svg.png"
+ alt="Chrome"
+ className="size-8"
+ />
+ Add to Chrome
+ </a>
+ </div>
+ </div>
+
+ <div className="grid grid-cols-3 w-fit gap-4 text-base font-sans font-medium tracking-normal">
+ <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden">
+ <div className="p-4 bg-white">
+ <h2 className="text-lg">Remember anything, anywhere</h2>
+ <p className="text-zinc-600 text-sm">
+ Just right-click to save instantly.
+ </p>
+ </div>
+ <div className="aspect-square bg-blue-500">
+ <SnippetDemo />
+ </div>
+ </div>
+ <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden">
+ <div className="p-4 bg-white">
+ <h2 className="text-lg">Integrate with your AI</h2>
+ {/* Supercharge your AI with memory */}
+ {/* Supercharge any AI with Supermemory. */}
+ {/* ChatGPT is better with Supermemory. */}
+ {/* Seamless integration with your workflow */}
+ <p className="text-zinc-600 text-sm">
+ {/* Integrates with ChatGPT and Claude. */}
+ {/* Integrates with your chat apps */}
+ Enhance any prompt with Supermemory.
+ {/* Seamlessly */}
+ </p>
+ </div>
+ <div className="aspect-square bg-blue-500">
+ <ChatGPTDemo />
+ </div>
+ </div>
+ <div className="flex flex-col w-80 divide-y divide-zinc-200 border border-zinc-200 shadow-xs rounded-xl overflow-hidden">
+ <div className="p-4 bg-white">
+ <h2 className="text-lg">Import Twitter bookmarks</h2>
+ <p className="text-zinc-600 text-sm">
+ Search semantically and effortlessly.
+ {/* Import instantly and search effortlessly. */}
+ </p>
+ </div>
+ <div className="aspect-square bg-blue-500">
+ <TwitterDemo />
+ </div>
+ </div>
+ </div>
+ <div className="flex w-full justify-end">
+ <Button
+ variant="link"
+ size="lg"
+ className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer"
+ onClick={nextStep}
+ >
+ Continue
+ <ChevronRightIcon className="size-4" />
+ </Button>
+ </div>
+ </div>
+ )
} \ No newline at end of file
diff --git a/apps/web/app/onboarding/intro.tsx b/apps/web/app/onboarding/intro.tsx
index ce106b59..5c8504a5 100644
--- a/apps/web/app/onboarding/intro.tsx
+++ b/apps/web/app/onboarding/intro.tsx
@@ -14,7 +14,7 @@ export function Intro() {
return (
<motion.div
- className="flex flex-col gap-4 relative max-sm:text-4xl max-sm:w-full"
+ className="flex flex-col gap-4 relative max-sm:text-4xl max-sm:w-full text-white"
layout
transition={{
layout: { duration: 0.8, ease: "anticipate" }
@@ -76,8 +76,6 @@ export function Intro() {
}}
>
<AnimatedText trigger={triggers.third} delay={0.4}>
- {/* You just found it. */}
- {/* You're about to find it. */}
It's right here.
{/* It's right in front of you. */}
{/* You're looking at it. */}
@@ -101,7 +99,7 @@ export function Intro() {
<Button
variant={"link"}
size={"lg"}
- className={cn("text-zinc-600 flex items-center gap-2 hover:text-zinc-950 text-2xl underline group font-normal w-fit px-0! cursor-pointer", !triggers.fourth && "pointer-events-none")}
+ className={cn("text-white/60 flex items-center gap-2 hover:text-white/90 text-2xl underline group font-normal w-fit px-0! cursor-pointer", !triggers.fourth && "pointer-events-none")}
style={{
transform: triggers.fourth ? "scale(1)" : "scale(0.95)"
}}
diff --git a/apps/web/app/onboarding/mcp-form.tsx b/apps/web/app/onboarding/mcp-form.tsx
index 50d48979..f8561a09 100644
--- a/apps/web/app/onboarding/mcp-form.tsx
+++ b/apps/web/app/onboarding/mcp-form.tsx
@@ -1,206 +1,251 @@
-"use client";
+"use client"
-import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from "@ui/components/select";
-import { useOnboarding } from "./onboarding-context";
-import { useEffect, useState } from "react";
-import { Button } from "@ui/components/button";
-import { CheckIcon, CircleCheckIcon, CopyIcon, LoaderIcon } from "lucide-react";
-import { toast } from "sonner";
-import { TextMorph } from "@/components/text-morph";
-import { NavMenu } from "./nav-menu";
-import { cn } from "@lib/utils";
-import { motion, AnimatePresence } from "framer-motion";
-import { useQuery } from "@tanstack/react-query";
-import { $fetch } from "@lib/api";
+import {
+ Select,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+} from "@ui/components/select"
+import { useOnboarding } from "./onboarding-context"
+import { useEffect, useState } from "react"
+import { Button } from "@ui/components/button"
+import { CheckIcon, CircleCheckIcon, CopyIcon, LoaderIcon } from "lucide-react"
+import { TextMorph } from "@/components/text-morph"
+import { NavMenu } from "./nav-menu"
+import { cn } from "@lib/utils"
+import { motion, AnimatePresence } from "framer-motion"
+import { useQuery } from "@tanstack/react-query"
+import { $fetch } from "@lib/api"
const clients = {
- cursor: "Cursor",
- claude: "Claude Desktop",
- vscode: "VSCode",
- cline: "Cline",
- "roo-cline": "Roo Cline",
- witsy: "Witsy",
- enconvo: "Enconvo",
- "gemini-cli": "Gemini CLI",
- "claude-code": "Claude Code",
-} as const;
+ cursor: "Cursor",
+ claude: "Claude Desktop",
+ vscode: "VSCode",
+ cline: "Cline",
+ "roo-cline": "Roo Cline",
+ witsy: "Witsy",
+ enconvo: "Enconvo",
+ "gemini-cli": "Gemini CLI",
+ "claude-code": "Claude Code",
+} as const
export function MCPForm() {
- const { totalSteps, nextStep, getStepNumberFor } = useOnboarding();
- const [client, setClient] = useState<keyof typeof clients>("cursor");
- const [isCopied, setIsCopied] = useState(false);
- const [isInstalling, setIsInstalling] = useState(true);
+ const { totalSteps, nextStep, getStepNumberFor } = useOnboarding()
+ const [client, setClient] = useState<keyof typeof clients>("cursor")
+ const [isCopied, setIsCopied] = useState(false)
+ const [isInstalling, setIsInstalling] = useState(true)
- const hasLoginQuery = useQuery({
- queryKey: ["mcp", "has-login"],
- queryFn: async (): Promise<{ previousLogin: boolean }> => {
- const response = await $fetch("@get/mcp/has-login");
- if (response.error) {
- throw new Error(response.error?.message || "Failed to check MCP login");
- }
- return response.data as { previousLogin: boolean };
- },
- enabled: isInstalling,
- refetchInterval: isInstalling ? 1000 : false,
- staleTime: 0,
- });
+ const hasLoginQuery = useQuery({
+ queryKey: ["mcp", "has-login"],
+ queryFn: async (): Promise<{ previousLogin: boolean }> => {
+ const response = await $fetch("@get/mcp/has-login")
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to check MCP login")
+ }
+ return response.data as { previousLogin: boolean }
+ },
+ enabled: isInstalling,
+ refetchInterval: isInstalling ? 1000 : false,
+ staleTime: 0,
+ })
- useEffect(() => {
- if (hasLoginQuery.data?.previousLogin) {
- setIsInstalling(false);
- }
- }, [hasLoginQuery.data?.previousLogin]);
+ useEffect(() => {
+ if (hasLoginQuery.data?.previousLogin) {
+ setIsInstalling(false)
+ }
+ }, [hasLoginQuery.data?.previousLogin])
- return (
- <div className="relative flex flex-col gap-6">
- <div className="space-y-4">
- <NavMenu>
- <p className="text-base text-zinc-600">
- Step {getStepNumberFor("mcp")} of {totalSteps}
- </p>
- </NavMenu>
- <h1 className="max-sm:text-4xl">Install the MCP server</h1>
- <p className="text-zinc-600 text-2xl max-sm:text-lg">
- Bring Supermemory to all your favourite tools
- </p>
- </div>
- <div className="flex flex-col gap-4 font-sans text-base tracking-normal font-normal">
- <div className="flex gap-4">
- <div className="relative flex-shrink-0">
- <div style={{
- height: "calc(100% - 0.5rem)",
- }} className="absolute -z-10 left-1/2 top-8 w-[1px] -translate-x-1/2 transform bg-neutral-200"></div>
- <div className="size-10 rounded-lg bg-zinc-100 border-zinc-200 border text-zinc-900 font-medium flex items-center justify-center">1</div>
- </div>
- <div className="mt-2 space-y-2 w-full">
- <p>Select the app you want to install Supermemory MCP to</p>
- <Select
- onValueChange={(value) => setClient(value as keyof typeof clients)}
- value={client}
- >
- <SelectTrigger id="client-select" className="w-full border border-zinc-200 bg-white!">
- <SelectValue placeholder="Select client" />
- </SelectTrigger>
- <SelectContent>
- {Object.entries(clients).map(([key, value]) => (
- <SelectItem key={key} value={key}>
- {value}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- </div>
- <div className="flex gap-4">
- <div className="relative flex-shrink-0">
- <div className="size-10 rounded-lg border-zinc-200 border bg-zinc-100 text-zinc-900 font-medium flex items-center justify-center">2</div>
- <div style={{
- height: "calc(100% - 0.5rem)",
- }} className="absolute left-1/2 -z-10 top-8 w-[1px] -translate-x-1/2 transform bg-neutral-200"></div>
- </div>
- <div className="mt-2 space-y-2">
- <p>Copy the installation command</p>
- <div className="bg-white relative shadow-xs rounded-lg max-w-md text-balance py-4 px-5 border border-zinc-200">
- <p className="text-zinc-900 font-mono text-xs w-4/5">
- npx -y install-mcp@latest https://api.supermemory.ai/mcp --client {client} --oauth=yes
- </p>
- <Button className="absolute right-2 top-2" onClick={() => {
- navigator.clipboard.writeText(`npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`);
- setIsCopied(true);
- setTimeout(() => {
- setIsCopied(false);
- }, 2000);
- }}>
- {isCopied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
- <TextMorph>
- {isCopied ? "Copied!" : "Copy"}
- </TextMorph>
- </Button>
- </div>
- </div>
- </div>
- <div className="flex gap-4">
- <div className="relative flex-shrink-0">
- <div className="size-10 rounded-lg bg-zinc-100 border-zinc-200 border text-zinc-900 font-medium flex items-center justify-center">3</div>
- </div>
- <div className="mt-2 space-y-2 w-full">
- <p>Run the command in your terminal of choice</p>
- <motion.div
- className={cn("px-5 py-4 border shadow-xs rounded-lg flex items-center gap-3 font-mono text-sm")}
- animate={{
- backgroundColor: isInstalling ? "rgb(250 250 250)" : "rgb(240 253 244)", // zinc-50 to green-50
- borderColor: isInstalling ? "rgb(228 228 231)" : "rgb(187 247 208)", // zinc-200 to green-200
- }}
- transition={{
- duration: 0.3,
- ease: "easeInOut"
- }}
- >
- <AnimatePresence mode="wait">
- {isInstalling ? (
- <motion.div
- key="loading"
- initial={{ opacity: 0, scale: 0.8 }}
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: 0.8 }}
- transition={{ duration: 0.2 }}
- >
- <LoaderIcon className="size-4 animate-spin" />
- </motion.div>
- ) : (
- <motion.div
- key="success"
- initial={{ opacity: 0, scale: 0.8 }}
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: 0.8 }}
- transition={{ duration: 0.2 }}
- >
- <CircleCheckIcon className="size-4 text-green-500" />
- </motion.div>
- )}
- </AnimatePresence>
- <motion.span
- key={isInstalling ? "installing" : "complete"}
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.3, ease: "easeOut" }}
- >
- {isInstalling ? "Waiting for installation..." : "Installation complete!"}
- </motion.span>
- </motion.div>
- </div>
- </div>
- </div>
- <AnimatePresence
- mode="sync">
- {
- !isInstalling ? (
- <motion.div
- key="save"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.2, ease: "easeOut" }}
- className="flex justify-end">
-
- <Button variant="link" size="lg" className="text-zinc-900 font-medium! text-lg underline w-fit px-0! cursor-pointer" onClick={nextStep}>
- Continue
- </Button>
- </motion.div>
- ) : <motion.div
- key="skip"
- initial={{ opacity: 0, filter: "blur(5px)", }}
- animate={{ opacity: 1, filter: "blur(0px)", }}
- exit={{ opacity: 0, filter: "blur(5px)", }}
- transition={{ duration: 0.2, ease: "easeOut" }}
- className="flex justify-end">
-
- <Button variant="link" size="lg" className="text-zinc-900 font-medium! text-lg underline w-fit px-0! cursor-pointer" onClick={nextStep}>
- Skip For Now
- </Button>
- </motion.div>
- }
- </AnimatePresence>
- </div>
- );
-} \ No newline at end of file
+ return (
+ <div className="relative flex flex-col gap-6">
+ <div className="space-y-4">
+ <NavMenu>
+ <p className="text-base text-white/60">
+ Step {getStepNumberFor("mcp")} of {totalSteps}
+ </p>
+ </NavMenu>
+ <h1 className="max-sm:text-4xl text-white font-medium">
+ Install the MCP server
+ </h1>
+ <p className="text-2xl max-sm:text-lg text-white/80">
+ Bring Supermemory to all your favourite tools
+ </p>
+ </div>
+ <div className="flex flex-col gap-4 font-sans text-base tracking-normal font-normal">
+ <div className="flex gap-4">
+ <div className="relative flex-shrink-0">
+ <div
+ style={{
+ height: "calc(100% - 0.5rem)",
+ }}
+ className="absolute -z-10 left-1/2 top-8 w-[1px] -translate-x-1/2 transform bg-white/10"
+ />
+ <div className="size-10 rounded-lg bg-white/10 text-white font-medium flex items-center justify-center">
+ 1
+ </div>
+ </div>
+ <div className="mt-2 space-y-2 w-full">
+ <p className="text-white/80">
+ Select the app you want to install Supermemory MCP to
+ </p>
+ <Select
+ onValueChange={(value) =>
+ setClient(value as keyof typeof clients)
+ }
+ value={client}
+ >
+ <SelectTrigger
+ id="client-select"
+ className="w-full bg-white/10! text-white"
+ >
+ <SelectValue placeholder="Select client" />
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(clients).map(([key, value]) => (
+ <SelectItem key={key} value={key}>
+ {value}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+ <div className="flex gap-4">
+ <div className="relative flex-shrink-0">
+ <div className="size-10 rounded-lg bg-white/10 text-white font-medium flex items-center justify-center">
+ 2
+ </div>
+ <div
+ style={{
+ height: "calc(100% - 0.5rem)",
+ }}
+ className="absolute left-1/2 -z-10 top-8 w-[1px] -translate-x-1/2 transform bg-white/10"
+ />
+ </div>
+ <div className="mt-2 space-y-2">
+ <p className="text-white/80">Copy the installation command</p>
+ <div className="bg-white/10 relative shadow-xs rounded-lg max-w-md text-balance py-4 px-5 align-middle justify-center">
+ <p className="text-white font-mono text-xs w-4/5 text-nowrap overflow-x-hidden text-ellipsis">
+ npx -y install-mcp@latest https://api.supermemory.ai/mcp
+ --client {client} --oauth=yes
+ </p>
+ <Button
+ className="absolute right-2 top-[6px]"
+ onClick={() => {
+ navigator.clipboard.writeText(
+ `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`,
+ )
+ setIsCopied(true)
+ setTimeout(() => {
+ setIsCopied(false)
+ }, 2000)
+ }}
+ >
+ {isCopied ? (
+ <CheckIcon className="size-4" />
+ ) : (
+ <CopyIcon className="size-4" />
+ )}
+ <TextMorph>{isCopied ? "Copied!" : "Copy"}</TextMorph>
+ </Button>
+ </div>
+ </div>
+ </div>
+ <div className="flex gap-4">
+ <div className="relative flex-shrink-0">
+ <div className="size-10 rounded-lg bg-white/10 text-white font-medium flex items-center justify-center">
+ 3
+ </div>
+ </div>
+ <div className="mt-2 space-y-2 w-full">
+ <p className="text-white/80">
+ Run the command in your terminal of choice
+ </p>
+ <motion.div
+ className={cn(
+ "px-5 py-4 bg-black/10 text-white shadow-xs rounded-lg flex items-center gap-3 font-mono text-sm",
+ )}
+ transition={{
+ duration: 0.3,
+ ease: "easeInOut",
+ }}
+ >
+ <AnimatePresence mode="wait">
+ {isInstalling ? (
+ <motion.div
+ key="loading"
+ initial={{ opacity: 0, scale: 0.8 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.8 }}
+ transition={{ duration: 0.2 }}
+ >
+ <LoaderIcon className="size-4 animate-spin" />
+ </motion.div>
+ ) : (
+ <motion.div
+ key="success"
+ initial={{ opacity: 0, scale: 0.8 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.8 }}
+ transition={{ duration: 0.2 }}
+ >
+ <CircleCheckIcon className="size-4 text-white" />
+ </motion.div>
+ )}
+ </AnimatePresence>
+ <motion.span
+ key={isInstalling ? "installing" : "complete"}
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ {isInstalling
+ ? "Waiting for installation..."
+ : "Installation complete!"}
+ </motion.span>
+ </motion.div>
+ </div>
+ </div>
+ </div>
+ <AnimatePresence mode="sync">
+ {!isInstalling ? (
+ <motion.div
+ key="save"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.2, ease: "easeOut" }}
+ className="flex justify-end"
+ >
+ <Button
+ variant="link"
+ size="lg"
+ className="text-black/40 font-medium! text-lg underline w-fit px-0! cursor-pointer"
+ onClick={nextStep}
+ >
+ Continue
+ </Button>
+ </motion.div>
+ ) : (
+ <motion.div
+ key="skip"
+ initial={{ opacity: 0, filter: "blur(5px)" }}
+ animate={{ opacity: 1, filter: "blur(0px)" }}
+ exit={{ opacity: 0, filter: "blur(5px)" }}
+ transition={{ duration: 0.2, ease: "easeOut" }}
+ className="flex justify-end"
+ >
+ <Button
+ variant="link"
+ size="lg"
+ className="text-black/40 font-medium! text-lg underline w-fit px-0! cursor-pointer"
+ onClick={nextStep}
+ >
+ Skip For Now
+ </Button>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/app/onboarding/name-form.tsx b/apps/web/app/onboarding/name-form.tsx
index c112933c..2b9520c9 100644
--- a/apps/web/app/onboarding/name-form.tsx
+++ b/apps/web/app/onboarding/name-form.tsx
@@ -1,95 +1,102 @@
-"use client";
+"use client"
-import { Button } from "@repo/ui/components/button";
-import { useOnboarding } from "./onboarding-context";
-import { useAuth } from "@lib/auth-context";
-import Link from "next/link";
-import { useEffect, useState } from "react";
-import { Input } from "@ui/components/input";
-import { CheckIcon } from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import { NavMenu } from "./nav-menu";
-import { authClient } from "@lib/auth";
+import { Button } from "@repo/ui/components/button"
+import { useOnboarding } from "./onboarding-context"
+import { useAuth } from "@lib/auth-context"
+import Link from "next/link"
+import { useEffect, useState } from "react"
+import { Input } from "@ui/components/input"
+import { CheckIcon } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import { NavMenu } from "./nav-menu"
+import { authClient } from "@lib/auth"
export function NameForm() {
- const { nextStep, totalSteps, getStepNumberFor } = useOnboarding();
- const { user } = useAuth();
- const [name, setName] = useState(user?.name ?? "");
+ const { nextStep, totalSteps, getStepNumberFor } = useOnboarding()
+ const { user } = useAuth()
+ const [name, setName] = useState(user?.name ?? "")
- useEffect(() => {
- if (!name && user?.name) {
- setName(user.name);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [user?.name]);
+ useEffect(() => {
+ if (!name && user?.name) {
+ setName(user.name)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [user?.name])
- function handleNext(): void {
- const trimmed = name.trim();
- if (!trimmed) {
- nextStep();
- return;
- }
+ function handleNext(): void {
+ const trimmed = name.trim()
+ if (!trimmed) {
+ nextStep()
+ return
+ }
- nextStep();
- void authClient
- .updateUser({ name: trimmed })
- .catch((error: unknown) => {
- console.error("Failed to update user name during onboarding:", error);
- });
- }
+ nextStep()
+ void authClient.updateUser({ name: trimmed }).catch((error: unknown) => {
+ console.error("Failed to update user name during onboarding:", error)
+ })
+ }
- function handleSubmit(e: React.FormEvent): void {
- e.preventDefault();
- handleNext();
- }
+ function handleSubmit(e: React.FormEvent): void {
+ e.preventDefault()
+ handleNext()
+ }
- if (!user) {
- return (
- <div className="flex flex-col gap-6">
- <h1 className="text-4xl">You need to sign in to continue</h1>
- <Link href="/login">Login</Link>
- </div>
- );
- }
+ if (!user) {
+ return (
+ <div className="flex flex-col gap-6">
+ <h1 className="text-4xl">You need to sign in to continue</h1>
+ <Link href="/login">Login</Link>
+ </div>
+ )
+ }
- return (
- <div className="flex flex-col gap-4">
- <NavMenu>
- <p className="text-base text-zinc-600">
- Step {getStepNumberFor("name")} of {totalSteps}
- </p>
- </NavMenu>
- <h1 className="text-4xl">
- What should we call you?
- </h1>
+ return (
+ <div className="flex flex-col gap-4">
+ <NavMenu>
+ <p className="text-base text-white/60">
+ Step {getStepNumberFor("name")} of {totalSteps}
+ </p>
+ </NavMenu>
+ <p className="text-4xl text-white font-medium">What should we call you?</p>
- <form onSubmit={handleSubmit} className="flex flex-col group">
- <div className="relative flex flex-col">
- <input type="text" autoFocus name="name" autoComplete="name" autoCorrect="off" autoCapitalize="none" spellCheck="false" className="text-black outline-0 text-2xl h-12 font-normal p-0" placeholder="John Doe" value={name} onChange={(e) => setName(e.target.value)} />
- <AnimatePresence
- mode="popLayout">
- {
- name && (
- <motion.div
- animate={{ opacity: 1 }}
- exit={{ opacity: 0 }}
- initial={{ opacity: 0 }}
- key="next"
- transition={{ duration: 0.15 }}
- className="absolute pointer-events-none inset-0 flex items-center justify-end">
- <button type="submit" className="cursor-pointer transition-colors duration-150 gap-2 pointer-events-auto flex items-center p-2 hover:bg-zinc-100 rounded-lg">
- <CheckIcon className="w-4 h-4" />
- {/* <span className="text-sm">Next</span> */}
- </button>
- </motion.div>
- )
- }
- </AnimatePresence>
- </div>
- <div className="w-full rounded-full group-focus-within:bg-zinc-400 transition-colors h-px bg-zinc-200"></div>
- </form>
-
-
- </div>
- );
-} \ No newline at end of file
+ <form onSubmit={handleSubmit} className="flex flex-col group text-white">
+ <div className="relative flex flex-col">
+ <input
+ type="text"
+ autoFocus
+ name="name"
+ autoComplete="name"
+ autoCorrect="off"
+ autoCapitalize="none"
+ spellCheck="false"
+ className="outline-0 text-2xl h-12 font-normal p-0"
+ placeholder="John Doe"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ />
+ <AnimatePresence mode="popLayout">
+ {name && (
+ <motion.div
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ initial={{ opacity: 0 }}
+ key="next"
+ transition={{ duration: 0.15 }}
+ className="absolute pointer-events-none inset-0 flex items-center justify-end"
+ >
+ <button
+ type="submit"
+ className="cursor-pointer transition-colors duration-150 gap-2 pointer-events-auto flex items-center p-2 hover:bg-zinc-100 hover:text-black/90 rounded-lg"
+ >
+ <CheckIcon className="w-4 h-4" />
+ {/* <span className="text-sm">Next</span> */}
+ </button>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ <div className="w-full rounded-full group-focus-within:bg-zinc-400 transition-colors h-px bg-zinc-200" />
+ </form>
+ </div>
+ )
+}
diff --git a/apps/web/app/onboarding/nav-menu.tsx b/apps/web/app/onboarding/nav-menu.tsx
index e66187d2..957cbbef 100644
--- a/apps/web/app/onboarding/nav-menu.tsx
+++ b/apps/web/app/onboarding/nav-menu.tsx
@@ -15,7 +15,7 @@ export function NavMenu({ children }: { children: React.ReactNode }) {
intro: "Intro",
name: "Name",
bio: "About you",
- connections: "Connections",
+ // connections: "Connections",
mcp: "MCP",
extension: "Extension",
welcome: "Welcome",
diff --git a/apps/web/app/onboarding/onboarding-background.tsx b/apps/web/app/onboarding/onboarding-background.tsx
new file mode 100644
index 00000000..366648fa
--- /dev/null
+++ b/apps/web/app/onboarding/onboarding-background.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import { useOnboarding } from "./onboarding-context"
+
+interface OnboardingBackgroundProps {
+ children: React.ReactNode
+}
+
+export function OnboardingBackground({ children }: OnboardingBackgroundProps) {
+ const { currentStep, visibleSteps } = useOnboarding()
+
+ const backgroundImage = "url(/onboarding.png)"
+
+ const currentZoomStepIndex = visibleSteps.indexOf(currentStep)
+
+ const zoomScale =
+ currentZoomStepIndex >= 0 ? 1.0 + currentZoomStepIndex * 0.1 : 1.0
+
+ return (
+ <div className="min-h-screen w-full overflow-x-hidden text-zinc-900 flex items-center justify-center relative">
+ <div
+ className="absolute inset-0 transition-transform duration-700 ease-in-out"
+ style={{
+ backgroundImage,
+ backgroundSize: "cover",
+ backgroundPosition: "bottom",
+ backgroundRepeat: "no-repeat",
+ transform: `scale(${zoomScale})`,
+ transformOrigin: "center bottom",
+ }}
+ />
+ <div className="relative z-10">{children}</div>
+ </div>
+ )
+}
diff --git a/apps/web/app/onboarding/onboarding-context.tsx b/apps/web/app/onboarding/onboarding-context.tsx
index 13e13139..260fb51e 100644
--- a/apps/web/app/onboarding/onboarding-context.tsx
+++ b/apps/web/app/onboarding/onboarding-context.tsx
@@ -1,202 +1,232 @@
-"use client";
-
-import { createContext, useContext, useState, useEffect, type ReactNode, useMemo } from "react";
-import { useQueryState } from "nuqs";
-import { useIsMobile } from "@hooks/use-mobile";
+"use client"
+
+import {
+ createContext,
+ useContext,
+ useState,
+ useEffect,
+ type ReactNode,
+ useMemo,
+} from "react"
+import { useQueryState } from "nuqs"
+import { useIsMobile } from "@hooks/use-mobile"
// Define the context interface
interface OnboardingContextType {
- currentStep: OnboardingStep;
- setStep: (step: OnboardingStep) => void;
- nextStep: () => void;
- previousStep: () => void;
- totalSteps: number;
- currentStepIndex: number;
- // Visible-step aware helpers
- visibleSteps: OnboardingStep[];
- currentVisibleStepIndex: number;
- currentVisibleStepNumber: number;
- getStepNumberFor: (step: OnboardingStep) => number;
- introTriggers: {
- first: boolean;
- second: boolean;
- third: boolean;
- fourth: boolean;
- };
- orbsRevealed: boolean;
- resetIntroTriggers: () => void;
+ currentStep: OnboardingStep
+ setStep: (step: OnboardingStep) => void
+ nextStep: () => void
+ previousStep: () => void
+ totalSteps: number
+ currentStepIndex: number
+ // Visible-step aware helpers
+ visibleSteps: OnboardingStep[]
+ currentVisibleStepIndex: number
+ currentVisibleStepNumber: number
+ getStepNumberFor: (step: OnboardingStep) => number
+ introTriggers: {
+ first: boolean
+ second: boolean
+ third: boolean
+ fourth: boolean
+ }
+ orbsRevealed: boolean
+ resetIntroTriggers: () => void
}
// Create the context
-const OnboardingContext = createContext<OnboardingContextType | undefined>(undefined);
+const OnboardingContext = createContext<OnboardingContextType | undefined>(
+ undefined,
+)
// Define the base step order
-const BASE_STEP_ORDER = ["intro", "name", "bio", "connections", "mcp", "extension", "welcome"] as const;
+const BASE_STEP_ORDER = [
+ "intro",
+ "name",
+ "bio",
+ "mcp",
+ "extension",
+ "welcome",
+] as const
-export type OnboardingStep = (typeof BASE_STEP_ORDER)[number];
+export type OnboardingStep = (typeof BASE_STEP_ORDER)[number]
interface OnboardingProviderProps {
- children: ReactNode;
- initialStep?: OnboardingStep;
+ children: ReactNode
+ initialStep?: OnboardingStep
}
-export function OnboardingProvider({ children, initialStep = "intro" }: OnboardingProviderProps) {
- // Helper function to validate if a step is valid
- const isValidStep = (step: string): step is OnboardingStep => {
- return BASE_STEP_ORDER.includes(step as OnboardingStep);
- };
-
- const [currentStep, setCurrentStep] = useQueryState("step", {
- defaultValue: initialStep,
- parse: (value: string) => {
- // Validate the step from URL - if invalid, use the initial step
- return isValidStep(value) ? value : initialStep;
- },
- serialize: (value: OnboardingStep) => value,
- });
- const [orbsRevealed, setOrbsRevealed] = useState(false);
- const [introTriggers, setIntroTriggers] = useState({
- first: false,
- second: false,
- third: false,
- fourth: false
- });
- const isMobile = useIsMobile();
-
- // Compute visible steps based on device
- const visibleSteps = useMemo(() => {
- if (isMobile) {
- // On mobile, hide MCP and Extension steps
- return BASE_STEP_ORDER.filter(s => s !== "mcp" && s !== "extension");
- }
- return [...BASE_STEP_ORDER];
- }, [isMobile]);
-
- // Setup intro trigger timings when on intro step
- useEffect(() => {
- if (currentStep !== "intro") return;
-
- const cleanups = [
- setTimeout(() => {
- setIntroTriggers(prev => ({ ...prev, first: true }));
- }, 300),
- setTimeout(() => {
- setIntroTriggers(prev => ({ ...prev, second: true }));
- }, 2000),
- setTimeout(() => {
- setIntroTriggers(prev => ({ ...prev, third: true }));
- }, 4000),
- setTimeout(() => {
- setIntroTriggers(prev => ({ ...prev, fourth: true }));
- }, 5500),
- ];
-
- return () => cleanups.forEach(cleanup => clearTimeout(cleanup));
- }, [currentStep]);
-
- // Set orbs as revealed once the fourth trigger is activated OR if we're on any non-intro step
- useEffect(() => {
- if (currentStep !== "intro") {
- // If we're not on the intro step, orbs should always be visible
- // (user has either completed intro or navigated directly to another step)
- if (!orbsRevealed) {
- setOrbsRevealed(true);
- }
- } else if (introTriggers.fourth && !orbsRevealed) {
- // On intro step, reveal orbs only after the fourth trigger
- setOrbsRevealed(true);
- }
- }, [introTriggers.fourth, orbsRevealed, currentStep]);
-
- // Ensure current step is always part of visible steps; if not, advance to the next visible step
- useEffect(() => {
- if (!visibleSteps.includes(currentStep)) {
- if (visibleSteps.length === 0) return;
- const baseIndex = BASE_STEP_ORDER.indexOf(currentStep);
- // Find the next visible step after the current base index
- const nextAfterBase = visibleSteps.find(step => BASE_STEP_ORDER.indexOf(step) > baseIndex);
- const targetStep = nextAfterBase ?? visibleSteps[visibleSteps.length - 1]!;
- setCurrentStep(targetStep);
- }
- }, [visibleSteps, currentStep]);
-
- function setStep(step: OnboardingStep) {
- setCurrentStep(step);
- }
-
- function nextStep() {
- const currentIndex = visibleSteps.indexOf(currentStep);
- const nextIndex = currentIndex + 1;
-
- if (nextIndex < visibleSteps.length) {
- setStep(visibleSteps[nextIndex]!);
- }
- }
-
- function previousStep() {
- const currentIndex = visibleSteps.indexOf(currentStep);
- const previousIndex = currentIndex - 1;
-
- if (previousIndex >= 0) {
- setStep(visibleSteps[previousIndex]!);
- }
- }
-
- function resetIntroTriggers() {
- setIntroTriggers({
- first: false,
- second: false,
- third: false,
- fourth: false
- });
- }
-
- const currentStepIndex = BASE_STEP_ORDER.indexOf(currentStep);
-
- // Visible-step aware helpers
- const stepsForNumbering = useMemo(() => visibleSteps.filter(s => s !== "intro" && s !== "welcome"), [visibleSteps]);
-
- function getStepNumberFor(step: OnboardingStep): number {
- if (step === "intro" || step === "welcome") {
- return 0;
- }
- const idx = stepsForNumbering.indexOf(step);
- return idx === -1 ? 0 : idx + 1;
- }
-
- const currentVisibleStepIndex = useMemo(() => visibleSteps.indexOf(currentStep), [visibleSteps, currentStep]);
- const currentVisibleStepNumber = useMemo(() => getStepNumberFor(currentStep), [currentStep, stepsForNumbering]);
- const totalSteps = stepsForNumbering.length;
-
- const contextValue: OnboardingContextType = {
- currentStep,
- setStep,
- nextStep,
- previousStep,
- totalSteps,
- currentStepIndex,
- visibleSteps,
- currentVisibleStepIndex,
- currentVisibleStepNumber,
- getStepNumberFor,
- introTriggers,
- orbsRevealed,
- resetIntroTriggers,
- };
-
- return (
- <OnboardingContext.Provider value={contextValue}>
- {children}
- </OnboardingContext.Provider>
- );
+export function OnboardingProvider({
+ children,
+ initialStep = "intro",
+}: OnboardingProviderProps) {
+ // Helper function to validate if a step is valid
+ const isValidStep = (step: string): step is OnboardingStep => {
+ return BASE_STEP_ORDER.includes(step as OnboardingStep)
+ }
+
+ const [currentStep, setCurrentStep] = useQueryState("step", {
+ defaultValue: initialStep,
+ parse: (value: string) => {
+ // Validate the step from URL - if invalid, use the initial step
+ return isValidStep(value) ? value : initialStep
+ },
+ serialize: (value: OnboardingStep) => value,
+ })
+ const [orbsRevealed, setOrbsRevealed] = useState(false)
+ const [introTriggers, setIntroTriggers] = useState({
+ first: false,
+ second: false,
+ third: false,
+ fourth: false,
+ })
+ const isMobile = useIsMobile()
+
+ // Compute visible steps based on device
+ const visibleSteps = useMemo(() => {
+ if (isMobile) {
+ // On mobile, hide MCP and Extension steps
+ return BASE_STEP_ORDER.filter((s) => s !== "mcp" && s !== "extension")
+ }
+ return [...BASE_STEP_ORDER]
+ }, [isMobile])
+
+ // Setup intro trigger timings when on intro step
+ useEffect(() => {
+ if (currentStep !== "intro") return
+
+ const cleanups = [
+ setTimeout(() => {
+ setIntroTriggers((prev) => ({ ...prev, first: true }))
+ }, 300),
+ setTimeout(() => {
+ setIntroTriggers((prev) => ({ ...prev, second: true }))
+ }, 300),
+ setTimeout(() => {
+ setIntroTriggers((prev) => ({ ...prev, third: true }))
+ }, 300),
+ setTimeout(() => {
+ setIntroTriggers((prev) => ({ ...prev, fourth: true }))
+ }, 400),
+ ]
+
+ return () => cleanups.forEach((cleanup) => clearTimeout(cleanup))
+ }, [currentStep])
+
+ // Set orbs as revealed once the fourth trigger is activated OR if we're on any non-intro step
+ useEffect(() => {
+ if (currentStep !== "intro") {
+ // If we're not on the intro step, orbs should always be visible
+ // (user has either completed intro or navigated directly to another step)
+ if (!orbsRevealed) {
+ setOrbsRevealed(true)
+ }
+ } else if (introTriggers.fourth && !orbsRevealed) {
+ // On intro step, reveal orbs only after the fourth trigger
+ setOrbsRevealed(true)
+ }
+ }, [introTriggers.fourth, orbsRevealed, currentStep])
+
+ // Ensure current step is always part of visible steps; if not, advance to the next visible step
+ useEffect(() => {
+ if (!visibleSteps.includes(currentStep)) {
+ if (visibleSteps.length === 0) return
+ const baseIndex = BASE_STEP_ORDER.indexOf(currentStep)
+ // Find the next visible step after the current base index
+ const nextAfterBase = visibleSteps.find(
+ (step) => BASE_STEP_ORDER.indexOf(step) > baseIndex,
+ )
+ const targetStep = nextAfterBase ?? visibleSteps[visibleSteps.length - 1]!
+ setCurrentStep(targetStep)
+ }
+ }, [visibleSteps, currentStep])
+
+ function setStep(step: OnboardingStep) {
+ setCurrentStep(step)
+ }
+
+ function nextStep() {
+ const currentIndex = visibleSteps.indexOf(currentStep)
+ const nextIndex = currentIndex + 1
+
+ if (nextIndex < visibleSteps.length) {
+ setStep(visibleSteps[nextIndex]!)
+ }
+ }
+
+ function previousStep() {
+ const currentIndex = visibleSteps.indexOf(currentStep)
+ const previousIndex = currentIndex - 1
+
+ if (previousIndex >= 0) {
+ setStep(visibleSteps[previousIndex]!)
+ }
+ }
+
+ function resetIntroTriggers() {
+ setIntroTriggers({
+ first: false,
+ second: false,
+ third: false,
+ fourth: false,
+ })
+ }
+
+ const currentStepIndex = BASE_STEP_ORDER.indexOf(currentStep)
+
+ // Visible-step aware helpers
+ const stepsForNumbering = useMemo(
+ () => visibleSteps.filter((s) => s !== "intro" && s !== "welcome"),
+ [visibleSteps],
+ )
+
+ function getStepNumberFor(step: OnboardingStep): number {
+ if (step === "intro" || step === "welcome") {
+ return 0
+ }
+ const idx = stepsForNumbering.indexOf(step)
+ return idx === -1 ? 0 : idx + 1
+ }
+
+ const currentVisibleStepIndex = useMemo(
+ () => visibleSteps.indexOf(currentStep),
+ [visibleSteps, currentStep],
+ )
+ const currentVisibleStepNumber = useMemo(
+ () => getStepNumberFor(currentStep),
+ [currentStep, stepsForNumbering],
+ )
+ const totalSteps = stepsForNumbering.length
+
+ const contextValue: OnboardingContextType = {
+ currentStep,
+ setStep,
+ nextStep,
+ previousStep,
+ totalSteps,
+ currentStepIndex,
+ visibleSteps,
+ currentVisibleStepIndex,
+ currentVisibleStepNumber,
+ getStepNumberFor,
+ introTriggers,
+ orbsRevealed,
+ resetIntroTriggers,
+ }
+
+ return (
+ <OnboardingContext.Provider value={contextValue}>
+ {children}
+ </OnboardingContext.Provider>
+ )
}
export function useOnboarding() {
- const context = useContext(OnboardingContext);
+ const context = useContext(OnboardingContext)
- if (context === undefined) {
- throw new Error("useOnboarding must be used within an OnboardingProvider");
- }
+ if (context === undefined) {
+ throw new Error("useOnboarding must be used within an OnboardingProvider")
+ }
- return context;
+ return context
}
diff --git a/apps/web/app/onboarding/onboarding-form.tsx b/apps/web/app/onboarding/onboarding-form.tsx
index 962f8e47..520a2691 100644
--- a/apps/web/app/onboarding/onboarding-form.tsx
+++ b/apps/web/app/onboarding/onboarding-form.tsx
@@ -1,55 +1,66 @@
-"use client";
+"use client"
-import { motion, AnimatePresence } from "motion/react";
-import { NameForm } from "./name-form";
-import { Intro } from "./intro";
-import { useOnboarding } from "./onboarding-context";
-import { BioForm } from "./bio-form";
-import { ConnectionsForm } from "./connections-form";
-import { ExtensionForm } from "./extension-form";
-import { MCPForm } from "./mcp-form";
-import { Welcome } from "./welcome";
+import { motion, AnimatePresence } from "motion/react"
+import { NameForm } from "./name-form"
+import { Intro } from "./intro"
+import { useOnboarding } from "./onboarding-context"
+import { BioForm } from "./bio-form"
+import { ExtensionForm } from "./extension-form"
+import { MCPForm } from "./mcp-form"
+import { Welcome } from "./welcome"
+import { Space_Grotesk } from "next/font/google"
+import { cn } from "@lib/utils"
+
+const sans = Space_Grotesk({
+ subsets: ["latin"],
+ variable: "--font-sans",
+})
export function OnboardingForm() {
- const { currentStep, resetIntroTriggers } = useOnboarding();
+ const { currentStep, resetIntroTriggers } = useOnboarding()
- return (
- <div className="text-6xl px-6 py-8 tracking-wide font-bold font-serif flex flex-col justify-center max-sm:w-full">
- <AnimatePresence mode="wait" onExitComplete={resetIntroTriggers}>
- {currentStep === "intro" && (
- <motion.div
- key="intro"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.98 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.98 }}
- transition={{ duration: 0.28, ease: "easeInOut" }}
- >
- <Intro />
- </motion.div>
- )}
- {currentStep === "name" && (
- <motion.div
- key="name"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.3, ease: "easeOut" }}
- >
- <NameForm />
- </motion.div>
- )}
- {currentStep === "bio" && (
- <motion.div
- key="bio"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.3, ease: "easeOut" }}
- >
- <BioForm />
- </motion.div>
- )}
- {currentStep === "connections" && (
+ return (
+ <div
+ className={cn(
+ "text-4xl px-6 py-8 flex flex-col justify-center max-sm:w-full",
+ sans.variable,
+ )}
+ >
+ <AnimatePresence mode="wait" onExitComplete={resetIntroTriggers}>
+ {currentStep === "intro" && (
+ <motion.div
+ key="intro"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.98 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.98 }}
+ transition={{ duration: 0.28, ease: "easeInOut" }}
+ >
+ <Intro />
+ </motion.div>
+ )}
+ {currentStep === "name" && (
+ <motion.div
+ key="name"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ <NameForm />
+ </motion.div>
+ )}
+ {currentStep === "bio" && (
+ <motion.div
+ key="bio"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ <BioForm />
+ </motion.div>
+ )}
+ {/*{currentStep === "connections" && (
<motion.div
key="connections"
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
@@ -59,41 +70,41 @@ export function OnboardingForm() {
>
<ConnectionsForm />
</motion.div>
- )}
- {currentStep === "mcp" && (
- <motion.div
- key="mcp"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.3, ease: "easeOut" }}
- >
- <MCPForm />
- </motion.div>
- )}
- {currentStep === "extension" && (
- <motion.div
- key="extension"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.3, ease: "easeOut" }}
- >
- <ExtensionForm />
- </motion.div>
- )}
- {currentStep === "welcome" && (
- <motion.div
- key="welcome"
- initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
- exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
- transition={{ duration: 0.3, ease: "easeOut" }}
- >
- <Welcome />
- </motion.div>
- )}
- </AnimatePresence>
- </div>
- );
-} \ No newline at end of file
+ )}*/}
+ {currentStep === "mcp" && (
+ <motion.div
+ key="mcp"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ <MCPForm />
+ </motion.div>
+ )}
+ {currentStep === "extension" && (
+ <motion.div
+ key="extension"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ <ExtensionForm />
+ </motion.div>
+ )}
+ {currentStep === "welcome" && (
+ <motion.div
+ key="welcome"
+ initial={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
+ exit={{ opacity: 0, filter: "blur(10px)", scale: 0.95 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ <Welcome />
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
diff --git a/apps/web/app/onboarding/page.tsx b/apps/web/app/onboarding/page.tsx
index b04f1349..dcf64ad0 100644
--- a/apps/web/app/onboarding/page.tsx
+++ b/apps/web/app/onboarding/page.tsx
@@ -1,28 +1,26 @@
-import { getSession } from "@lib/auth";
-import { OnboardingForm } from "./onboarding-form";
-import { OnboardingProvider } from "./onboarding-context";
-import { FloatingOrbs } from "./floating-orbs";
-import { OnboardingProgressBar } from "./progress-bar";
-import { redirect } from "next/navigation";
-import { type Metadata } from "next";
-
+import { getSession } from "@lib/auth"
+import { OnboardingForm } from "./onboarding-form"
+import { OnboardingProvider } from "./onboarding-context"
+import { OnboardingProgressBar } from "./progress-bar"
+import { redirect } from "next/navigation"
+import { OnboardingBackground } from "./onboarding-background"
+import type { Metadata } from "next"
export const metadata: Metadata = {
- title: "Welcome to Supermemory",
- description: "We're excited to have you on board.",
-};
+ title: "Welcome to Supermemory",
+ description: "We're excited to have you on board.",
+}
export default function OnboardingPage() {
- const session = getSession();
+ const session = getSession()
- if (!session) redirect("/login");
+ if (!session) redirect("/login")
- return (
- <div className="min-h-screen w-full overflow-x-hidden text-zinc-900 bg-white flex items-center justify-center relative">
- <OnboardingProvider>
- <OnboardingProgressBar />
- <FloatingOrbs />
- <OnboardingForm />
- </OnboardingProvider>
- </div>
- );
-} \ No newline at end of file
+ return (
+ <OnboardingProvider>
+ <OnboardingProgressBar />
+ <OnboardingBackground>
+ <OnboardingForm />
+ </OnboardingBackground>
+ </OnboardingProvider>
+ )
+}
diff --git a/apps/web/app/onboarding/progress-bar.tsx b/apps/web/app/onboarding/progress-bar.tsx
index 9bc01b01..82aae3e9 100644
--- a/apps/web/app/onboarding/progress-bar.tsx
+++ b/apps/web/app/onboarding/progress-bar.tsx
@@ -1,26 +1,25 @@
-"use client";
+"use client"
-import { motion } from "motion/react";
-import { useOnboarding } from "./onboarding-context";
+import { motion } from "motion/react"
+import { useOnboarding } from "./onboarding-context"
export function OnboardingProgressBar() {
- const { currentVisibleStepNumber, totalSteps } = useOnboarding();
+ const { currentVisibleStepNumber, totalSteps } = useOnboarding()
- const progress = totalSteps === 0
- ? 0
- : (currentVisibleStepNumber / totalSteps) * 100;
+ const progress =
+ totalSteps === 0 ? 0 : (currentVisibleStepNumber / totalSteps) * 100
- return (
- <div className="fixed top-0 left-0 right-0 z-50 h-1 bg-zinc-200">
- <motion.div
- className="h-full bg-gradient-to-r from-orange-500 via-amber-500 to-gold-600"
- initial={{ width: "0%" }}
- animate={{ width: `${progress}%` }}
- transition={{
- duration: 0.8,
- ease: "easeInOut"
- }}
- />
- </div>
- );
+ return (
+ <div className="fixed top-0 left-0 right-0 z-50 h-3 bg-zinc-200">
+ <motion.div
+ className="h-full bg-gradient-to-r from-[#06245B] via-[#1A5EA7] to-[#DDF5FF]"
+ initial={{ width: "0%" }}
+ animate={{ width: `${progress}%` }}
+ transition={{
+ duration: 0.8,
+ ease: "easeInOut",
+ }}
+ />
+ </div>
+ )
}
diff --git a/apps/web/app/onboarding/welcome.tsx b/apps/web/app/onboarding/welcome.tsx
index d3b19dcd..3f73e43a 100644
--- a/apps/web/app/onboarding/welcome.tsx
+++ b/apps/web/app/onboarding/welcome.tsx
@@ -1,20 +1,33 @@
-"use client";
+"use client"
-import { Button } from "@ui/components/button";
-import { ArrowRightIcon, ChevronRightIcon } from "lucide-react";
+import { ArrowRightIcon } from "lucide-react"
+import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
+import { useRouter } from "next/navigation"
export function Welcome() {
- return (
- <div className="flex flex-col gap-4 items-center text-center">
- <h1>Welcome to Supermemory</h1>
- <p className="text-zinc-600 text-2xl">
- We're excited to have you on board.
- </p>
+ const { markOnboardingCompleted } = useOnboardingStorage()
+ const router = useRouter()
- <a href="/" className="tracking-normal w-fit flex items-center justify-center text-2xl underline cursor-pointer font-medium text-zinc-900">
- Get started
- <ArrowRightIcon className="size-4 ml-2" />
- </a>
- </div>
- );
-} \ No newline at end of file
+ const handleGetStarted = () => {
+ markOnboardingCompleted()
+ router.push("/")
+ }
+
+ return (
+ <div className="flex flex-col gap-4 items-center text-center">
+ <h1 className="text-white">Welcome to Supermemory</h1>
+ <p className="text-white/80 text-2xl">
+ We're excited to have you on board.
+ </p>
+
+ <button
+ type="button"
+ onClick={handleGetStarted}
+ className="tracking-normal w-fit flex items-center justify-center text-2xl underline cursor-pointer font-medium text-white/80 hover:text-white transition-colors"
+ >
+ Get started
+ <ArrowRightIcon className="size-4 ml-2" />
+ </button>
+ </div>
+ )
+}
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 5b7561ea..8886067d 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,6 +1,7 @@
"use client"
import { useIsMobile } from "@hooks/use-mobile"
+import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
import { useAuth } from "@lib/auth-context"
import { $fetch } from "@repo/lib/api"
import { MemoryGraph } from "@repo/ui/memory-graph"
@@ -19,6 +20,7 @@ import {
} from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import Link from "next/link"
+import { useRouter } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
import type { z } from "zod"
import { ConnectAIModal } from "@/components/connect-ai-modal"
@@ -27,11 +29,8 @@ import { MemoryListView } from "@/components/memory-list-view"
import Menu from "@/components/menu"
import { ProjectSelector } from "@/components/project-selector"
import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"
-import type { TourStep } from "@/components/tour"
-import { TourAlertDialog, useTour } from "@/components/tour"
import { AddMemoryView } from "@/components/views/add-memory"
import { ChatRewrite } from "@/components/views/chat"
-import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"
import { useViewMode } from "@/lib/view-mode-context"
import { useChatOpen, useProject } from "@/stores"
import { useGraphHighlights } from "@/stores/highlights"
@@ -42,9 +41,8 @@ type DocumentWithMemories = DocumentsResponse["documents"][0]
const MemoryGraphPage = () => {
const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
const isMobile = useIsMobile()
- const { viewMode, setViewMode, isInitialized } = useViewMode()
+ const { viewMode, setViewMode } = useViewMode()
const { selectedProject } = useProject()
- const { setSteps, isTourCompleted } = useTour()
const { isOpen, setIsOpen } = useChatOpen()
const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
@@ -66,167 +64,6 @@ const MemoryGraphPage = () => {
(p: any) => p.containerTag === selectedProject,
)?.isExperimental
- // Tour state
- const [showTourDialog, setShowTourDialog] = useState(false)
-
- // Define tour steps with useMemo to prevent recreation
- const tourSteps: TourStep[] = useMemo(() => {
- return [
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">
- Memories Overview
- </h3>
- <p className="text-gray-200">
- This is your memory graph. Each node represents a memory, and
- connections show relationships between them.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.MEMORY_GRAPH,
- position: "center",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">
- Add Memories
- </h3>
- <p className="text-gray-200">
- Click here to add new memories to your knowledge base. You can add
- text, links, or connect external sources.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY,
- position: "right",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">
- Connections
- </h3>
- <p className="text-gray-200">
- Connect your external accounts like Google Drive, Notion, or
- OneDrive to automatically sync and organize your content.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS,
- position: "right",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">Projects</h3>
- <p className="text-gray-200">
- Organize your memories into projects. Switch between different
- contexts easily.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.MENU_PROJECTS,
- position: "right",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">
- MCP Servers
- </h3>
- <p className="text-gray-200">
- Access Model Context Protocol servers to give AI tools access to
- your memories securely.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.MENU_MCP,
- position: "right",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">Billing</h3>
- <p className="text-gray-200">
- Manage your subscription and billing information.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.MENU_BILLING,
- position: "right",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">
- View Toggle
- </h3>
- <p className="text-gray-200">
- Switch between graph view and list view to see your memories in
- different ways.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.VIEW_TOGGLE,
- position: "left",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">Legend</h3>
- <p className="text-gray-200">
- Understand the different types of nodes and connections in your
- memory graph.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.LEGEND,
- position: "left",
- },
- {
- content: (
- <div>
- <h3 className="font-semibold text-lg mb-2 text-white">
- Chat Assistant
- </h3>
- <p className="text-gray-200">
- Ask questions or add new memories using our AI-powered chat
- interface.
- </p>
- </div>
- ),
- selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
- position: "left",
- },
- ]
- }, [])
-
- // Check if tour has been completed before
- useEffect(() => {
- const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
- if (!hasCompletedTour && !isTourCompleted) {
- const timer = setTimeout(() => {
- setShowTourDialog(true)
- setShowConnectAIModal(false)
- }, 1000) // Show after 1 second
- return () => clearTimeout(timer)
- }
- }, [isTourCompleted])
-
- // Set up tour steps
- useEffect(() => {
- setSteps(tourSteps)
- }, [setSteps, tourSteps])
-
- // Save tour completion to localStorage
- useEffect(() => {
- if (isTourCompleted) {
- localStorage.setItem(TOUR_STORAGE_KEY, "true")
- }
- }, [isTourCompleted])
-
// Progressive loading via useInfiniteQuery
const IS_DEV = process.env.NODE_ENV === "development"
const PAGE_SIZE = IS_DEV ? 100 : 100
@@ -374,13 +211,10 @@ const MemoryGraphPage = () => {
)
useEffect(() => {
- const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
- if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) {
+ if (allDocuments.length === 0) {
setShowConnectAIModal(true)
- } else if (showTourDialog) {
- setShowConnectAIModal(false)
}
- }, [allDocuments.length, showTourDialog])
+ }, [allDocuments.length])
// Prevent body scrolling
useEffect(() => {
@@ -413,7 +247,6 @@ const MemoryGraphPage = () => {
<motion.div
animate={{ opacity: 1, y: 0 }}
className="absolute md:top-4 md:right-4 md:bottom-auto md:left-auto bottom-8 left-6 z-20 rounded-xl overflow-hidden"
- id={TOUR_STEP_IDS.VIEW_TOGGLE}
initial={{ opacity: 0, y: -20 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
@@ -482,7 +315,6 @@ const MemoryGraphPage = () => {
animate={{ opacity: 1, scale: 1 }}
className="absolute inset-0"
exit={{ opacity: 0, scale: 0.95 }}
- id={TOUR_STEP_IDS.MEMORY_GRAPH}
initial={{ opacity: 0, scale: 0.95 }}
key="graph"
transition={{
@@ -501,7 +333,7 @@ const MemoryGraphPage = () => {
isExperimental={isCurrentProjectExperimental}
isLoading={isPending}
isLoadingMore={isLoadingMore}
- legendId={TOUR_STEP_IDS.LEGEND}
+ legendId={undefined}
loadMoreDocuments={loadMoreDocuments}
occludedRightPx={isOpen && !isMobile ? 600 : 0}
showSpacesSelector={false}
@@ -546,7 +378,6 @@ const MemoryGraphPage = () => {
animate={{ opacity: 1, scale: 1 }}
className="absolute inset-0 md:ml-18"
exit={{ opacity: 0, scale: 0.95 }}
- id={TOUR_STEP_IDS.MEMORY_LIST}
initial={{ opacity: 0, scale: 0.95 }}
key="list"
transition={{
@@ -609,11 +440,8 @@ const MemoryGraphPage = () => {
rel="noopener noreferrer"
target="_blank"
>
- <LogoFull
- className="h-8 hidden md:block"
- id={TOUR_STEP_IDS.LOGO}
- />
- <Logo className="h-8 md:hidden" id={TOUR_STEP_IDS.LOGO} />
+ <LogoFull className="h-8 hidden md:block" />
+ <Logo className="h-8 md:hidden" />
</Link>
<div className="hidden sm:block">
@@ -697,7 +525,6 @@ const MemoryGraphPage = () => {
{/* Chat panel - positioned absolutely */}
<motion.div
className="fixed top-0 right-0 h-full z-50 md:z-auto"
- id={TOUR_STEP_IDS.FLOATING_CHAT}
style={{
width: isOpen ? (isMobile ? "100vw" : "600px") : 0,
pointerEvents: isOpen ? "auto" : "none",
@@ -726,9 +553,6 @@ const MemoryGraphPage = () => {
/>
)}
- {/* Tour Alert Dialog */}
- <TourAlertDialog onOpenChange={setShowTourDialog} open={showTourDialog} />
-
{/* Referral/Upgrade Modal */}
<ReferralUpgradeModal
isOpen={showReferralModal}
@@ -741,6 +565,9 @@ const MemoryGraphPage = () => {
// Wrapper component to handle auth and waitlist checks
export default function Page() {
const { user, session } = useAuth()
+ const { shouldShowOnboarding, isLoading: onboardingLoading } =
+ useOnboardingStorage()
+ const router = useRouter()
useEffect(() => {
const url = new URL(window.location.href)
@@ -765,8 +592,13 @@ export default function Page() {
}
}, [user, session])
- // Show loading state while checking authentication and waitlist status
- if (!user) {
+ useEffect(() => {
+ if (user && !onboardingLoading && shouldShowOnboarding()) {
+ router.push("/onboarding")
+ }
+ }, [user, shouldShowOnboarding, onboardingLoading, router])
+
+ if (!user || onboardingLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
<div className="flex flex-col items-center gap-4">
@@ -777,7 +609,10 @@ export default function Page() {
)
}
- // If we have a user and they have access, show the main component
+ if (shouldShowOnboarding()) {
+ return null
+ }
+
return (
<>
<MemoryGraphPage />
diff --git a/apps/web/biome.json b/apps/web/biome.json
index 48649190..8ab5697e 100644
--- a/apps/web/biome.json
+++ b/apps/web/biome.json
@@ -8,5 +8,12 @@
"recommended": true
}
}
+ },
+ "assist": {
+ "actions": {
+ "source": {
+ "useSortedAttributes": "off"
+ }
+ }
}
} \ No newline at end of file
diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx
index db012ab7..f32b56d1 100644
--- a/apps/web/components/menu.tsx
+++ b/apps/web/components/menu.tsx
@@ -16,10 +16,8 @@ import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { Drawer } from "vaul"
import { useMobilePanel } from "@/lib/mobile-panel-context"
-import { TOUR_STEP_IDS } from "@/lib/tour-constants"
import { useChatOpen } from "@/stores"
import { ProjectSelector } from "./project-selector"
-import { useTour } from "./tour"
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
import { IntegrationsView } from "./views/integrations"
import { ProfileView } from "./views/profile"
@@ -65,7 +63,6 @@ function Menu({ id }: { id?: string }) {
const [showConnectAIModal, setShowConnectAIModal] = useState(false)
const isMobile = useIsMobile()
const { activePanel, setActivePanel } = useMobilePanel()
- const { setMenuExpanded } = useTour()
const autumn = useCustomer()
const { setIsOpen } = useChatOpen()
@@ -99,14 +96,6 @@ function Menu({ id }: { id?: string }) {
const shouldShowLimitWarning =
!isProUser && memoriesUsed >= memoriesLimit * 0.8
- // Map menu item keys to tour IDs
- const menuItemTourIds: Record<string, string> = {
- addUrl: TOUR_STEP_IDS.MENU_ADD_MEMORY,
- projects: TOUR_STEP_IDS.MENU_PROJECTS,
- mcp: TOUR_STEP_IDS.MENU_MCP,
- integrations: "", // No tour ID for integrations yet
- }
-
const menuItems = [
{
icon: Plus,
@@ -221,14 +210,6 @@ function Menu({ id }: { id?: string }) {
}
}, [isMobile, activePanel])
- // Notify tour provider about expansion state changes
- useEffect(() => {
- const isExpanded = isMobile
- ? isMobileMenuOpen || !!expandedView
- : isHovered || !!expandedView
- setMenuExpanded(isExpanded)
- }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded])
-
// Calculate width based on state
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56
@@ -324,7 +305,6 @@ function Menu({ id }: { id?: string }) {
},
}}
className={`flex w-full items-center text-white/80 transition-colors duration-100 hover:text-white cursor-pointer relative ${isHovered || expandedView ? "px-1" : ""}`}
- id={menuItemTourIds[item.key]}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
layout
onClick={() => handleMenuItemClick(item.key)}
@@ -590,7 +570,6 @@ function Menu({ id }: { id?: string }) {
},
}}
className="flex w-full items-center gap-3 px-2 py-2 text-white/90 hover:text-white hover:bg-white/10 rounded-lg cursor-pointer relative"
- id={menuItemTourIds[item.key]}
initial={{ opacity: 0, y: 10 }}
layout
onClick={() => {
diff --git a/apps/web/components/tour.tsx b/apps/web/components/tour.tsx
deleted file mode 100644
index 33919efe..00000000
--- a/apps/web/components/tour.tsx
+++ /dev/null
@@ -1,414 +0,0 @@
-"use client";
-
-import { Button } from "@repo/ui/components/button";
-import { GlassMenuEffect } from "@repo/ui/other/glass-effect";
-import { AnimatePresence, motion } from "motion/react";
-import * as React from "react";
-import { analytics } from "@/lib/analytics";
-
-// Types
-export interface TourStep {
- content: React.ReactNode;
- selectorId: string;
- position?: "top" | "bottom" | "left" | "right" | "center";
- onClickWithinArea?: () => void;
-}
-
-interface TourContextType {
- currentStep: number;
- totalSteps: number;
- nextStep: () => void;
- previousStep: () => void;
- endTour: () => void;
- isActive: boolean;
- isPaused: boolean;
- startTour: () => void;
- setSteps: (steps: TourStep[]) => void;
- steps: TourStep[];
- isTourCompleted: boolean;
- setIsTourCompleted: (completed: boolean) => void;
- // Expansion state tracking
- setMenuExpanded: (expanded: boolean) => void;
- setChatExpanded: (expanded: boolean) => void;
-}
-
-// Context
-const TourContext = React.createContext<TourContextType | undefined>(undefined);
-
-export function useTour() {
- const context = React.useContext(TourContext);
- if (!context) {
- throw new Error("useTour must be used within a TourProvider");
- }
- return context;
-}
-
-// Provider
-interface TourProviderProps {
- children: React.ReactNode;
- onComplete?: () => void;
- className?: string;
- isTourCompleted?: boolean;
-}
-
-export function TourProvider({
- children,
- onComplete,
- className,
- isTourCompleted: initialCompleted = false,
-}: TourProviderProps) {
- const [currentStep, setCurrentStep] = React.useState(-1);
- const [steps, setSteps] = React.useState<TourStep[]>([]);
- const [isActive, setIsActive] = React.useState(false);
- const [isTourCompleted, setIsTourCompleted] =
- React.useState(initialCompleted);
-
- // Track expansion states
- const [isMenuExpanded, setIsMenuExpanded] = React.useState(false);
- const [isChatExpanded, setIsChatExpanded] = React.useState(false);
-
- // Calculate if tour should be paused
- const isPaused = React.useMemo(() => {
- return isActive && (isMenuExpanded || isChatExpanded);
- }, [isActive, isMenuExpanded, isChatExpanded]);
-
- const startTour = React.useCallback(() => {
- console.debug("Starting tour with", steps.length, "steps");
- analytics.tourStarted();
- setCurrentStep(0);
- setIsActive(true);
- }, [steps]);
-
- const endTour = React.useCallback(() => {
- setCurrentStep(-1);
- setIsActive(false);
- setIsTourCompleted(true); // Mark tour as completed when ended/skipped
- analytics.tourSkipped();
- if (onComplete) {
- onComplete();
- }
- }, [onComplete]);
-
- const nextStep = React.useCallback(() => {
- if (currentStep < steps.length - 1) {
- setCurrentStep(currentStep + 1);
- } else {
- analytics.tourCompleted();
- endTour();
- setIsTourCompleted(true);
- }
- }, [currentStep, steps.length, endTour]);
-
- const previousStep = React.useCallback(() => {
- if (currentStep > 0) {
- setCurrentStep(currentStep - 1);
- }
- }, [currentStep]);
-
- const setMenuExpanded = React.useCallback((expanded: boolean) => {
- setIsMenuExpanded(expanded);
- }, []);
-
- const setChatExpanded = React.useCallback((expanded: boolean) => {
- setIsChatExpanded(expanded);
- }, []);
-
- const value = React.useMemo(
- () => ({
- currentStep,
- totalSteps: steps.length,
- nextStep,
- previousStep,
- endTour,
- isActive,
- isPaused,
- startTour,
- setSteps,
- steps,
- isTourCompleted,
- setIsTourCompleted,
- setMenuExpanded,
- setChatExpanded,
- }),
- [
- currentStep,
- steps,
- nextStep,
- previousStep,
- endTour,
- isActive,
- isPaused,
- startTour,
- isTourCompleted,
- setMenuExpanded,
- setChatExpanded,
- ],
- );
-
- return (
- <TourContext.Provider value={value}>
- {children}
- {isActive && !isPaused && (
- <>
- {console.log(
- "Rendering TourHighlight for step:",
- currentStep,
- currentStep >= 0 && currentStep < steps.length
- ? steps[currentStep]
- : "No step",
- )}
- <TourHighlight
- className={className}
- currentStepIndex={currentStep}
- steps={steps}
- />
- </>
- )}
- </TourContext.Provider>
- );
-}
-
-// Tour Highlight Component
-function TourHighlight({
- currentStepIndex,
- steps,
- className,
-}: {
- currentStepIndex: number;
- steps: TourStep[];
- className?: string;
-}) {
- const { nextStep, previousStep, endTour } = useTour();
- const [elementRect, setElementRect] = React.useState<DOMRect | null>(null);
-
- // Get current step safely
- const step =
- currentStepIndex >= 0 && currentStepIndex < steps.length
- ? steps[currentStepIndex]
- : null;
-
- React.useEffect(() => {
- if (!step) return;
-
- // Use requestAnimationFrame to ensure DOM is ready
- const rafId = requestAnimationFrame(() => {
- const element = document.getElementById(step.selectorId);
- console.debug(
- "Looking for element with ID:",
- step.selectorId,
- "Found:",
- !!element,
- );
- if (element) {
- const rect = element.getBoundingClientRect();
- console.debug("Element rect:", {
- id: step.selectorId,
- top: rect.top,
- left: rect.left,
- width: rect.width,
- height: rect.height,
- bottom: rect.bottom,
- right: rect.right,
- });
- setElementRect(rect);
- }
- });
-
- // Add click listener for onClickWithinArea
- let clickHandler: ((e: MouseEvent) => void) | null = null;
- if (step.onClickWithinArea) {
- const element = document.getElementById(step.selectorId);
- if (element) {
- clickHandler = (e: MouseEvent) => {
- if (element.contains(e.target as Node)) {
- step.onClickWithinArea?.();
- }
- };
- document.addEventListener("click", clickHandler);
- }
- }
-
- return () => {
- cancelAnimationFrame(rafId);
- if (clickHandler) {
- document.removeEventListener("click", clickHandler);
- }
- };
- }, [step]);
-
- if (!step) return null;
-
- // Keep the wrapper mounted but animate the content
- return (
- <AnimatePresence mode="wait">
- {elementRect && (
- <motion.div
- animate={{ opacity: 1 }}
- exit={{ opacity: 0 }}
- initial={{ opacity: 0 }}
- key={`tour-step-${currentStepIndex}`}
- transition={{ duration: 0.2 }}
- >
- {/* Highlight Border */}
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- className={`fixed z-[101] pointer-events-none ${className}`}
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
- style={{
- top: elementRect.top + window.scrollY,
- left: elementRect.left + window.scrollX,
- width: elementRect.width,
- height: elementRect.height,
- }}
- >
- <div className="absolute inset-0 rounded-lg outline-4 outline-blue-500/50 outline-offset-0" />
- </motion.div>
-
- {/* Tooltip with Glass Effect */}
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="fixed z-[102] w-72 rounded-lg shadow-xl overflow-hidden"
- exit={{ opacity: 0, y: step.position === "top" ? 10 : -10 }}
- initial={{ opacity: 0, y: step.position === "top" ? 10 : -10 }}
- style={{
- top: (() => {
- const baseTop =
- step.position === "bottom"
- ? elementRect.bottom + 8
- : step.position === "top"
- ? elementRect.top - 200
- : elementRect.top + elementRect.height / 2 - 100;
-
- // Ensure tooltip stays within viewport
- const maxTop = window.innerHeight - 250; // Leave space for tooltip height
- const minTop = 10;
- return Math.max(minTop, Math.min(baseTop, maxTop));
- })(),
- left: (() => {
- const baseLeft =
- step.position === "right"
- ? elementRect.right + 8
- : step.position === "left"
- ? elementRect.left - 300
- : elementRect.left + elementRect.width / 2 - 150;
-
- // Ensure tooltip stays within viewport
- const maxLeft = window.innerWidth - 300; // Tooltip width
- const minLeft = 10;
- return Math.max(minLeft, Math.min(baseLeft, maxLeft));
- })(),
- }}
- >
- {/* Glass effect background */}
- <GlassMenuEffect rounded="rounded-lg" />
-
- {/* Content */}
- <div className="relative z-10 p-4">
- <div className="mb-4 text-white">{step.content}</div>
- <div className="flex items-center justify-between">
- <span className="text-sm text-gray-300">
- {currentStepIndex + 1} / {steps.length}
- </span>
- <div className="flex gap-2">
- <Button
- className="border-white/20 text-white hover:bg-white/10"
- onClick={endTour}
- size="sm"
- variant="outline"
- >
- Skip
- </Button>
- {currentStepIndex > 0 && (
- <Button
- className="border-white/20 text-white hover:bg-white/10"
- onClick={previousStep}
- size="sm"
- variant="outline"
- >
- Previous
- </Button>
- )}
- <Button
- className="bg-white/20 text-white hover:bg-white/30"
- onClick={nextStep}
- size="sm"
- >
- {currentStepIndex === steps.length - 1 ? "Finish" : "Next"}
- </Button>
- </div>
- </div>
- </div>
- </motion.div>
- </motion.div>
- )}
- </AnimatePresence>
- );
-}
-
-// Tour Alert Dialog
-interface TourAlertDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) {
- const { startTour, setIsTourCompleted } = useTour();
-
- const handleStart = () => {
- console.debug("TourAlertDialog: Starting tour");
- onOpenChange(false);
- startTour();
- };
-
- const handleSkip = () => {
- analytics.tourSkipped();
- setIsTourCompleted(true); // Mark tour as completed when skipped
- onOpenChange(false);
- };
-
- if (!open) return null;
-
- return (
- <AnimatePresence>
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[102] w-[90vw] max-w-2xl rounded-lg shadow-xl overflow-hidden"
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
- >
- {/* Glass effect background */}
- <GlassMenuEffect rounded="rounded-lg" />
-
- {/* Content */}
- <div className="relative z-10 p-8 md:p-10 lg:p-12">
- <h2 className="text-3xl md:text-4xl font-bold text-white mb-4">
- Welcome to supermemory™
- </h2>
- <p className="text-lg md:text-xl text-gray-200 mb-8 leading-relaxed">
- This is your personal knowledge graph where all your memories are
- stored and connected. Let's take a quick tour to help you get
- familiar with the interface.
- </p>
- <div className="flex gap-4 justify-end">
- <Button
- className="border-white/20 text-white hover:bg-white/10 px-6 py-2 text-base"
- onClick={handleSkip}
- size="lg"
- variant="outline"
- >
- Skip Tour
- </Button>
- <Button
- className="bg-white/20 text-white hover:bg-white/30 px-6 py-2 text-base"
- onClick={handleStart}
- size="lg"
- >
- Start Tour
- </Button>
- </div>
- </div>
- </motion.div>
- </AnimatePresence>
- );
-}
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
index 28f1d4fe..428ac872 100644
--- a/apps/web/lib/analytics.ts
+++ b/apps/web/lib/analytics.ts
@@ -2,9 +2,6 @@ import posthog from "posthog-js"
export const analytics = {
userSignedOut: () => posthog.capture("user_signed_out"),
- tourStarted: () => posthog.capture("tour_started"),
- tourCompleted: () => posthog.capture("tour_completed"),
- tourSkipped: () => posthog.capture("tour_skipped"),
memoryAdded: (props: {
type: "note" | "link" | "file"
diff --git a/apps/web/lib/tour-constants.ts b/apps/web/lib/tour-constants.ts
deleted file mode 100644
index 9857c878..00000000
--- a/apps/web/lib/tour-constants.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// Tour step IDs - these should match the IDs added to elements in your app
-export const TOUR_STEP_IDS = {
- LOGO: "tour-logo",
- MENU_BUTTON: "tour-menu-button",
- VIEW_TOGGLE: "tour-view-toggle",
- MEMORY_GRAPH: "tour-memory-graph",
- MEMORY_LIST: "tour-memory-list",
- FLOATING_CHAT: "tour-floating-chat",
- ADD_MEMORY: "tour-add-memory",
- SPACES_DROPDOWN: "tour-spaces-dropdown",
- SETTINGS: "tour-settings",
- MENU_CONNECTIONS: "tour-connections",
- // Menu items
- MENU_ADD_MEMORY: "tour-menu-add-memory",
- MENU_PROJECTS: "tour-menu-projects",
- MENU_MCP: "tour-menu-mcp",
- MENU_BILLING: "tour-menu-billing",
- // Legend
- LEGEND: "tour-legend",
-} as const;
-
-// Tour storage key for localStorage
-export const TOUR_STORAGE_KEY = "supermemory-tour-completed";
diff --git a/apps/web/public/onboarding-complete.png b/apps/web/public/onboarding-complete.png
new file mode 100644
index 00000000..329cccda
--- /dev/null
+++ b/apps/web/public/onboarding-complete.png
Binary files differ
diff --git a/apps/web/public/onboarding.png b/apps/web/public/onboarding.png
new file mode 100644
index 00000000..f8ba97b1
--- /dev/null
+++ b/apps/web/public/onboarding.png
Binary files differ
diff --git a/packages/hooks/use-onboarding-storage.ts b/packages/hooks/use-onboarding-storage.ts
new file mode 100644
index 00000000..ce7a155d
--- /dev/null
+++ b/packages/hooks/use-onboarding-storage.ts
@@ -0,0 +1,33 @@
+"use client"
+
+import { useCallback, useEffect, useState } from "react"
+
+const ONBOARDING_STORAGE_KEY = "supermemory_onboarding_completed"
+
+export function useOnboardingStorage() {
+ const [isOnboardingCompleted, setIsOnboardingCompleted] = useState<
+ boolean | null
+ >(null)
+
+ useEffect(() => {
+ const completed = localStorage.getItem(ONBOARDING_STORAGE_KEY)
+ setIsOnboardingCompleted(completed === "true")
+ }, [])
+
+ const markOnboardingCompleted = useCallback(() => {
+ localStorage.setItem(ONBOARDING_STORAGE_KEY, "true")
+ setIsOnboardingCompleted(true)
+ }, [])
+
+ const shouldShowOnboarding = useCallback(() => {
+ if (isOnboardingCompleted === null) return null // Still loading
+ return !isOnboardingCompleted
+ }, [isOnboardingCompleted])
+
+ return {
+ isOnboardingCompleted,
+ markOnboardingCompleted,
+ shouldShowOnboarding,
+ isLoading: isOnboardingCompleted === null,
+ }
+}