From aa2c1adf49ceb8b20d56295b1331501b6873ae96 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommmu Date: Thu, 23 Oct 2025 13:04:12 -0700 Subject: [PATCH] chore: update the flow of onboarding structure --- apps/web/app/api/exa/fetch-content/route.ts | 61 +++++ apps/web/app/onboarding/page.tsx | 252 ++++++++++++++++++ .../web/app/onboarding/setup/chat-sidebar.tsx | 114 +++++++- .../onboarding/setup/integrations-step.tsx | 10 +- apps/web/app/onboarding/setup/page.tsx | 46 +--- .../onboarding/setup/relatable-question.tsx | 18 +- .../app/onboarding/welcome/continue-step.tsx | 15 +- .../app/onboarding/welcome/features-step.tsx | 20 +- .../app/onboarding/welcome/memories-step.tsx | 126 ++------- apps/web/app/onboarding/welcome/page.tsx | 180 ------------- apps/web/middleware.ts | 14 +- apps/web/utils/exa-api.ts | 53 ---- apps/web/utils/url-helpers.ts | 59 ++++ 13 files changed, 537 insertions(+), 431 deletions(-) create mode 100644 apps/web/app/api/exa/fetch-content/route.ts create mode 100644 apps/web/app/onboarding/page.tsx delete mode 100644 apps/web/app/onboarding/welcome/page.tsx delete mode 100644 apps/web/utils/exa-api.ts create mode 100644 apps/web/utils/url-helpers.ts 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 (