aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2026-01-16 21:44:26 +0000
committerMaheshtheDev <[email protected]>2026-01-16 21:44:26 +0000
commit9fd49193ac6dcd8c6cdf75da642bfb4a11c76e9a (patch)
tree993317bf9c73035a93856f7284ce611c019d93ac
parentfeat: deep-research on user profile and tiptap integration (#672) (diff)
downloadsupermemory-9fd49193ac6dcd8c6cdf75da642bfb4a11c76e9a.tar.xz
supermemory-9fd49193ac6dcd8c6cdf75da642bfb4a11c76e9a.zip
chore: delete document, document ui (#673)01-16-chore_delete_document_document_ui
-rw-r--r--.github/workflows/claude-code-review.yml5
-rw-r--r--apps/web/app/layout.tsx8
-rw-r--r--apps/web/app/new/onboarding/layout.tsx106
-rw-r--r--apps/web/app/new/onboarding/page.tsx263
-rw-r--r--apps/web/app/new/onboarding/setup/layout.tsx70
-rw-r--r--apps/web/app/new/onboarding/setup/page.tsx68
-rw-r--r--apps/web/app/new/onboarding/welcome/layout.tsx143
-rw-r--r--apps/web/app/new/onboarding/welcome/page.tsx185
-rw-r--r--apps/web/components/new/document-modal/index.tsx125
-rw-r--r--apps/web/components/new/document-modal/title.tsx6
-rw-r--r--apps/web/components/new/onboarding/setup/chat-sidebar.tsx549
-rw-r--r--apps/web/components/new/onboarding/setup/integrations-step.tsx2
-rw-r--r--apps/web/components/new/onboarding/setup/relatable-question.tsx2
-rw-r--r--apps/web/components/new/onboarding/welcome/continue-step.tsx200
-rw-r--r--apps/web/components/new/onboarding/welcome/features-step.tsx98
-rw-r--r--apps/web/components/new/onboarding/welcome/profile-step.tsx2
-rw-r--r--apps/web/components/views/chat/chat-messages.tsx3
-rw-r--r--apps/web/hooks/use-document-mutations.ts114
-rw-r--r--apps/web/lib/variants.ts95
19 files changed, 1505 insertions, 539 deletions
diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
index 8564713b..7f63c2df 100644
--- a/.github/workflows/claude-code-review.yml
+++ b/.github/workflows/claude-code-review.yml
@@ -26,6 +26,9 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
use_sticky_comment: true
prompt: |
+ REPO: ${{ github.repository }}
+ PR NUMBER: ${{ github.event.pull_request.number }}
+
You are reviewing a PR for supermemory - a Turbo monorepo with multiple apps and packages.
## Repository Structure Context
@@ -75,4 +78,4 @@ jobs:
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
- claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
+ claude_args: '--allowedTools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 030b5b90..37e01cd7 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -33,6 +33,14 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
+ <head>
+ {process.env.NODE_ENV === "development" && (
+ <script
+ crossOrigin="anonymous"
+ src="https://unpkg.com/react-scan/dist/auto.global.js"
+ />
+ )}
+ </head>
<body
className={`${font.variable} antialiased overflow-x-hidden`}
suppressHydrationWarning
diff --git a/apps/web/app/new/onboarding/layout.tsx b/apps/web/app/new/onboarding/layout.tsx
new file mode 100644
index 00000000..0b077f1f
--- /dev/null
+++ b/apps/web/app/new/onboarding/layout.tsx
@@ -0,0 +1,106 @@
+"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?.name) {
+ setNameState(user.name)
+ localStorage.setItem("onboarding_name", user.name)
+ }
+
+ if (storedMemoryFormData) {
+ try {
+ setMemoryFormDataState(JSON.parse(storedMemoryFormData))
+ } catch {
+ // ignore parse errors
+ }
+ }
+ }, [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>
+ )
+}
diff --git a/apps/web/app/new/onboarding/page.tsx b/apps/web/app/new/onboarding/page.tsx
index 1b4962e4..19e9c62a 100644
--- a/apps/web/app/new/onboarding/page.tsx
+++ b/apps/web/app/new/onboarding/page.tsx
@@ -1,267 +1,18 @@
"use client"
-import { useSearchParams } from "next/navigation"
-import { motion, AnimatePresence } from "motion/react"
-import { useState, useEffect } from "react"
-import { useAuth } from "@lib/auth-context"
-import { cn } from "@lib/utils"
-
-import { InputStep } from "../../../components/new/onboarding/welcome/input-step"
-import { GreetingStep } from "../../../components/new/onboarding/welcome/greeting-step"
-import { WelcomeStep } from "../../../components/new/onboarding/welcome/welcome-step"
-import { ContinueStep } from "../../../components/new/onboarding/welcome/continue-step"
-import { FeaturesStep } from "../../../components/new/onboarding/welcome/features-step"
-import { ProfileStep } from "../../../components/new/onboarding/welcome/profile-step"
-import { RelatableQuestion } from "../../../components/new/onboarding/setup/relatable-question"
-import { IntegrationsStep } from "../../../components/new/onboarding/setup/integrations-step"
-
-import { InitialHeader } from "@/components/initial-header"
-import { SetupHeader } from "../../../components/new/onboarding/setup/header"
-import { ChatSidebar } from "../../../components/new/onboarding/setup/chat-sidebar"
-import { Logo } from "@ui/assets/Logo"
-import NovaOrb from "@/components/nova/nova-orb"
-import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
-
-function UserSupermemory({ name }: { name: string }) {
- return (
- <motion.div
- className="absolute inset-0 top-[-34px] flex items-center justify-center z-10"
- initial={{ opacity: 0, y: 0 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: 0 }}
- transition={{ duration: 1, ease: "easeOut" }}
- >
- <Logo className="h-14 text-white" />
- <div className="flex flex-col items-start justify-center ml-4">
- <p className="text-white text-[25px] font-medium leading-none">
- {name.split(" ")[0]}'s
- </p>
- <p className="text-white font-bold text-4xl leading-none -mt-2">
- supermemory
- </p>
- </div>
- </motion.div>
- )
-}
+import { useEffect } from "react"
+import { useRouter } from "next/navigation"
export default function OnboardingPage() {
- const searchParams = useSearchParams()
- const { user } = useAuth()
-
- const flow = searchParams.get("flow") as "welcome" | "setup" | null
- const step = searchParams.get("step") as string | null
-
- const [name, setName] = useState(user?.name ?? "")
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [memoryFormData, setMemoryFormData] = useState<{
- twitter: string
- linkedin: string
- description: string
- otherLinks: string[]
- } | null>(null)
- const [showWelcomeContent, setShowWelcomeContent] = useState(false)
-
- const currentFlow = flow || "welcome"
- const currentStep = step || "input"
-
- useEffect(() => {
- if (user?.name) {
- setName(user.name)
- localStorage.setItem("username", user.name)
- }
- }, [user?.name])
+ const router = useRouter()
useEffect(() => {
- if (currentFlow === "welcome" && currentStep === "input") {
- setShowWelcomeContent(false)
- const timer = setTimeout(() => {
- setShowWelcomeContent(true)
- }, 1250)
- return () => clearTimeout(timer)
- }
- }, [currentFlow, currentStep])
-
- useEffect(() => {
- if (currentFlow !== "welcome") return
-
- const timers: NodeJS.Timeout[] = []
-
- switch (currentStep) {
- case "greeting":
- timers.push(
- setTimeout(() => {
- // Auto-advance to welcome step
- window.history.replaceState(
- null,
- "",
- "/new/onboarding?flow=welcome&step=welcome",
- )
- }, 2000),
- )
- break
- case "welcome":
- timers.push(
- setTimeout(() => {
- // Auto-advance to username step
- window.history.replaceState(
- null,
- "",
- "/new/onboarding?flow=welcome&step=username",
- )
- }, 2000),
- )
- break
- }
-
- return () => {
- timers.forEach(clearTimeout)
- }
- }, [currentStep, currentFlow])
-
- const handleSubmit = () => {
- localStorage.setItem("username", name)
- if (name.trim()) {
- setIsSubmitting(true)
- window.history.replaceState(
- null,
- "",
- "/new/onboarding?flow=welcome&step=greeting",
- )
- setIsSubmitting(false)
- }
- }
-
- const renderWelcomeStep = () => {
- switch (currentStep) {
- case "input":
- return (
- <InputStep
- key="input"
- name={name}
- setName={setName}
- handleSubmit={handleSubmit}
- isSubmitting={isSubmitting}
- />
- )
- case "greeting":
- return <GreetingStep key="greeting" name={name} />
- case "welcome":
- return <WelcomeStep key="welcome" />
- case "username":
- return <ContinueStep key="username" />
- case "features":
- return <FeaturesStep key="features" />
- case "memories":
- return <ProfileStep key="profile" onSubmit={setMemoryFormData} />
- default:
- return null
- }
- }
-
- const renderSetupStep = () => {
- switch (currentStep) {
- case "relatable":
- return <RelatableQuestion key="relatable" />
- case "integrations":
- return <IntegrationsStep key="integrations" />
- default:
- return null
- }
- }
-
- const isWelcomeFlow = currentFlow === "welcome"
- const isSetupFlow = currentFlow === "setup"
-
- const minimizeNovaOrb =
- isWelcomeFlow && ["features", "memories"].includes(currentStep)
- const novaSize = currentStep === "memories" ? 150 : 300
-
- const showUserSupermemory = isWelcomeFlow && currentStep === "username"
+ router.replace("/new/onboarding/welcome?step=input")
+ }, [router])
return (
- <div className="h-screen overflow-hidden bg-black">
- {isWelcomeFlow && (
- <InitialHeader
- showUserSupermemory={
- currentStep === "features" || currentStep === "memories"
- }
- name={name}
- />
- )}
- {isSetupFlow && <SetupHeader />}
-
- {isSetupFlow && <AnimatedGradientBackground animateFromBottom={false} />}
-
- {isWelcomeFlow && currentStep === "input" && (
- <AnimatedGradientBackground animateFromBottom={true} />
- )}
-
- {isWelcomeFlow && showWelcomeContent && (
- <div className="fixed inset-0 flex flex-col items-center justify-center overflow-y-auto">
- <motion.div
- className="absolute inset-0 bg-[url('/bg-rectangle.png')] bg-cover bg-center bg-no-repeat pointer-events-none"
- transition={{ duration: 0.75, ease: "easeOut", bounce: 0 }}
- style={{
- mixBlendMode: "soft-light",
- opacity: 0.6,
- }}
- />
- <motion.div
- className={cn(
- "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex flex-col items-center justify-center",
- )}
- animate={{
- gap: minimizeNovaOrb ? 0 : 16,
- }}
- transition={{
- duration: 0.6,
- ease: "easeOut",
- }}
- >
- <motion.div
- animate={{
- scale:
- currentStep === "features"
- ? 0.7
- : currentStep === "memories"
- ? 0.4
- : 1,
- padding: minimizeNovaOrb ? 0 : 48,
- paddingTop: 0,
- }}
- transition={{
- duration: 0.8,
- ease: "easeOut",
- delay: 0.2,
- }}
- className="relative"
- >
- <NovaOrb size={novaSize} />
- {showUserSupermemory && <UserSupermemory name={name} />}
- </motion.div>
-
- <AnimatePresence mode="wait">{renderWelcomeStep()}</AnimatePresence>
- </motion.div>
- </div>
- )}
-
- {isSetupFlow && (
- <main className="relative min-h-screen">
- <div className="relative z-10">
- <div className="flex flex-row h-[calc(100vh-90px)] relative">
- <div className="flex-1 flex flex-col items-center justify-start p-8">
- <AnimatePresence mode="wait">
- {renderSetupStep()}
- </AnimatePresence>
- </div>
-
- <AnimatePresence mode="popLayout">
- <ChatSidebar formData={memoryFormData} />
- </AnimatePresence>
- </div>
- </div>
- </main>
- )}
+ <div className="h-screen overflow-hidden bg-black flex items-center justify-center">
+ <div className="text-white/50 text-sm">Loading...</div>
</div>
)
}
diff --git a/apps/web/app/new/onboarding/setup/layout.tsx b/apps/web/app/new/onboarding/setup/layout.tsx
new file mode 100644
index 00000000..3a958469
--- /dev/null
+++ b/apps/web/app/new/onboarding/setup/layout.tsx
@@ -0,0 +1,70 @@
+"use client"
+
+import { createContext, useContext, useCallback, type ReactNode } from "react"
+import { useRouter, useSearchParams } from "next/navigation"
+import { useOnboardingContext, type MemoryFormData } from "../layout"
+
+export const SETUP_STEPS = ["relatable", "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)
+ : "relatable"
+
+ const goToStep = useCallback(
+ (step: SetupStep) => {
+ router.push(`/new/onboarding/setup?step=${step}`)
+ },
+ [router],
+ )
+
+ const goToWelcome = useCallback(
+ (step = "input") => {
+ router.push(`/new/onboarding/welcome?step=${step}`)
+ },
+ [router],
+ )
+
+ const finishOnboarding = useCallback(() => {
+ resetOnboarding()
+ router.push("/new")
+ }, [router, resetOnboarding])
+
+ const contextValue: SetupContextValue = {
+ memoryFormData,
+ currentStep,
+ goToStep,
+ goToWelcome,
+ finishOnboarding,
+ }
+
+ return (
+ <SetupContext.Provider value={contextValue}>
+ {children}
+ </SetupContext.Provider>
+ )
+}
diff --git a/apps/web/app/new/onboarding/setup/page.tsx b/apps/web/app/new/onboarding/setup/page.tsx
new file mode 100644
index 00000000..d601970d
--- /dev/null
+++ b/apps/web/app/new/onboarding/setup/page.tsx
@@ -0,0 +1,68 @@
+"use client"
+
+import { motion, AnimatePresence } from "motion/react"
+
+import { RelatableQuestion } from "@/components/new/onboarding/setup/relatable-question"
+import { IntegrationsStep } from "@/components/new/onboarding/setup/integrations-step"
+
+import { SetupHeader } from "@/components/new/onboarding/setup/header"
+import { ChatSidebar } from "@/components/new/onboarding/setup/chat-sidebar"
+import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
+
+import { useSetupContext, type SetupStep } from "./layout"
+
+function StepNotFound({ goToStep }: { goToStep: (step: SetupStep) => 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("relatable")}
+ className="text-blue-400 underline"
+ >
+ Go to first step
+ </button>
+ </motion.div>
+ )
+}
+
+export default function SetupPage() {
+ const { memoryFormData, currentStep, goToStep } = useSetupContext()
+
+ const renderStep = () => {
+ switch (currentStep) {
+ case "relatable":
+ return <RelatableQuestion key="relatable" />
+ case "integrations":
+ return <IntegrationsStep key="integrations" />
+ default:
+ return <StepNotFound key="not-found" goToStep={goToStep} />
+ }
+ }
+
+ return (
+ <div className="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-row h-[calc(100vh-90px)] relative">
+ <div className="flex-1 flex flex-col items-center justify-start p-8">
+ <AnimatePresence mode="wait">{renderStep()}</AnimatePresence>
+ </div>
+
+ <AnimatePresence mode="popLayout">
+ <ChatSidebar formData={memoryFormData} />
+ </AnimatePresence>
+ </div>
+ </div>
+ </main>
+ </div>
+ )
+}
diff --git a/apps/web/app/new/onboarding/welcome/layout.tsx b/apps/web/app/new/onboarding/welcome/layout.tsx
new file mode 100644
index 00000000..427907c2
--- /dev/null
+++ b/apps/web/app/new/onboarding/welcome/layout.tsx
@@ -0,0 +1,143 @@
+"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"
+
+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 stepParam = searchParams.get("step")
+ const currentStep: WelcomeStep = WELCOME_STEPS.includes(
+ stepParam as WelcomeStep,
+ )
+ ? (stepParam as WelcomeStep)
+ : "input"
+
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [showWelcomeContent, setShowWelcomeContent] = useState(false)
+ const isMountedRef = useRef(true)
+
+ useEffect(() => {
+ isMountedRef.current = true
+ return () => {
+ isMountedRef.current = false
+ }
+ }, [])
+
+ useEffect(() => {
+ if (currentStep === "input") {
+ setShowWelcomeContent(false)
+ const timer = setTimeout(() => {
+ if (isMountedRef.current) {
+ setShowWelcomeContent(true)
+ }
+ }, 1000)
+ return () => clearTimeout(timer)
+ }
+ setShowWelcomeContent(true)
+ }, [currentStep])
+
+ useEffect(() => {
+ const timers: NodeJS.Timeout[] = []
+
+ if (currentStep === "greeting") {
+ timers.push(
+ setTimeout(() => {
+ if (isMountedRef.current) {
+ router.replace("/new/onboarding/welcome?step=welcome")
+ }
+ }, 2000),
+ )
+ } else if (currentStep === "welcome") {
+ timers.push(
+ setTimeout(() => {
+ if (isMountedRef.current) {
+ router.replace("/new/onboarding/welcome?step=username")
+ }
+ }, 2000),
+ )
+ }
+
+ return () => {
+ timers.forEach(clearTimeout)
+ }
+ }, [currentStep, router])
+
+ const goToStep = useCallback(
+ (step: WelcomeStep) => {
+ router.push(`/new/onboarding/welcome?step=${step}`)
+ },
+ [router],
+ )
+
+ const goToSetup = useCallback(
+ (step = "relatable") => {
+ router.push(`/new/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>
+ )
+}
diff --git a/apps/web/app/new/onboarding/welcome/page.tsx b/apps/web/app/new/onboarding/welcome/page.tsx
new file mode 100644
index 00000000..40f0134b
--- /dev/null
+++ b/apps/web/app/new/onboarding/welcome/page.tsx
@@ -0,0 +1,185 @@
+"use client"
+
+import { motion, AnimatePresence } from "motion/react"
+import { cn } from "@lib/utils"
+
+import { InputStep } from "@/components/new/onboarding/welcome/input-step"
+import { GreetingStep } from "@/components/new/onboarding/welcome/greeting-step"
+import { WelcomeStep } from "@/components/new/onboarding/welcome/welcome-step"
+import { OnboardingContentStep } from "@/components/new/onboarding/welcome/continue-step"
+
+import { InitialHeader } from "@/components/initial-header"
+import { Logo } from "@ui/assets/Logo"
+import NovaOrb from "@/components/nova/nova-orb"
+import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
+
+import {
+ useWelcomeContext,
+ type WelcomeStep as WelcomeStepType,
+} from "./layout"
+import { gapVariants, orbVariants } from "@/lib/variants"
+
+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 handleSubmit = () => {
+ localStorage.setItem("username", name)
+ if (name.trim()) {
+ setIsSubmitting(true)
+ goToStep("greeting")
+ 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="h-screen overflow-hidden bg-black">
+ <InitialHeader
+ showUserSupermemory={
+ currentStep === "features" || currentStep === "memories"
+ }
+ name={name}
+ />
+
+ {currentStep === "input" && (
+ <AnimatedGradientBackground animateFromBottom={true} />
+ )}
+
+ {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,
+ y: 60,
+ }}
+ className="relative"
+ >
+ <NovaOrb size={novaSize} />
+ {showUserSupermemory && <UserSupermemory name={name} />}
+ </motion.div>
+
+ <AnimatePresence mode="wait">{renderStep()}</AnimatePresence>
+ </motion.div>
+ </div>
+ )}
+ </div>
+ )
+}
diff --git a/apps/web/components/new/document-modal/index.tsx b/apps/web/components/new/document-modal/index.tsx
index bd8ec05d..8d4aac87 100644
--- a/apps/web/components/new/document-modal/index.tsx
+++ b/apps/web/components/new/document-modal/index.tsx
@@ -2,7 +2,7 @@
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
-import { ArrowUpRightIcon, XIcon, Loader2 } from "lucide-react"
+import { ArrowUpRightIcon, XIcon, Loader2, Trash2Icon, CheckIcon } from "lucide-react"
import type { z } from "zod"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@lib/utils"
@@ -20,6 +20,8 @@ import { useState, useEffect, useCallback, useMemo } from "react"
import { motion, AnimatePresence } from "motion/react"
import { Button } from "@repo/ui/components/button"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
+import type { UseMutationResult } from "@tanstack/react-query"
+import { toast } from "sonner"
// Dynamically importing to prevent DOMMatrix error
const PdfViewer = dynamic(
@@ -43,12 +45,102 @@ interface DocumentModalProps {
onClose: () => void
}
+interface DeleteButtonProps {
+ documentId: string | null | undefined
+ customId: string | null | undefined
+ deleteMutation: UseMutationResult<
+ unknown,
+ Error,
+ { documentId: string },
+ unknown
+ >
+}
+
+function isTemporaryId(id: string | null | undefined): boolean {
+ if (!id) return false
+ return id.startsWith("temp-") || id.startsWith("temp-file-")
+}
+
+function DeleteButton({ documentId, customId, deleteMutation }: DeleteButtonProps) {
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
+
+ const handleDelete = useCallback(() => {
+ const id = documentId ?? customId
+ if (!id) return
+
+ // Check both IDs to ensure we catch temporary documents regardless of which ID is used
+ if (isTemporaryId(documentId) || isTemporaryId(customId)) {
+ // this is when user added document immediately and trying to delete
+ toast.error("Cannot delete document", {
+ description: "This document is still being processed. Please wait.",
+ })
+ return
+ }
+
+ deleteMutation.mutate({ documentId: id as string })
+ }, [documentId, customId, deleteMutation])
+
+ return (
+ <AnimatePresence mode="wait">
+ {!deleteConfirmOpen ? (
+ <motion.button
+ key="trash"
+ type="button"
+ initial={{ opacity: 0, scale: 0.8 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.8 }}
+ transition={{ duration: 0.15 }}
+ onClick={() => setDeleteConfirmOpen(true)}
+ tabIndex={-1}
+ className="bg-[#0D121A] w-7 h-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none cursor-pointer shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ disabled={deleteMutation.isPending}
+ >
+ <Trash2Icon className="w-4 h-4 text-red-500" />
+ <span className="sr-only">Delete document</span>
+ </motion.button>
+ ) : (
+ <motion.div
+ key="confirm"
+ initial={{ opacity: 0, scale: 0.8 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.8 }}
+ transition={{ duration: 0.15 }}
+ className="flex items-center gap-1 px-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ >
+ <button
+ type="button"
+ onClick={() => setDeleteConfirmOpen(false)}
+ disabled={deleteMutation.isPending}
+ className="w-6 h-6 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ <XIcon className="w-4 h-4 text-[#737373]" />
+ <span className="sr-only">Cancel delete</span>
+ </button>
+ <button
+ type="button"
+ onClick={handleDelete}
+ disabled={deleteMutation.isPending}
+ className="w-6 h-6 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {deleteMutation.isPending ? (
+ <Loader2 className="w-4 h-4 text-green-500 animate-spin" />
+ ) : (
+ <CheckIcon className="w-4 h-4 text-green-500" />
+ )}
+ <span className="sr-only">Confirm delete</span>
+ </button>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ )
+}
+
export function DocumentModal({
document: _document,
isOpen,
onClose,
}: DocumentModalProps) {
- const { updateMutation } = useDocumentMutations()
+ const { updateMutation, deleteMutation } = useDocumentMutations({ onClose })
const { initialEditorContent, initialEditorString } = useMemo(() => {
const content = _document?.content as string | null | undefined
@@ -76,7 +168,9 @@ export function DocumentModal({
}, [initialEditorString])
useEffect(() => {
- if (!isOpen) resetEditor()
+ if (!isOpen) {
+ resetEditor()
+ }
}, [isOpen, resetEditor])
const hasUnsavedChanges =
@@ -107,13 +201,20 @@ export function DocumentModal({
<DialogTitle className="sr-only">
{_document?.title} - Document
</DialogTitle>
- <div className="flex justify-between h-fit">
- <Title
- title={_document?.title}
- documentType={_document?.type ?? "text"}
- url={_document?.url}
- />
- <div className="flex items-center gap-2">
+ <div className="flex items-center justify-between h-fit gap-4">
+ <div className="flex-1 min-w-0">
+ <Title
+ title={_document?.title}
+ documentType={_document?.type ?? "text"}
+ url={_document?.url}
+ />
+ </div>
+ <div className="flex items-center gap-2 shrink-0">
+ <DeleteButton
+ documentId={_document?.id}
+ customId={_document?.customId}
+ deleteMutation={deleteMutation}
+ />
{_document?.url && (
<a
href={_document.url}
@@ -126,8 +227,10 @@ export function DocumentModal({
</a>
)}
<DialogPrimitive.Close
- className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ className="bg-[#0D121A] w-7 h-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus:outline-none disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
data-slot="dialog-close"
+ type="button"
+ tabIndex={-1}
>
<XIcon stroke="#737373" />
<span className="sr-only">Close</span>
diff --git a/apps/web/components/new/document-modal/title.tsx b/apps/web/components/new/document-modal/title.tsx
index 066c437b..59eb285e 100644
--- a/apps/web/components/new/document-modal/title.tsx
+++ b/apps/web/components/new/document-modal/title.tsx
@@ -30,10 +30,10 @@ export function Title({
<div
className={cn(
dmSansClassName(),
- "text-[16px] font-semibold text-[#FAFAFA] line-clamp-1 leading-[125%] flex items-center gap-3",
+ "text-[16px] font-semibold text-[#FAFAFA] leading-[125%] flex items-center gap-3 min-w-0",
)}
>
- <div className="pl-1 flex items-center gap-1 w-5 h-5">
+ <div className="pl-1 flex items-center gap-1 w-5 h-5 shrink-0">
{getDocumentIcon(
documentType as DocumentType,
"w-5 h-5",
@@ -49,7 +49,7 @@ export function Title({
</p>
)}
</div>
- {title || "Untitled Document"}
+ <span className="truncate">{title || "Untitled Document"}</span>
</div>
)
}
diff --git a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
index 59742271..dd6eeb23 100644
--- a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
+++ b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
@@ -2,14 +2,18 @@
import { useState, useEffect, useCallback, useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
+import { useChat } from "@ai-sdk/react"
+import { DefaultChatTransport } from "ai"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
-import { PanelRightCloseIcon, SendIcon } from "lucide-react"
+import { PanelRightCloseIcon, SendIcon, CheckIcon, XIcon } 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"
interface ChatSidebarProps {
formData: {
@@ -20,11 +24,20 @@ interface ChatSidebarProps {
} | 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 [message, setMessage] = useState("")
const [isChatOpen, setIsChatOpen] = useState(true)
- const [messages, setMessages] = useState<
+ const [timelineMessages, setTimelineMessages] = useState<
{
message: string
type?: "formData" | "exa" | "memory" | "waiting"
@@ -40,10 +53,82 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
}[]
>([])
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 displayedMemoriesRef = useRef<Set<string>>(new Set())
+ const contextInjectedRef = useRef(false)
+ const draftsBuiltRef = useRef(false)
+ const isProcessingRef = useRef(false)
+
+ const {
+ messages: chatMessages,
+ sendMessage,
+ status,
+ } = useChat({
+ transport: new DefaultChatTransport({
+ api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
+ credentials: "include",
+ body: {
+ metadata: {
+ projectId: selectedProject,
+ model: "gemini-2.5-pro",
+ },
+ },
+ }),
+ })
+
+ 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 = () => {
- console.log("Message:", message)
+ 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("")
}
@@ -97,8 +182,8 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
},
)
- if (newMemories.length > 0 && messages.length < 10) {
- setMessages((prev) => [
+ if (newMemories.length > 0 && timelineMessages.length < 10) {
+ setTimelineMessages((prev) => [
...prev,
{
message: newMemories
@@ -123,13 +208,151 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
}
}
},
- [messages.length],
+ [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
+
+ setIsFetchingDrafts(true)
+ const drafts: DraftDoc[] = []
+
+ try {
+ if (formData.description?.trim()) {
+ drafts.push({
+ kind: "likes",
+ content: formData.description,
+ metadata: {
+ sm_source: "consumer",
+ description_source: "user_input",
+ },
+ title: "Your Interests",
+ })
+ }
+
+ const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
+
+ const [exaResults, xResearchResult] = await Promise.all([
+ urls.length > 0
+ ? fetch("/api/onboarding/extract-content", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ urls }),
+ })
+ .then((r) => r.json())
+ .then((data) => data.results || [])
+ .catch(() => [])
+ : Promise.resolve([]),
+ formData.twitter
+ ? fetch("/api/onboarding/research", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ xUrl: formData.twitter,
+ name: user?.name,
+ email: user?.email,
+ }),
+ })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => data?.text?.trim() || null)
+ .catch(() => null)
+ : Promise.resolve(null),
+ ])
+
+ for (const result of exaResults) {
+ if (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 {
+ setIsFetchingDrafts(false)
+ }
+ }, [formData, user])
+
+ const handleConfirmDocs = useCallback(async () => {
+ if (isConfirmed || isProcessingRef.current) return
+ isProcessingRef.current = true
+ setIsConfirmed(true)
+ setIsLoading(true)
+
+ try {
+ const documentIds: string[] = []
+
+ for (const draft of draftDocs) {
+ if (draft.kind === "x_research" && xResearchStatus !== "correct") {
+ continue
+ }
+
+ try {
+ const docResponse = await $fetch("@post/documents", {
+ body: {
+ content: draft.content,
+ containerTags: ["sm_project_default"],
+ metadata: draft.metadata,
+ },
+ })
+
+ if (docResponse.data?.id) {
+ documentIds.push(docResponse.data.id)
+ }
+ } catch (error) {
+ console.warn("Error creating document:", error)
+ }
+ }
+
+ 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 messages = []
+ const formDataMessages: typeof timelineMessages = []
if (formData.twitter) {
formDataMessages.push({
@@ -172,125 +395,9 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
})
}
- setMessages(formDataMessages)
-
- const hasContent =
- formData.twitter ||
- formData.linkedin ||
- formData.otherLinks.length > 0 ||
- formData.description?.trim()
-
- if (!hasContent) return
-
- const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
-
- const processContent = async () => {
- setIsLoading(true)
-
- try {
- const documentIds: string[] = []
-
- if (formData.description?.trim()) {
- try {
- const descDocResponse = await $fetch("@post/documents", {
- body: {
- content: formData.description,
- containerTags: ["sm_project_default"],
- metadata: {
- sm_source: "consumer",
- description_source: "user_input",
- },
- },
- })
-
- if (descDocResponse.data?.id) {
- documentIds.push(descDocResponse.data.id)
- }
- } catch (error) {
- console.warn("Error creating description document:", error)
- }
- }
-
- if (formData.twitter) {
- try {
- const researchResponse = 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 (researchResponse.ok) {
- const { text } = await researchResponse.json()
-
- if (text?.trim()) {
- const xDocResponse = await $fetch("@post/documents", {
- body: {
- content: text,
- containerTags: ["sm_project_default"],
- metadata: {
- sm_source: "consumer",
- onboarding_source: "x_research",
- x_url: formData.twitter,
- },
- },
- })
-
- if (xDocResponse.data?.id) {
- documentIds.push(xDocResponse.data.id)
- }
- }
- }
- } catch (error) {
- console.warn("Error fetching X research:", error)
- }
- }
-
- if (urls.length > 0) {
- const response = await fetch("/api/onboarding/extract-content", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ urls }),
- })
- const { results } = await response.json()
-
- for (const result of results) {
- try {
- const docResponse = await $fetch("@post/documents", {
- body: {
- content: result.text || result.description || "",
- containerTags: ["sm_project_default"],
- metadata: {
- sm_source: "consumer",
- exa_url: result.url,
- exa_title: result.title,
- },
- },
- })
-
- if (docResponse.data?.id) {
- documentIds.push(docResponse.data.id)
- }
- } catch (error) {
- console.warn("Error creating document:", error)
- }
- }
- }
-
- if (documentIds.length > 0) {
- await pollForMemories(documentIds)
- }
- } catch (error) {
- console.warn("Error processing content:", error)
- }
- setIsLoading(false)
- }
-
- processContent()
- }, [formData, pollForMemories, user])
+ setTimelineMessages(formDataMessages)
+ buildDraftDocs()
+ }, [formData, buildDraftDocs])
return (
<AnimatePresence mode="wait">
@@ -337,8 +444,8 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
<PanelRightCloseIcon className="size-4" />
Close chat
</motion.button>
- <div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end">
- {messages.map((msg, i) => (
+ <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"
@@ -438,12 +545,78 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
)}
</div>
))}
- {messages.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>
- )}
+ {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!" />
@@ -452,7 +625,106 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
)}
</div>
- <div className="p-4">
+ {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) => {
@@ -468,11 +740,16 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
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()}
+ 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"
>
diff --git a/apps/web/components/new/onboarding/setup/integrations-step.tsx b/apps/web/components/new/onboarding/setup/integrations-step.tsx
index 49e3062a..8f83bdfe 100644
--- a/apps/web/components/new/onboarding/setup/integrations-step.tsx
+++ b/apps/web/components/new/onboarding/setup/integrations-step.tsx
@@ -165,7 +165,7 @@ export function IntegrationsStep() {
variant="link"
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
onClick={() =>
- router.push("/new/onboarding?flow=setup&step=relatable")
+ router.push("/new/onboarding/setup?step=relatable")
}
>
← Back
diff --git a/apps/web/components/new/onboarding/setup/relatable-question.tsx b/apps/web/components/new/onboarding/setup/relatable-question.tsx
index 5fbe9bb4..5edb4a99 100644
--- a/apps/web/components/new/onboarding/setup/relatable-question.tsx
+++ b/apps/web/components/new/onboarding/setup/relatable-question.tsx
@@ -35,7 +35,7 @@ export function RelatableQuestion() {
const [selectedOptions, setSelectedOptions] = useState<number[]>([])
const handleContinueOrSkip = () => {
- router.push("/new/onboarding?flow=setup&step=integrations")
+ router.push("/new/onboarding/setup?step=integrations")
}
return (
diff --git a/apps/web/components/new/onboarding/welcome/continue-step.tsx b/apps/web/components/new/onboarding/welcome/continue-step.tsx
index 86b4593a..7d56d30e 100644
--- a/apps/web/components/new/onboarding/welcome/continue-step.tsx
+++ b/apps/web/components/new/onboarding/welcome/continue-step.tsx
@@ -1,45 +1,195 @@
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
-import { motion } from "motion/react"
+import { motion, type Variants } from "motion/react"
import { useRouter } from "next/navigation"
+import { ProfileStep } from "./profile-step"
+import { continueVariants, contentVariants } from "@/lib/variants"
-export function ContinueStep() {
+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("/new/onboarding?flow=welcome&step=features")
+ router.push("/new/onboarding/welcome?step=features")
}
+ const handleAddMemories = () => {
+ router.push("/new/onboarding/welcome?step=memories")
+ }
+
+ const isContinue = currentView === "continue"
+ const isFeatures = currentView === "features"
+ const isMemories = currentView === "memories"
+
return (
<motion.div
- className="text-center"
- initial={{ opacity: 0, y: 0, scale: 0.9 }}
- animate={{ opacity: 1, y: 0, scale: 1 }}
- exit={{ opacity: 0, y: 0, scale: 0.9 }}
- transition={{ duration: 1, ease: "easeOut" }}
- layout
+ variants={containerVariants}
+ initial="hidden"
+ animate="visible"
+ exit="hidden"
+ className="text-center relative"
>
- <p
+ {/* 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(
- "text-[#8A8A8A] text-sm mb-6 max-w-sm",
- dmSansClassName(),
+ "space-y-6 max-w-88",
+ !isFeatures && "absolute inset-0 pointer-events-none",
)}
>
- 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",
- }}
+ <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",
+ )}
>
- Continue →
- </Button>
+ {onSubmit && (
+ <motion.div
+ variants={contentVariants}
+ animate={isMemories ? "visible" : "hiddenDown"}
+ initial="hiddenDown"
+ >
+ <ProfileStep onSubmit={onSubmit} />
+ </motion.div>
+ )}
+ </div>
</motion.div>
)
}
diff --git a/apps/web/components/new/onboarding/welcome/features-step.tsx b/apps/web/components/new/onboarding/welcome/features-step.tsx
deleted file mode 100644
index 2671efc1..00000000
--- a/apps/web/components/new/onboarding/welcome/features-step.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { motion } from "motion/react"
-import { Button } from "@ui/components/button"
-import { useRouter } from "next/navigation"
-import { cn } from "@lib/utils"
-import { dmSansClassName } from "@/lib/fonts"
-
-export function FeaturesStep() {
- const router = useRouter()
-
- const handleContinue = () => {
- router.push("/new/onboarding?flow=welcome&step=memories")
- }
- return (
- <motion.div
- initial={{ opacity: 0, y: 40 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: 0.6, ease: "easeOut", delay: 0.3 }}
- className="text-center max-w-88 space-y-6"
- layout
- >
- <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>
-
- <motion.div
- animate={{
- opacity: 1,
- y: 0,
- }}
- transition={{ duration: 1, ease: "easeOut", delay: 1 }}
- initial={{ opacity: 0, y: 10 }}
- >
- <Button
- variant="onboarding"
- style={{
- background: "linear-gradient(180deg, #0D121A -26.14%, #000 100%)",
- }}
- onClick={handleContinue}
- >
- Add memories →
- </Button>
- </motion.div>
- </motion.div>
- )
-}
diff --git a/apps/web/components/new/onboarding/welcome/profile-step.tsx b/apps/web/components/new/onboarding/welcome/profile-step.tsx
index 65eb21c6..34393dad 100644
--- a/apps/web/components/new/onboarding/welcome/profile-step.tsx
+++ b/apps/web/components/new/onboarding/welcome/profile-step.tsx
@@ -292,7 +292,7 @@ export function ProfileStep({ onSubmit }: ProfileStepProps) {
otherLinks: otherLinks.filter((l) => l.trim()),
}
onSubmit(formData)
- router.push("/new/onboarding?flow=setup&step=relatable")
+ router.push("/new/onboarding/setup?step=relatable")
}}
>
{isSubmitting ? "Fetching..." : "Remember this →"}
diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx
index 55940a60..3e55dc23 100644
--- a/apps/web/components/views/chat/chat-messages.tsx
+++ b/apps/web/components/views/chat/chat-messages.tsx
@@ -20,7 +20,7 @@ import { Streamdown } from "streamdown"
import { TextShimmer } from "@/components/text-shimmer"
import { usePersistentChat, useProject } from "@/stores"
import { useGraphHighlights } from "@/stores/highlights"
-import { modelNames, ModelIcon } from "@/lib/models"
+import { ModelIcon } from "@/lib/models"
import { Spinner } from "../../spinner"
import { areUIMessageArraysEqual } from "@/stores/chat"
@@ -270,7 +270,6 @@ export function ChatMessages() {
},
},
}),
- maxSteps: 10,
onFinish: (result) => {
const activeId = activeChatIdRef.current
if (!activeId) return
diff --git a/apps/web/hooks/use-document-mutations.ts b/apps/web/hooks/use-document-mutations.ts
index f3931b5f..bafa0e58 100644
--- a/apps/web/hooks/use-document-mutations.ts
+++ b/apps/web/hooks/use-document-mutations.ts
@@ -3,12 +3,28 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { $fetch } from "@lib/api"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import type { z } from "zod"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+
+interface DocumentWithId {
+ id?: string
+ customId?: string | null
+}
interface DocumentsQueryData {
- documents: unknown[]
+ documents: DocumentWithId[]
totalCount: number
}
+type InfiniteQueryData = {
+ pages: DocumentsResponse[]
+ pageParams: number[]
+}
+
+type QueryData = DocumentsQueryData | InfiniteQueryData
+
interface UseDocumentMutationsOptions {
onClose?: () => void
}
@@ -51,7 +67,7 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
])
const optimisticMemory = {
- id: `temp-${Date.now()}`,
+ id: `temp-${crypto.randomUUID()}`,
content: content,
url: null,
title: content.substring(0, 100),
@@ -134,7 +150,7 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
])
const optimisticMemory = {
- id: `temp-${Date.now()}`,
+ id: `temp-${crypto.randomUUID()}`,
content: "",
url: url,
title: "Processing...",
@@ -251,7 +267,7 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
])
const optimisticMemory = {
- id: `temp-file-${Date.now()}`,
+ id: `temp-file-${crypto.randomUUID()}`,
content: "",
url: null,
title: title || file.name,
@@ -341,10 +357,100 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
},
})
+ const deleteMutation = useMutation({
+ mutationFn: async ({ documentId }: { documentId: string }) => {
+ const response = await $fetch("@delete/documents/:id", {
+ params: { id: documentId },
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to delete document")
+ }
+
+ return response.data
+ },
+ onMutate: async ({ documentId }) => {
+ await queryClient.cancelQueries({
+ queryKey: ["documents-with-memories"],
+ })
+
+ const previousQueries = queryClient.getQueriesData({
+ queryKey: ["documents-with-memories"],
+ })
+
+ queryClient.setQueriesData(
+ { queryKey: ["documents-with-memories"] },
+ (old: QueryData | undefined) => {
+ if (!old) return old
+
+ if ("pages" in old) {
+ const infiniteData = old as InfiniteQueryData
+ return {
+ ...infiniteData,
+ pages: infiniteData.pages.map((page) => {
+ if (!page?.documents) return page
+ return {
+ ...page,
+ documents: page.documents.filter(
+ (doc) => doc.id !== documentId && doc.customId !== documentId,
+ ),
+ pagination: page.pagination
+ ? {
+ ...page.pagination,
+ totalItems: Math.max(
+ 0,
+ (page.pagination.totalItems ?? 0) - 1,
+ ),
+ }
+ : page.pagination,
+ }
+ }),
+ }
+ }
+
+ if ("documents" in old) {
+ const queryData = old as DocumentsQueryData
+ return {
+ ...queryData,
+ documents: queryData.documents.filter(
+ (doc: DocumentWithId) => {
+ return doc.id !== documentId && doc.customId !== documentId
+ },
+ ),
+ totalCount: Math.max(0, (queryData.totalCount ?? 0) - 1),
+ }
+ }
+
+ return old
+ },
+ )
+
+ return { previousQueries }
+ },
+ onError: (_error, _variables, context) => {
+ if (context?.previousQueries) {
+ context.previousQueries.forEach(([queryKey, data]) => {
+ queryClient.setQueryData(queryKey, data)
+ })
+ }
+ toast.error("Failed to delete document", {
+ description: _error instanceof Error ? _error.message : "Unknown error",
+ })
+ },
+ onSuccess: () => {
+ toast.success("Document deleted successfully!")
+ queryClient.invalidateQueries({
+ queryKey: ["documents-with-memories"],
+ })
+ onClose?.()
+ },
+ })
+
return {
noteMutation,
linkMutation,
fileMutation,
updateMutation,
+ deleteMutation,
}
}
diff --git a/apps/web/lib/variants.ts b/apps/web/lib/variants.ts
new file mode 100644
index 00000000..1bfebb7c
--- /dev/null
+++ b/apps/web/lib/variants.ts
@@ -0,0 +1,95 @@
+import type { Variants } from "motion/react"
+
+export const contentVariants: Variants = {
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.5,
+ ease: "easeInOut",
+ delay: 0.2,
+ },
+ },
+ hiddenUp: {
+ opacity: 0,
+ y: -40,
+ transition: {
+ duration: 0,
+ },
+ },
+ hiddenDown: {
+ opacity: 0,
+ y: 40,
+ transition: {
+ duration: 0,
+ },
+ },
+}
+
+export const continueVariants: Variants = {
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.5,
+ ease: "easeInOut",
+ },
+ },
+ hidden: {
+ opacity: 0,
+ y: -40,
+ transition: {
+ duration: 0,
+ },
+ },
+}
+
+export const gapVariants: Variants = {
+ default: {
+ gap: 16,
+ transition: {
+ duration: 0.6,
+ ease: "easeOut",
+ },
+ },
+ minimized: {
+ gap: 0,
+ transition: {
+ duration: 0.6,
+ ease: "easeOut",
+ },
+ },
+}
+
+export const orbVariants: Variants = {
+ default: {
+ scale: 1,
+ padding: 48,
+ paddingTop: 0,
+ y: 0,
+ transition: {
+ duration: 0.8,
+ ease: "easeOut",
+ },
+ },
+ features: {
+ scale: 0.7,
+ padding: 0,
+ paddingTop: 0,
+ y: 0,
+ transition: {
+ duration: 0.8,
+ ease: "easeOut",
+ },
+ },
+ memories: {
+ scale: 0.4,
+ padding: 0,
+ paddingTop: 0,
+ y: 0,
+ transition: {
+ duration: 0.8,
+ ease: "easeOut",
+ },
+ },
+}