diff --git a/apps/web/app/(app)/old/onboarding/layout.tsx b/apps/web/app/(app)/old/onboarding/layout.tsx deleted file mode 100644 index 644b2018d..000000000 --- a/apps/web/app/(app)/old/onboarding/layout.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client" - -import { - createContext, - useContext, - useState, - useEffect, - useCallback, - type ReactNode, -} from "react" -import { useAuth } from "@lib/auth-context" - -export type MemoryFormData = { - twitter: string - linkedin: string - description: string - otherLinks: string[] -} | null - -interface OnboardingContextValue { - name: string - setName: (name: string) => void - memoryFormData: MemoryFormData - setMemoryFormData: (data: MemoryFormData) => void - resetOnboarding: () => void -} - -const OnboardingContext = createContext(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("") - const [memoryFormData, setMemoryFormDataState] = - useState(null) - - useEffect(() => { - const storedName = localStorage.getItem("onboarding_name") - const storedMemoryFormData = localStorage.getItem( - "onboarding_memoryFormData", - ) - - if (storedName) { - setNameState(storedName) - } else if (user?.displayUsername) { - setNameState(user.displayUsername) - localStorage.setItem("onboarding_name", user.displayUsername) - } else if (user?.name) { - setNameState(user.name) - localStorage.setItem("onboarding_name", user.name) - } - - if (storedMemoryFormData) { - try { - setMemoryFormDataState(JSON.parse(storedMemoryFormData)) - } catch { - // ignore parse errors - } - } - }, [user?.displayUsername, user?.name]) - - const setName = useCallback((newName: string) => { - setNameState(newName) - localStorage.setItem("onboarding_name", newName) - localStorage.setItem("username", newName) - }, []) - - const setMemoryFormData = useCallback((data: MemoryFormData) => { - setMemoryFormDataState(data) - if (data) { - localStorage.setItem("onboarding_memoryFormData", JSON.stringify(data)) - } else { - localStorage.removeItem("onboarding_memoryFormData") - } - }, []) - - const resetOnboarding = useCallback(() => { - localStorage.removeItem("onboarding_name") - localStorage.removeItem("onboarding_memoryFormData") - setNameState("") - setMemoryFormDataState(null) - }, []) - - const contextValue: OnboardingContextValue = { - name, - setName, - memoryFormData, - setMemoryFormData, - resetOnboarding, - } - - return ( - - {children} - - ) -} diff --git a/apps/web/app/(app)/old/onboarding/page.tsx b/apps/web/app/(app)/old/onboarding/page.tsx deleted file mode 100644 index 883a031b7..000000000 --- a/apps/web/app/(app)/old/onboarding/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { useRouter } from "next/navigation" - -export default function OnboardingPage() { - const router = useRouter() - - useEffect(() => { - router.replace("/old/onboarding/welcome?step=input") - }, [router]) - - return ( -
-
Loading...
-
- ) -} diff --git a/apps/web/app/(app)/old/onboarding/setup/layout.tsx b/apps/web/app/(app)/old/onboarding/setup/layout.tsx deleted file mode 100644 index d68f43252..000000000 --- a/apps/web/app/(app)/old/onboarding/setup/layout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client" - -import { - createContext, - useContext, - useCallback, - useEffect, - useRef, - type ReactNode, -} from "react" -import { useRouter, useSearchParams } from "next/navigation" -import { useOnboardingContext, type MemoryFormData } from "../layout" -import { analytics } from "@/lib/analytics" - -export const SETUP_STEPS = ["integrations"] as const -export type SetupStep = (typeof SETUP_STEPS)[number] - -interface SetupContextValue { - memoryFormData: MemoryFormData - currentStep: SetupStep - goToStep: (step: SetupStep) => void - goToWelcome: (step?: string) => void - finishOnboarding: () => void -} - -const SetupContext = createContext(null) - -export function useSetupContext() { - const ctx = useContext(SetupContext) - if (!ctx) { - throw new Error("useSetupContext must be used within SetupLayout") - } - return ctx -} - -export default function SetupLayout({ children }: { children: ReactNode }) { - const router = useRouter() - const searchParams = useSearchParams() - const { memoryFormData, resetOnboarding } = useOnboardingContext() - - const stepParam = searchParams.get("step") - const currentStep: SetupStep = SETUP_STEPS.includes(stepParam as SetupStep) - ? (stepParam as SetupStep) - : "integrations" - const hasTrackedInitialStep = useRef(false) - - const goToStep = useCallback( - (step: SetupStep) => { - analytics.onboardingStepViewed({ step, trigger: "user" }) - router.push(`/onboarding/setup?step=${step}`) - }, - [router], - ) - - const goToWelcome = useCallback( - (step = "input") => { - router.push(`/onboarding/welcome?step=${step}`) - }, - [router], - ) - - const finishOnboarding = useCallback(() => { - resetOnboarding() - router.push("/") - }, [router, resetOnboarding]) - - useEffect(() => { - if (!hasTrackedInitialStep.current) { - analytics.onboardingStepViewed({ step: currentStep, trigger: "user" }) - hasTrackedInitialStep.current = true - } - }, [currentStep]) - - const contextValue: SetupContextValue = { - memoryFormData, - currentStep, - goToStep, - goToWelcome, - finishOnboarding, - } - - return ( - - {children} - - ) -} diff --git a/apps/web/app/(app)/old/onboarding/setup/page.tsx b/apps/web/app/(app)/old/onboarding/setup/page.tsx deleted file mode 100644 index 725a249e3..000000000 --- a/apps/web/app/(app)/old/onboarding/setup/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client" - -import { AnimatePresence } from "motion/react" - -import { IntegrationsStep } from "@/components/onboarding/setup/integrations-step" - -import { SetupHeader } from "@/components/onboarding/setup/header" -import { ChatSidebar } from "@/components/onboarding/setup/chat-sidebar" -import { AnimatedGradientBackground } from "@/components/animated-gradient-background" -import { useIsMobile } from "@hooks/use-mobile" - -import { useSetupContext } from "./layout" - -export default function SetupPage() { - const { memoryFormData } = useSetupContext() - const isMobile = useIsMobile() - - return ( -
- - - - -
-
-
-
- - - -
- - {!isMobile && ( - - - - )} -
-
-
- - {isMobile && } -
- ) -} diff --git a/apps/web/app/(app)/old/onboarding/welcome/layout.tsx b/apps/web/app/(app)/old/onboarding/welcome/layout.tsx deleted file mode 100644 index 21d349b15..000000000 --- a/apps/web/app/(app)/old/onboarding/welcome/layout.tsx +++ /dev/null @@ -1,166 +0,0 @@ -"use client" - -import { - createContext, - useContext, - useState, - useEffect, - useCallback, - useRef, - type ReactNode, -} from "react" -import { useRouter, useSearchParams } from "next/navigation" -import { useOnboardingContext, type MemoryFormData } from "../layout" -import { useAuth } from "@lib/auth-context" -import { analytics } from "@/lib/analytics" - -export const WELCOME_STEPS = [ - "input", - "greeting", - "welcome", - "username", - "features", - "memories", -] as const -export type WelcomeStep = (typeof WELCOME_STEPS)[number] - -interface WelcomeContextValue { - name: string - setName: (name: string) => void - isSubmitting: boolean - setIsSubmitting: (value: boolean) => void - showWelcomeContent: boolean - memoryFormData: MemoryFormData - setMemoryFormData: (data: MemoryFormData) => void - currentStep: WelcomeStep - goToStep: (step: WelcomeStep) => void - goToSetup: (step?: string) => void -} - -const WelcomeContext = createContext(null) - -export function useWelcomeContext() { - const ctx = useContext(WelcomeContext) - if (!ctx) { - throw new Error("useWelcomeContext must be used within WelcomeLayout") - } - return ctx -} - -export default function WelcomeLayout({ children }: { children: ReactNode }) { - const router = useRouter() - const searchParams = useSearchParams() - const { name, setName, memoryFormData, setMemoryFormData } = - useOnboardingContext() - const { organizations } = useAuth() - const hasOrgs = Array.isArray(organizations) && organizations.length > 0 - - const stepParam = searchParams.get("step") - const resolvedStep: WelcomeStep = WELCOME_STEPS.includes( - stepParam as WelcomeStep, - ) - ? (stepParam as WelcomeStep) - : "input" - const currentStep: WelcomeStep = - resolvedStep === "input" && hasOrgs ? "greeting" : resolvedStep - - const [isSubmitting, setIsSubmitting] = useState(false) - const [showWelcomeContent, setShowWelcomeContent] = useState(false) - const isMountedRef = useRef(true) - const hasTrackedInitialStep = useRef(false) - - useEffect(() => { - isMountedRef.current = true - return () => { - isMountedRef.current = false - } - }, []) - - useEffect(() => { - if (currentStep === "input") { - setShowWelcomeContent(false) - const timer = setTimeout(() => { - if (isMountedRef.current) { - setShowWelcomeContent(true) - } - }, 400) - return () => clearTimeout(timer) - } - setShowWelcomeContent(true) - }, [currentStep]) - - useEffect(() => { - const timers: NodeJS.Timeout[] = [] - - if (currentStep === "greeting") { - timers.push( - setTimeout(() => { - if (isMountedRef.current) { - analytics.onboardingStepViewed({ step: "welcome", trigger: "auto" }) - router.replace("/old/onboarding/welcome?step=welcome") - } - }, 2000), - ) - } else if (currentStep === "welcome") { - timers.push( - setTimeout(() => { - if (isMountedRef.current) { - analytics.onboardingStepViewed({ - step: "username", - trigger: "auto", - }) - router.replace("/old/onboarding/welcome?step=username") - } - }, 2000), - ) - } - - return () => { - timers.forEach(clearTimeout) - } - }, [currentStep, router]) - - useEffect(() => { - if (!hasTrackedInitialStep.current) { - analytics.onboardingStepViewed({ - step: currentStep, - trigger: "user", - }) - hasTrackedInitialStep.current = true - } - }, [currentStep]) - - const goToStep = useCallback( - (step: WelcomeStep) => { - analytics.onboardingStepViewed({ step, trigger: "user" }) - router.push(`/old/onboarding/welcome?step=${step}`) - }, - [router], - ) - - const goToSetup = useCallback( - (step = "integrations") => { - router.push(`/old/onboarding/setup?step=${step}`) - }, - [router], - ) - - const contextValue: WelcomeContextValue = { - name, - setName, - isSubmitting, - setIsSubmitting, - showWelcomeContent, - memoryFormData, - setMemoryFormData, - currentStep, - goToStep, - goToSetup, - } - - return ( - - {children} - - ) -} diff --git a/apps/web/app/(app)/old/onboarding/welcome/page.tsx b/apps/web/app/(app)/old/onboarding/welcome/page.tsx deleted file mode 100644 index fe12bf16a..000000000 --- a/apps/web/app/(app)/old/onboarding/welcome/page.tsx +++ /dev/null @@ -1,265 +0,0 @@ -"use client" - -import { useRef } from "react" -import { motion, AnimatePresence } from "motion/react" -import { cn } from "@lib/utils" - -import { InputStep } from "@/components/onboarding/welcome/input-step" -import { GreetingStep } from "@/components/onboarding/welcome/greeting-step" -import { WelcomeStep } from "@/components/onboarding/welcome/welcome-step" -import { OnboardingContentStep } from "@/components/onboarding/welcome/continue-step" - -import { InitialHeader } from "@/components/initial-header" -import { Logo } from "@ui/assets/Logo" -import NovaOrb from "@/components/nova/nova-orb" - -import { - useWelcomeContext, - type WelcomeStep as WelcomeStepType, -} from "./layout" -import { gapVariants, orbVariants } from "@/lib/variants" -import { authClient } from "@lib/auth" -import { useAuth } from "@lib/auth-context" -import { analytics } from "@/lib/analytics" -import { toast } from "sonner" - -function generateSlugFromName(value: string) { - return ( - value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, "") || "org" - ) -} - -function generateOrgSlug(name: string) { - const base = generateSlugFromName(name.trim()) - const randomNum = Math.floor(100000 + Math.random() * 900000) - return `${base}-${randomNum}` -} - -function generateUsername(name: string) { - const base = generateSlugFromName(name.trim()).replace(/-/g, "_") - const randomNum = Math.floor(100000 + Math.random() * 900000) - return `${base}${randomNum}` -} - -function UserSupermemory({ name }: { name: string }) { - return ( - - -
-

- {name.split(" ")[0]}'s -

-

- supermemory -

-
-
- ) -} - -function StepNotFound({ - goToStep, -}: { - goToStep: (step: WelcomeStepType) => void -}) { - return ( - -

Unknown step

- -
- ) -} - -export default function WelcomePage() { - const { - name, - setName, - isSubmitting, - setIsSubmitting, - showWelcomeContent, - setMemoryFormData, - currentStep, - goToStep, - } = useWelcomeContext() - - const { refetchOrganizations, setActiveOrg } = useAuth() - const submitLockRef = useRef(false) - - const handleSubmit = async () => { - const trimmed = name.trim() - if (!trimmed) return - if (submitLockRef.current) return - submitLockRef.current = true - localStorage.setItem("username", trimmed) - setIsSubmitting(true) - - try { - await authClient.updateUser({ - displayUsername: trimmed, - username: generateUsername(trimmed), - }) - - const refetchResult = await refetchOrganizations() - const refetchData = ( - refetchResult as { data?: unknown[] | null | undefined } - )?.data - const existingOrgs = Array.isArray(refetchData) ? refetchData : [] - - if (existingOrgs.length > 0) { - analytics.onboardingNameSubmitted({ - name_length: trimmed.length, - }) - goToStep("greeting") - return - } - - const uniqueSlug = generateOrgSlug(trimmed) - const completedAt = new Date().toISOString() - const newOrg = await authClient.organization.create({ - name: trimmed, - slug: uniqueSlug, - metadata: { - signupSource: "consumer", - webOnboarding: { - completedAt: null, - steps: { - welcomeInput: { - startedAt: completedAt, - completedAt, - data: {}, - }, - }, - }, - }, - }) - - await setActiveOrg(newOrg.slug) - - analytics.onboardingNameSubmitted({ name_length: trimmed.length }) - goToStep("greeting") - } catch (error) { - console.error("Onboarding submit failed:", error) - toast.error( - error instanceof Error - ? error.message - : "Could not set up your workspace. Please try again.", - ) - } finally { - submitLockRef.current = false - setIsSubmitting(false) - } - } - - const renderStep = () => { - switch (currentStep) { - case "input": - return ( - - ) - case "greeting": - return - case "welcome": - return - case "username": - case "features": - case "memories": - return ( - - ) - default: - return - } - } - - const minimizeNovaOrb = ["features", "memories"].includes(currentStep) - const novaSize = currentStep === "memories" ? 150 : 300 - const showUserSupermemory = currentStep === "username" - - return ( -
- - - {showWelcomeContent && ( -
- - - - - {showUserSupermemory && } - - - {renderStep()} - -
- )} -
- ) -} diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index 3127c148d..3a0219867 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -129,6 +129,7 @@ function fetchOgData(url: string): Promise { const PAGE_SIZE = 100 const MAX_TOTAL = 1000 +const EMPTY_SET = new Set() const MEMORIES_LOADING_LABELS = [ "Getting your supermemories…", @@ -231,7 +232,7 @@ export function MemoriesGrid({ isChatOpen, onOpenDocument, isSelectionMode = false, - selectedDocumentIds = new Set(), + selectedDocumentIds = EMPTY_SET, onEnterSelectionMode, onToggleSelection, onClearSelection, diff --git a/apps/web/components/onboarding/setup/chat-sidebar.tsx b/apps/web/components/onboarding/setup/chat-sidebar.tsx deleted file mode 100644 index b6d2f0409..000000000 --- a/apps/web/components/onboarding/setup/chat-sidebar.tsx +++ /dev/null @@ -1,870 +0,0 @@ -"use client" - -import { useState, useEffect, useCallback, useRef } from "react" -import { motion, AnimatePresence } from "motion/react" -import { useAgent } from "agents/react" -import { useAgentChat } from "@cloudflare/ai-chat/react" -import NovaOrb from "@/components/nova/nova-orb" -import { Button } from "@ui/components/button" -import { - PanelRightCloseIcon, - SendIcon, - CheckIcon, - XIcon, - Loader2, -} from "lucide-react" -import { collectValidUrls } from "@/lib/url-helpers" -import { $fetch } from "@lib/api" -import { cn } from "@lib/utils" -import { dmSansClassName } from "@/lib/fonts" -import { useAuth } from "@lib/auth-context" -import { useProject } from "@/stores" -import { Streamdown } from "streamdown" -import { useIsMobile } from "@hooks/use-mobile" - -interface ChatSidebarProps { - formData: { - twitter: string - linkedin: string - description: string - otherLinks: string[] - } | null -} - -interface DraftDoc { - kind: "likes" | "link" | "x_research" - content: string - metadata: Record - title?: string - url?: string -} - -export function ChatSidebar({ formData }: ChatSidebarProps) { - const { user } = useAuth() - const { selectedProject } = useProject() - const isMobile = useIsMobile() - const [message, setMessage] = useState("") - const [isChatOpen, setIsChatOpen] = useState(!isMobile) - const [timelineMessages, setTimelineMessages] = useState< - { - message: string - type?: "formData" | "exa" | "memory" | "waiting" - memories?: { - url: string - title: string - description: string - fullContent: string - }[] - url?: string - title?: string - description?: string - }[] - >([]) - const [isLoading, setIsLoading] = useState(false) - const [isFetchingDrafts, setIsFetchingDrafts] = useState(false) - const [draftDocs, setDraftDocs] = useState([]) - const [xResearchStatus, setXResearchStatus] = useState< - "correct" | "incorrect" | null - >(null) - const [isConfirmed, setIsConfirmed] = useState(false) - const [processingByUrl, setProcessingByUrl] = useState< - Record - >({}) - const displayedMemoriesRef = useRef>(new Set()) - const contextInjectedRef = useRef(false) - const draftsBuiltRef = useRef(false) - const isProcessingRef = useRef(false) - const draftRequestIdRef = useRef(0) - - const backendUrl = new URL(process.env.NEXT_PUBLIC_BACKEND_URL!) - const agent = useAgent({ - agent: "chat-agent", - name: user?.id ?? "anonymous", - host: backendUrl.host, - }) - - useEffect(() => { - agent.setState({ - model: "claude-sonnet-4.6" as const, - projectId: selectedProject, - }) - }, [agent, selectedProject]) - - const { - messages: chatMessages, - sendMessage, - status, - } = useAgentChat({ - agent, - getInitialMessages: null, - credentials: "include", - }) - - const buildOnboardingContext = useCallback(() => { - if (!formData) return "" - - const contextParts: string[] = [] - - if (formData.description?.trim()) { - contextParts.push(`User's interests/likes: ${formData.description}`) - } - - if (formData.twitter) { - contextParts.push(`X/Twitter profile: ${formData.twitter}`) - } - - if (formData.linkedin) { - contextParts.push(`LinkedIn profile: ${formData.linkedin}`) - } - - if (formData.otherLinks.length > 0) { - contextParts.push(`Other links: ${formData.otherLinks.join(", ")}`) - } - - const memoryTexts = timelineMessages - .filter((msg) => msg.type === "memory" && msg.memories) - .flatMap( - (msg) => msg.memories?.map((m) => `${m.title}: ${m.description}`) || [], - ) - - if (memoryTexts.length > 0) { - contextParts.push(`Extracted memories:\n${memoryTexts.join("\n")}`) - } - - return contextParts.join("\n\n") - }, [formData, timelineMessages]) - - const handleSend = () => { - if (!message.trim() || status === "submitted" || status === "streaming") - return - - let messageToSend = message - - const context = buildOnboardingContext() - - if (context && !contextInjectedRef.current && chatMessages.length === 0) { - messageToSend = `${context}\n\nUser question: ${message}` - contextInjectedRef.current = true - } - - sendMessage({ text: messageToSend }) - setMessage("") - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault() - handleSend() - } - } - - const toggleChat = () => { - setIsChatOpen(!isChatOpen) - } - - const pollForMemories = useCallback( - async (documentIds: string[]) => { - const maxAttempts = 30 // 30 attempts * 3 seconds = 90 seconds max - const pollInterval = 3000 // 3 seconds - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - try { - const response = await $fetch("@get/documents/:id", { - params: { id: documentIds[0] ?? "" }, - disableValidation: true, - }) - - console.log("response", response) - - if (response.data) { - const document = response.data - - if (document.memories && document.memories.length > 0) { - const newMemories: { - url: string - title: string - description: string - fullContent: string - }[] = [] - - document.memories.forEach( - (memory: { memory: string; title?: string }) => { - if (!displayedMemoriesRef.current.has(memory.memory)) { - displayedMemoriesRef.current.add(memory.memory) - newMemories.push({ - url: document.url || "", - title: memory.title || document.title || "Memory", - description: memory.memory || "", - fullContent: memory.memory || "", - }) - } - }, - ) - - if (newMemories.length > 0 && timelineMessages.length < 10) { - setTimelineMessages((prev) => [ - ...prev, - { - message: newMemories - .map((memory) => memory.description) - .join("\n"), - type: "memory" as const, - memories: newMemories, - }, - ]) - } - } - - if (document.memories && document.memories.length > 0) { - break - } - } - - await new Promise((resolve) => setTimeout(resolve, pollInterval)) - } catch (error) { - console.warn("Error polling for memories:", error) - await new Promise((resolve) => setTimeout(resolve, pollInterval)) - } - } - }, - [timelineMessages.length], - ) - - const buildDraftDocs = useCallback(async () => { - if (!formData || draftsBuiltRef.current) return - draftsBuiltRef.current = true - - const hasContent = - formData.twitter || - formData.linkedin || - formData.otherLinks.length > 0 || - formData.description?.trim() - - if (!hasContent) return - - const requestId = ++draftRequestIdRef.current - - setIsFetchingDrafts(true) - const drafts: DraftDoc[] = [] - - const urls = collectValidUrls(formData.linkedin, formData.otherLinks) - const allProcessingUrls: string[] = [...urls] - if (formData.twitter) { - allProcessingUrls.push(formData.twitter) - } - - if (allProcessingUrls.length > 0) { - setProcessingByUrl((prev) => { - const next = { ...prev } - for (const url of allProcessingUrls) { - next[url] = true - } - return next - }) - } - - try { - if (formData.description?.trim()) { - drafts.push({ - kind: "likes", - content: formData.description, - metadata: { - sm_source: "consumer", - description_source: "user_input", - }, - title: "Your Interests", - }) - } - - // Fetch each URL separately for per-link loading state - const linkPromises = urls.map(async (url) => { - try { - const response = await fetch("/api/onboarding/extract-content", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ urls: [url] }), - }) - const data = await response.json() - return data.results?.[0] || null - } catch { - return null - } finally { - // Clear this URL's processing state - if (draftRequestIdRef.current === requestId) { - setProcessingByUrl((prev) => ({ ...prev, [url]: false })) - } - } - }) - - // Fetch X/Twitter research - const xResearchPromise = formData.twitter - ? (async () => { - try { - const response = await fetch("/api/onboarding/research", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - xUrl: formData.twitter, - name: user?.name, - email: user?.email, - }), - }) - if (!response.ok) return null - const data = await response.json() - return data?.text?.trim() || null - } catch { - return null - } finally { - // Clear twitter URL's processing state - if (draftRequestIdRef.current === requestId) { - setProcessingByUrl((prev) => ({ - ...prev, - [formData.twitter]: false, - })) - } - } - })() - : Promise.resolve(null) - - const [exaResults, xResearchResult] = await Promise.all([ - Promise.all(linkPromises), - xResearchPromise, - ]) - - // Guard against stale request completing after a newer one - if (draftRequestIdRef.current !== requestId) return - - for (const result of exaResults) { - if (result && (result.text || result.description)) { - drafts.push({ - kind: "link", - content: result.text || result.description || "", - metadata: { - sm_source: "consumer", - exa_url: result.url, - exa_title: result.title, - }, - title: result.title || "Extracted Content", - url: result.url, - }) - } - } - - if (xResearchResult) { - drafts.push({ - kind: "x_research", - content: xResearchResult, - metadata: { - sm_source: "consumer", - onboarding_source: "x_research", - x_url: formData.twitter, - }, - title: "X/Twitter Profile Research", - url: formData.twitter, - }) - } - - setDraftDocs(drafts) - } catch (error) { - console.warn("Error building draft docs:", error) - } finally { - if (draftRequestIdRef.current === requestId) { - setIsFetchingDrafts(false) - } - } - }, [formData, user]) - - const handleConfirmDocs = useCallback(async () => { - if (isConfirmed || isProcessingRef.current) return - isProcessingRef.current = true - setIsConfirmed(true) - setIsLoading(true) - - try { - const promises = draftDocs.map(async (draft) => { - if (draft.kind === "x_research" && xResearchStatus !== "correct") { - return null - } - - try { - const docResponse = await $fetch("@post/documents", { - body: { - content: draft.content, - containerTags: ["sm_project_default"], - metadata: draft.metadata, - }, - }) - - return docResponse.data?.id - } catch (error) { - console.warn("Error creating document:", error) - return null - } - }) - - const results = await Promise.all(promises) - const documentIds = results.filter( - (id): id is string => id !== null && id !== undefined, - ) - - if (documentIds.length > 0) { - await pollForMemories(documentIds) - } - } catch (error) { - console.warn("Error confirming documents:", error) - setIsConfirmed(false) - } finally { - setIsLoading(false) - isProcessingRef.current = false - } - }, [draftDocs, xResearchStatus, isConfirmed, pollForMemories]) - - useEffect(() => { - if (!formData) return - - const formDataMessages: typeof timelineMessages = [] - - if (formData.twitter) { - formDataMessages.push({ - message: formData.twitter, - url: formData.twitter, - title: "X/Twitter", - description: formData.twitter, - type: "formData" as const, - }) - } - - if (formData.linkedin) { - formDataMessages.push({ - message: formData.linkedin, - url: formData.linkedin, - title: "LinkedIn", - description: formData.linkedin, - type: "formData" as const, - }) - } - - if (formData.otherLinks.length > 0) { - formData.otherLinks.forEach((link) => { - formDataMessages.push({ - message: link, - url: link, - title: "Link", - description: link, - type: "formData" as const, - }) - }) - } - - if (formData.description?.trim()) { - formDataMessages.push({ - message: formData.description, - title: "Likes", - description: formData.description, - type: "formData" as const, - }) - } - - setTimelineMessages(formDataMessages) - buildDraftDocs() - }, [formData, buildDraftDocs]) - - return ( - - {!isChatOpen ? ( - - - - {!isMobile && "Chat with Nova"} - - - ) : ( - - - {isMobile ? ( - - ) : ( - <> - - Close chat - - )} - -
- {timelineMessages.map((msg, i) => ( -
- {msg.type === "waiting" ? ( -
- - {msg.message} -
- ) : ( - <> -
- {i === 0 && ( -
- )} -
-
- {msg.type === "formData" && ( -
- {msg.title && ( -
-

- {msg.title} -

- {msg.url && processingByUrl[msg.url] && ( - - )} -
- )} - {msg.url && ( - - {msg.url} - - )} - {msg.title === "Likes" && msg.description && ( -

- {msg.description} -

- )} -
- )} - {msg.type === "memory" && ( -
- {msg.memories?.map((memory) => ( -
- {memory.title && ( -

- {memory.title} -

- )} - {memory.url && ( - - {memory.url} - - )} - {memory.description && ( -

- {memory.description} -

- )} -
- ))} -
- )} - - )} -
- ))} - {chatMessages.map((msg) => { - if (msg.role === "user") { - const text = msg.parts - .filter((part) => part.type === "text") - .map((part) => part.text) - .join(" ") - return ( -
-
-

{text}

-
-
- ) - } - if (msg.role === "assistant") { - return ( -
- -
- {msg.parts.map((part, partIndex) => { - if (part.type === "text") { - return ( -
- {part.text} -
- ) - } - if (part.type === "tool-searchMemories") { - if ( - part.state === "input-available" || - part.state === "input-streaming" - ) { - return ( -
- Searching memories... -
- ) - } - } - return null - })} -
-
- ) - } - return null - })} - {(status === "submitted" || status === "streaming") && - chatMessages[chatMessages.length - 1]?.role === "user" && ( -
- - Thinking... -
- )} - {timelineMessages.length === 0 && - chatMessages.length === 0 && - !isLoading && - !formData && ( -
- - Waiting for your input -
- )} - {isLoading && ( -
- - Extracting memories... -
- )} -
- - {draftDocs.some((d) => d.kind === "x_research") && !isConfirmed && ( -
-
-

- Your Profile Summary -

-
-

- {draftDocs.find((d) => d.kind === "x_research")?.content} -

-
-
- - Is this accurate? - - - -
- {xResearchStatus === "incorrect" && ( - <> -

- If incorrect, share your info in the input below, or you - can add memories later as well. -

- - - )} -
-
- )} - - {!draftDocs.some((d) => d.kind === "x_research") && - draftDocs.length > 0 && - !isConfirmed && ( -
- -
- )} - -
- {isFetchingDrafts && ( -
- - - Getting all relevant info about you... - -
- )} -
{ - e.preventDefault() - if (message.trim()) { - handleSend() - } - }} - > - setMessage(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Chat with your Supermemory" - className="w-full text-white placeholder:text-white/20 rounded-sm outline-none resize-none text-base leading-relaxed bg-transparent px-2 h-10" - disabled={status === "submitted" || status === "streaming"} - /> -
- -
-
-
- - )} - - ) -} diff --git a/apps/web/components/onboarding/setup/header.tsx b/apps/web/components/onboarding/setup/header.tsx deleted file mode 100644 index 97c0cea76..000000000 --- a/apps/web/components/onboarding/setup/header.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client" - -import { motion } from "motion/react" -import { Logo } from "@ui/assets/Logo" -import { useAuth } from "@lib/auth-context" -import { UserProfileMenu } from "@/components/user-profile-menu" -import { useRouter } from "next/navigation" -import { cn } from "@lib/utils" -import { dmSansClassName } from "@/lib/fonts" -import { useLocalStorageUsername } from "@hooks/use-local-storage-username" -import { useOrgOnboarding } from "@hooks/use-org-onboarding" -import { analytics } from "@/lib/analytics" - -export function SetupHeader() { - const { user } = useAuth() - const router = useRouter() - const localStorageUsername = useLocalStorageUsername() - const { markOrgOnboarded, isLoading: isOrgLoading } = useOrgOnboarding() - - const handleSkip = () => { - markOrgOnboarded() - analytics.onboardingCompleted() - router.push("/") - } - - const displayName = - user?.displayUsername || localStorageUsername || user?.name || "" - const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My" - - return ( - - -
- {!isOrgLoading && ( - - )} - {user && } -
-
- ) -} diff --git a/apps/web/components/onboarding/setup/integrations-step.tsx b/apps/web/components/onboarding/setup/integrations-step.tsx deleted file mode 100644 index 40a856a23..000000000 --- a/apps/web/components/onboarding/setup/integrations-step.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client" - -import { CHROME_EXTENSION_URL } from "@repo/lib/constants" -import { useState } from "react" -import { Button } from "@ui/components/button" -import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view" -import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view" -import { useRouter } from "next/navigation" -import { cn } from "@lib/utils" -import { dmSansClassName } from "@/lib/fonts" -import { useOrgOnboarding } from "@hooks/use-org-onboarding" -import { analytics } from "@/lib/analytics" - -const integrationCards = [ - { - title: "Capture", - description: "Add the Chrome extension for one-click saves", - icon: ( -
- Chrome -
- ), - }, - { - title: "Connect to AI", - description: "Set up once and use your memory in Cursor, Claude, etc", - icon: ( -
- MCP -
- ), - }, - { - title: "Connect", - description: "Link Notion, Google Drive, or OneDrive to import your docs", - icon: ( -
- Connectors -
- ), - }, - { - title: "Import", - description: - "Bring in X/Twitter bookmarks, and turn them into useful memories", - icon: ( -
- X -
- ), - }, -] - -export function IntegrationsStep() { - const router = useRouter() - const [selectedCard, setSelectedCard] = useState(null) - const { markOrgOnboarded } = useOrgOnboarding() - - const handleContinue = () => { - markOrgOnboarded() - analytics.onboardingCompleted() - router.push("/") - } - - if (selectedCard === "Connect to AI") { - return setSelectedCard(null)} /> - } - if (selectedCard === "Import") { - return setSelectedCard(null)} /> - } - return ( -
-
-

- Build your personal memory -

-

- Your supermemory comes alive when you
capture and connect - what's important -

-
- -
- {integrationCards.map((card) => { - const isClickable = - card.title === "Connect to AI" || - card.title === "Capture" || - card.title === "Import" - - if (isClickable) { - return ( - - ) - } - - return ( -
-
-

{card.title}

-

- {card.description} -

-
-
{card.icon}
-
- ) - })} -
- -
- -
-
- ) -} diff --git a/apps/web/components/onboarding/welcome/continue-step.tsx b/apps/web/components/onboarding/welcome/continue-step.tsx deleted file mode 100644 index 1b8fe91b1..000000000 --- a/apps/web/components/onboarding/welcome/continue-step.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { dmSansClassName } from "@/lib/fonts" -import { cn } from "@lib/utils" -import { Button } from "@ui/components/button" -import { motion, type Variants } from "motion/react" -import { useRouter } from "next/navigation" -import { ProfileStep } from "./profile-step" -import { continueVariants, contentVariants } from "@/lib/variants" - -type OnboardingView = "continue" | "features" | "memories" - -interface OnboardingContentStepProps { - currentView?: OnboardingView - onSubmit?: (data: { - twitter: string - linkedin: string - description: string - otherLinks: string[] - }) => void -} - -const containerVariants: Variants = { - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.4, - ease: "easeOut", - }, - }, - hidden: { - opacity: 0, - transition: { - duration: 0, - }, - }, -} - -export function OnboardingContentStep({ - currentView = "continue", - onSubmit, -}: OnboardingContentStepProps) { - const router = useRouter() - - const handleContinue = () => { - router.push("/old/onboarding/welcome?step=features") - } - - const handleAddMemories = () => { - router.push("/old/onboarding/welcome?step=memories") - } - - const isContinue = currentView === "continue" - const isFeatures = currentView === "features" - const isMemories = currentView === "memories" - - return ( - - {/* Continue content */} - -

- I'm built with Supermemory's super fast memory API, -
so you never have to worry about forgetting
what matters - across your AI apps. -

- -
- - {/* Features content */} - -

- What I can do for you -

- -
-
-
- Brain icon -
-
-

Remember every context

-

- I keep track of what you've saved and shared with your - supermemory. -

-
-
- -
-
- Search icon -
-
-

Find when you need it

-

- I surface the right memories inside
your supermemory, - superfast. -

-
-
- -
-
- Growth icon -
-
-

- Grow with your supermemory -

-

- I learn and personalize over time, so every interaction feels - natural. -

-
-
-
- - -
- - {/* Memories/Profile content */} -
- {onSubmit && ( - - - - )} -
-
- ) -} diff --git a/apps/web/components/onboarding/welcome/greeting-step.tsx b/apps/web/components/onboarding/welcome/greeting-step.tsx deleted file mode 100644 index 744e3719a..000000000 --- a/apps/web/components/onboarding/welcome/greeting-step.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { motion } from "motion/react" - -interface GreetingStepProps { - name: string -} - -export function GreetingStep({ name }: GreetingStepProps) { - const userName = name ? `${name.split(" ")[0]}` : "" - return ( - -

- Hi {userName}, I'm Nova -

-
- ) -} diff --git a/apps/web/components/onboarding/welcome/input-step.tsx b/apps/web/components/onboarding/welcome/input-step.tsx deleted file mode 100644 index f89156c85..000000000 --- a/apps/web/components/onboarding/welcome/input-step.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { motion } from "motion/react" -import { cn } from "@lib/utils" -import { LabeledInput } from "@ui/input/labeled-input" -import { Button } from "@ui/components/button" - -interface InputStepProps { - name: string - setName: (name: string) => void - handleSubmit: () => void - isSubmitting: boolean -} - -export function InputStep({ - name, - setName, - handleSubmit, - isSubmitting, -}: InputStepProps) { - return ( - -

- What should I call you? -

-
- { - if (e.key !== "Enter") return - e.preventDefault() - if (isSubmitting) return - handleSubmit() - }, - className: "!text-white placeholder:!text-[#525966] !h-[40px] pl-4", - }} - onChange={(e) => { - if (isSubmitting) return - setName((e.target as HTMLInputElement).value) - }} - style={{ - background: - "linear-gradient(0deg, rgba(91, 126, 245, 0.04) 0%, rgba(91, 126, 245, 0.04) 100%)", - }} - /> - -
-
- ) -} diff --git a/apps/web/components/onboarding/welcome/profile-step.tsx b/apps/web/components/onboarding/welcome/profile-step.tsx deleted file mode 100644 index adaa450fe..000000000 --- a/apps/web/components/onboarding/welcome/profile-step.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { motion } from "motion/react" -import { Button } from "@ui/components/button" -import { useState } from "react" -import { useRouter } from "next/navigation" -import { - parseXHandle, - parseLinkedInHandle, - toXProfileUrl, - toLinkedInProfileUrl, - normalizeUrl, -} from "@/lib/url-helpers" -import { analytics } from "@/lib/analytics" - -interface ProfileStepProps { - onSubmit: (data: { - twitter: string - linkedin: string - description: string - otherLinks: string[] - }) => void -} - -type ValidationError = { - twitter: string | null - linkedin: string | null -} - -export function ProfileStep({ onSubmit }: ProfileStepProps) { - const router = useRouter() - const [otherLinks, setOtherLinks] = useState([""]) - const [twitterHandle, setTwitterHandle] = useState("") - const [linkedinProfile, setLinkedinProfile] = useState("") - const [description, setDescription] = useState("") - const [isSubmitting] = useState(false) - const [errors, setErrors] = useState({ - twitter: null, - linkedin: null, - }) - - const addOtherLink = () => { - if (otherLinks.length < 3) { - setOtherLinks([...otherLinks, ""]) - } - } - - const updateOtherLink = (index: number, value: string) => { - const updated = [...otherLinks] - updated[index] = value - setOtherLinks(updated) - } - - const validateTwitterHandle = (handle: string): string | null => { - if (!handle.trim()) return null - - // Basic validation: handle should be alphanumeric, underscore, or hyphen - // X/Twitter handles can contain letters, numbers, and underscores, max 15 chars - const handlePattern = /^[a-zA-Z0-9_]{1,15}$/ - if (!handlePattern.test(handle.trim())) { - return "Enter your handle or profile link" - } - - return null - } - - const validateLinkedInHandle = (handle: string): string | null => { - if (!handle.trim()) return null - - // Basic validation: LinkedIn handles are typically alphanumeric with hyphens - // They can be quite long, so we'll be lenient - const handlePattern = /^[a-zA-Z0-9-]+$/ - if (!handlePattern.test(handle.trim())) { - return "Enter your handle or profile link" - } - - return null - } - - const handleTwitterChange = (value: string) => { - setTwitterHandle(value) - setErrors((prev) => ({ ...prev, twitter: null })) - } - - const handleTwitterBlur = () => { - if (!twitterHandle.trim()) return - const parsed = parseXHandle(twitterHandle) - setTwitterHandle(parsed) - const error = validateTwitterHandle(parsed) - setErrors((prev) => ({ ...prev, twitter: error })) - } - - const handleLinkedInChange = (value: string) => { - setLinkedinProfile(value) - setErrors((prev) => ({ ...prev, linkedin: null })) - } - - const handleLinkedInBlur = () => { - if (!linkedinProfile.trim()) return - const parsed = parseLinkedInHandle(linkedinProfile) - setLinkedinProfile(parsed) - const error = validateLinkedInHandle(parsed) - setErrors((prev) => ({ ...prev, linkedin: error })) - } - - return ( - -

- Let's add your memories -

- -
-
- - handleTwitterChange(e.target.value)} - onBlur={handleTwitterBlur} - className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none focus:border-[#4A4A4A] transition-colors h-[40px] ${ - errors.twitter - ? "border-[#52596633] bg-[#290F0A]" - : "border-onboarding/20" - }`} - /> -
- -
- - handleLinkedInChange(e.target.value)} - onBlur={handleLinkedInBlur} - className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none focus:border-[#4A4A4A] transition-colors h-[40px] ${ - errors.linkedin - ? "border-[#52596633] bg-[#290F0A]" - : "border-onboarding/20" - }`} - /> -
- - - -
- -