diff --git a/apps/web/app/api/exa/fetch-content/route.ts b/apps/web/app/api/exa/fetch-content/route.ts
new file mode 100644
index 00000000..6cdb40d5
--- /dev/null
+++ b/apps/web/app/api/exa/fetch-content/route.ts
@@ -0,0 +1,61 @@
+export interface ExaContentResult {
+ url: string
+ text: string
+ title: string
+ author?: string
+}
+
+interface ExaApiResponse {
+ results: ExaContentResult[]
+}
+
+export async function POST(request: Request) {
+ try {
+ const { urls } = await request.json()
+
+ if (!Array.isArray(urls) || urls.length === 0) {
+ return Response.json(
+ { error: "Invalid input: urls must be a non-empty array" },
+ { status: 400 },
+ )
+ }
+
+ if (!urls.every((url) => typeof url === "string" && url.trim())) {
+ return Response.json(
+ { error: "Invalid input: all urls must be non-empty strings" },
+ { status: 400 },
+ )
+ }
+
+ const response = await fetch("https://api.exa.ai/contents", {
+ method: "POST",
+ headers: {
+ "x-api-key": process.env.EXA_API_KEY ?? "",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ urls,
+ text: true,
+ livecrawl: "fallback",
+ }),
+ })
+
+ if (!response.ok) {
+ console.error(
+ "Exa API request failed:",
+ response.status,
+ response.statusText,
+ )
+ return Response.json(
+ { error: "Failed to fetch content from Exa API" },
+ { status: 500 },
+ )
+ }
+
+ const data: ExaApiResponse = await response.json()
+ return Response.json({ results: data.results })
+ } catch (error) {
+ console.error("Exa API request error:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/apps/web/app/onboarding/page.tsx b/apps/web/app/onboarding/page.tsx
new file mode 100644
index 00000000..1f59078d
--- /dev/null
+++ b/apps/web/app/onboarding/page.tsx
@@ -0,0 +1,252 @@
+"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 "./welcome/input-step"
+import { GreetingStep } from "./welcome/greeting-step"
+import { WelcomeStep } from "./welcome/welcome-step"
+import { ContinueStep } from "./welcome/continue-step"
+import { FeaturesStep } from "./welcome/features-step"
+import { MemoriesStep } from "./welcome/memories-step"
+import { RelatableQuestion } from "./setup/relatable-question"
+import { IntegrationsStep } from "./setup/integrations-step"
+
+import { InitialHeader } from "@/components/initial-header"
+import { SetupHeader } from "./setup/header"
+import { ChatSidebar } from "./setup/chat-sidebar"
+import { Logo } from "@ui/assets/Logo"
+import NovaOrb from "@/components/nova/nova-orb"
+import { AnimatedGradientBackground } from "./setup/page"
+
+function UserSupermemory({ name }: { name: string }) {
+ return (
+
+
+
+
{name}'s
+
+ supermemory
+
+
+
+ )
+}
+
+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 currentFlow = flow || "welcome"
+ const currentStep = step || "input"
+
+ useEffect(() => {
+ if (user?.name) {
+ setName(user.name)
+ localStorage.setItem("username", user.name)
+ }
+ }, [user?.name])
+
+ 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,
+ "",
+ "/onboarding?flow=welcome&step=welcome",
+ )
+ }, 2000),
+ )
+ break
+ case "welcome":
+ timers.push(
+ setTimeout(() => {
+ // Auto-advance to username step
+ window.history.replaceState(
+ null,
+ "",
+ "/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,
+ "",
+ "/onboarding?flow=welcome&step=greeting",
+ )
+ setIsSubmitting(false)
+ }
+ }
+
+ const renderWelcomeStep = () => {
+ switch (currentStep) {
+ case "input":
+ return (
+
+ )
+ case "greeting":
+ return
+ case "welcome":
+ return
+ case "username":
+ return
+ case "features":
+ return
+ case "memories":
+ return
+ default:
+ return null
+ }
+ }
+
+ const renderSetupStep = () => {
+ switch (currentStep) {
+ case "relatable":
+ return
+ case "integrations":
+ return
+ 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"
+
+ return (
+
+ {isWelcomeFlow && (
+
+ )}
+ {isSetupFlow &&
}
+
+
+
+ {isWelcomeFlow && (
+
+
+
+
+
+ {showUserSupermemory && }
+
+
+ {renderWelcomeStep()}
+
+
+ )}
+
+ {isSetupFlow && (
+
+
+
+
+
+ {renderSetupStep()}
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/web/app/onboarding/setup/chat-sidebar.tsx b/apps/web/app/onboarding/setup/chat-sidebar.tsx
index cab3d7ef..1203eb96 100644
--- a/apps/web/app/onboarding/setup/chat-sidebar.tsx
+++ b/apps/web/app/onboarding/setup/chat-sidebar.tsx
@@ -1,14 +1,33 @@
"use client"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "motion/react"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
import { PanelRightCloseIcon, SendIcon } from "lucide-react"
+import { collectValidUrls } from "@/utils/url-helpers"
-export function ChatSidebar() {
+interface ChatSidebarProps {
+ formData: {
+ twitter: string
+ linkedin: string
+ description: string
+ otherLinks: string[]
+ } | null
+}
+
+export function ChatSidebar({ formData }: ChatSidebarProps) {
const [message, setMessage] = useState("")
const [isChatOpen, setIsChatOpen] = useState(true)
+ const [messages, setMessages] = useState<
+ {
+ url: string
+ title?: string
+ description: string
+ fullContent: string
+ }[]
+ >([])
+ const [isLoading, setIsLoading] = useState(false)
const handleSend = () => {
console.log("Message:", message)
@@ -26,6 +45,53 @@ export function ChatSidebar() {
setIsChatOpen(!isChatOpen)
}
+ // Helper function to truncate text to 2 lines
+ const truncateText = (text: string, maxLength = 120) => {
+ if (text.length <= maxLength) return text
+ return text.slice(0, maxLength).trim() + "..."
+ }
+
+ // Fetch content when formData is available
+ useEffect(() => {
+ if (!formData) return
+
+ const fetchContent = async () => {
+ setIsLoading(true)
+ const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
+
+ if (urls.length > 0) {
+ try {
+ const response = await fetch("/api/exa/fetch-content", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ urls }),
+ })
+ const { results } = await response.json()
+
+ const newMessages = results.map(
+ (result: {
+ url: string
+ title?: string
+ text?: string
+ description?: string
+ }) => ({
+ url: result.url,
+ title: result.title,
+ description: result.text || result.description || "",
+ fullContent: result.text || result.description || "",
+ }),
+ )
+ setMessages(newMessages)
+ } catch (error) {
+ console.warn("Error fetching content:", error)
+ }
+ }
+ setIsLoading(false)
+ }
+
+ fetchContent()
+ }, [formData])
+
return (
{!isChatOpen ? (
@@ -64,19 +130,41 @@ export function ChatSidebar() {
Close chat
-
-
-
+
+ {messages.length === 0 && !isLoading && (
+
-
Waiting for your input
-
-
+
+ )}
+ {isLoading && (
+
+ Fetching your memories...
+
+ )}
+ {messages.map((msg, i) => (
+
+ {msg.title && (
+
+ {msg.title}
+
+ )}
+
+ {msg.url}
+
+
+ {truncateText(msg.description)}
+
+
+ ))}
diff --git a/apps/web/app/onboarding/setup/integrations-step.tsx b/apps/web/app/onboarding/setup/integrations-step.tsx
index e0c0f57b..a1e96f4e 100644
--- a/apps/web/app/onboarding/setup/integrations-step.tsx
+++ b/apps/web/app/onboarding/setup/integrations-step.tsx
@@ -3,6 +3,7 @@
import { useState } from "react"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/mcp-detail-view"
+import { useRouter } from "next/navigation"
const integrationCards = [
{
@@ -52,11 +53,8 @@ const integrationCards = [
},
]
-interface IntegrationsStepProps {
- onBack: () => void
-}
-
-export function IntegrationsStep({ onBack }: IntegrationsStepProps) {
+export function IntegrationsStep() {
+ const router = useRouter()
const [selectedCard, setSelectedCard] = useState(null)
if (selectedCard === "Connect to AI") {
@@ -120,7 +118,7 @@ export function IntegrationsStep({ onBack }: IntegrationsStepProps) {
diff --git a/apps/web/app/onboarding/setup/page.tsx b/apps/web/app/onboarding/setup/page.tsx
index 0626122a..7437e49f 100644
--- a/apps/web/app/onboarding/setup/page.tsx
+++ b/apps/web/app/onboarding/setup/page.tsx
@@ -1,11 +1,4 @@
-"use client"
-
-import { SetupHeader } from "./header"
-import { AnimatePresence, motion } from "motion/react"
-import { RelatableQuestion } from "./relatable-question"
-import { IntegrationsStep } from "./integrations-step"
-import { ChatSidebar } from "./chat-sidebar"
-import { useState } from "react"
+import { motion } from "motion/react"
export function AnimatedGradientBackground() {
return (
@@ -55,40 +48,3 @@ export function AnimatedGradientBackground() {
)
}
-
-export default function SetupPage() {
- const [currentStep, setCurrentStep] = useState<"relatable" | "integrations">(
- "relatable",
- )
-
- const handleContinueOrSkip = () => {
- setCurrentStep("integrations")
- }
-
- const handleBack = () => {
- setCurrentStep("relatable")
- }
-
- return (
-
-
-
-
-
-
- {currentStep === "relatable" && (
-
- )}
- {currentStep === "integrations" && (
-
- )}
-
-
-
-
-
-
-
-
- )
-}
diff --git a/apps/web/app/onboarding/setup/relatable-question.tsx b/apps/web/app/onboarding/setup/relatable-question.tsx
index f642d92a..e48699d5 100644
--- a/apps/web/app/onboarding/setup/relatable-question.tsx
+++ b/apps/web/app/onboarding/setup/relatable-question.tsx
@@ -3,6 +3,7 @@
import { useState } from "react"
import { motion } from "motion/react"
import { Button } from "@ui/components/button"
+import { useRouter } from "next/navigation"
const relatableOptions = [
{
@@ -27,15 +28,14 @@ const relatableOptions = [
},
]
-interface RelatableQuestionProps {
- onContinueOrSkip: () => void
-}
-
-export function RelatableQuestion({
- onContinueOrSkip,
-}: RelatableQuestionProps) {
+export function RelatableQuestion() {
+ const router = useRouter()
const [selectedOptions, setSelectedOptions] = useState
([])
+ const handleContinueOrSkip = () => {
+ router.push("/onboarding?flow=setup&step=integrations")
+ }
+
return (
{option.emoji}
-
+
{option.text}
@@ -99,7 +99,7 @@ export function RelatableQuestion({
className="text-foreground font-medium"
variant="link"
size="lg"
- onClick={onContinueOrSkip}
+ onClick={handleContinueOrSkip}
>
{selectedOptions.length === 0 ? "Skip for now →" : "Continue →"}
diff --git a/apps/web/app/onboarding/welcome/continue-step.tsx b/apps/web/app/onboarding/welcome/continue-step.tsx
index 783ae700..0578912f 100644
--- a/apps/web/app/onboarding/welcome/continue-step.tsx
+++ b/apps/web/app/onboarding/welcome/continue-step.tsx
@@ -1,13 +1,14 @@
import { Button } from "@ui/components/button"
import { motion } from "motion/react"
+import { useRouter } from "next/navigation"
-interface ContinueStepProps {
- setCurrentStep: (
- step: "input" | "greeting" | "welcome" | "username" | "features",
- ) => void
-}
+export function ContinueStep() {
+ const router = useRouter()
+
+ const handleContinue = () => {
+ router.push("/onboarding?flow=welcome&step=features")
+ }
-export function ContinueStep({ setCurrentStep }: ContinueStepProps) {
return (
diff --git a/apps/web/app/onboarding/welcome/features-step.tsx b/apps/web/app/onboarding/welcome/features-step.tsx
index 6deca0bb..ee21a261 100644
--- a/apps/web/app/onboarding/welcome/features-step.tsx
+++ b/apps/web/app/onboarding/welcome/features-step.tsx
@@ -1,19 +1,13 @@
import { motion } from "motion/react"
import { Button } from "@ui/components/button"
+import { useRouter } from "next/navigation"
-interface FeaturesStepProps {
- setCurrentStep: (
- step:
- | "input"
- | "greeting"
- | "welcome"
- | "username"
- | "features"
- | "memories",
- ) => void
-}
+export function FeaturesStep() {
+ const router = useRouter()
-export function FeaturesStep({ setCurrentStep }: FeaturesStepProps) {
+ const handleContinue = () => {
+ router.push("/onboarding?flow=welcome&step=memories")
+ }
return (