Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions apps/web/app/(navigation)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client"

import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { useAuth } from "@lib/auth-context"
import { ChevronsDown, LoaderIcon } from "lucide-react"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { useEffect, useMemo } from "react"
import { InstallPrompt } from "@/components/install-prompt"
import { ChromeExtensionButton } from "@/components/chrome-extension-button"
import { ChatInput } from "@/components/chat-input"
Expand All @@ -14,11 +15,37 @@ import { useFeatureFlagEnabled } from "posthog-js/react"

export default function Page() {
const { user, session } = useAuth()
const { shouldShowOnboarding, isLoading: onboardingLoading } =
useOnboardingStorage()
const router = useRouter()
const flagEnabled = useFeatureFlagEnabled("nova-alpha-access")

// TODO: remove this flow after the feature flag is removed
// Old app: localStorage-backed onboarding
const {
shouldShowOnboarding: shouldShowOldOnboarding,
isLoading: oldOnboardingLoading,
} = useOnboardingStorage()

// New app: DB-backed onboarding (org.metadata.isOnboarded)
const {
shouldShowOnboarding: shouldShowNewOnboarding,
isLoading: newOnboardingLoading,
} = useOrgOnboarding()

// Select the appropriate onboarding state based on feature flag
const isOnboardingLoading = useMemo(() => {
if (flagEnabled) {
return newOnboardingLoading
}
return oldOnboardingLoading
}, [flagEnabled, newOnboardingLoading, oldOnboardingLoading])

const shouldShowOnboarding = useMemo(() => {
if (flagEnabled) {
return shouldShowNewOnboarding()
}
return shouldShowOldOnboarding()
}, [flagEnabled, shouldShowNewOnboarding, shouldShowOldOnboarding])

useEffect(() => {
const url = new URL(window.location.href)
const authenticateChromeExtension = url.searchParams.get(
Expand Down Expand Up @@ -46,16 +73,16 @@ export default function Page() {
}, [user, session])

useEffect(() => {
if (user && !onboardingLoading && shouldShowOnboarding()) {
if (user && !isOnboardingLoading && shouldShowOnboarding) {
if (flagEnabled) {
router.push("/new/onboarding?step=input&flow=welcome")
} else {
router.push("/onboarding")
}
}
}, [user, shouldShowOnboarding, onboardingLoading, router, flagEnabled])
}, [user, shouldShowOnboarding, isOnboardingLoading, router, flagEnabled])

if (!user || onboardingLoading) {
if (!user || isOnboardingLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
<div className="flex flex-col items-center gap-4">
Expand All @@ -66,7 +93,7 @@ export default function Page() {
)
}

if (shouldShowOnboarding()) {
if (shouldShowOnboarding) {
return null
}

Expand Down
57 changes: 23 additions & 34 deletions apps/web/app/api/onboarding/research/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ interface ResearchRequest {
email?: string
}

// prompt to get user context from X/Twitter profile
function finalPrompt(xUrl: string, userContext: string) {
function extractHandle(url: string): string {
const cleaned = url
.toLowerCase()
.replace("https://x.com/", "")
.replace("https://twitter.com/", "")
.replace("http://x.com/", "")
.replace("http://twitter.com/", "")
.replace("@", "")

return (cleaned.split("/")[0] ?? cleaned).split("?")[0] ?? cleaned
}

function finalPrompt(handle: string, userContext: string) {
return `You are researching a user based on their X/Twitter profile to help personalize their experience.

X/Twitter Profile URL: ${xUrl}${userContext}
X Handle: @${handle}${userContext}

Please analyze this X/Twitter profile and provide a comprehensive but concise summary of the user. Include:
- Professional background and current role (if available)
Expand All @@ -29,18 +40,12 @@ export async function POST(req: Request) {

if (!xUrl?.trim()) {
return Response.json(
{ error: "X/Twitter URL is required" },
{ error: "X/Twitter URL or handle is required" },
{ status: 400 },
)
}

const lowerUrl = xUrl.toLowerCase()
if (!lowerUrl.includes("x.com") && !lowerUrl.includes("twitter.com")) {
return Response.json(
{ error: "URL must be an X/Twitter profile link" },
{ status: 400 },
)
}
const handle = extractHandle(xUrl)

const contextParts: string[] = []
if (name) contextParts.push(`Name: ${name}`)
Expand All @@ -51,29 +56,13 @@ export async function POST(req: Request) {
: ""

const { text } = await generateText({
model: xai("grok-4-1-fast-reasoning"),
prompt: finalPrompt(xUrl, userContext),
providerOptions: {
xai: {
searchParameters: {
mode: "on",
sources: [
{
type: "web",
safeSearch: true,
},
{
type: "x",
includedXHandles: [
lowerUrl
.replace("https://x.com/", "")
.replace("https://twitter.com/", ""),
],
postFavoriteCount: 10,
},
],
},
},
model: xai.responses("grok-4-fast"),
prompt: finalPrompt(handle, userContext),
tools: {
web_search: xai.tools.webSearch(),
x_search: xai.tools.xSearch({
allowedXHandles: [handle],
}),
},
})

Expand Down
19 changes: 18 additions & 1 deletion apps/web/app/new/onboarding/setup/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
"use client"

import { createContext, useContext, useCallback, type ReactNode } from "react"
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 = ["relatable", "integrations"] as const
export type SetupStep = (typeof SETUP_STEPS)[number]
Expand Down Expand Up @@ -34,9 +42,11 @@ export default function SetupLayout({ children }: { children: ReactNode }) {
const currentStep: SetupStep = SETUP_STEPS.includes(stepParam as SetupStep)
? (stepParam as SetupStep)
: "relatable"
const hasTrackedInitialStep = useRef(false)

const goToStep = useCallback(
(step: SetupStep) => {
analytics.onboardingStepViewed({ step, trigger: "user" })
router.push(`/new/onboarding/setup?step=${step}`)
},
[router],
Expand All @@ -54,6 +64,13 @@ export default function SetupLayout({ children }: { children: ReactNode }) {
router.push("/new")
}, [router, resetOnboarding])

useEffect(() => {
if (!hasTrackedInitialStep.current) {
analytics.onboardingStepViewed({ step: currentStep, trigger: "user" })
hasTrackedInitialStep.current = true
}
}, [currentStep])

const contextValue: SetupContextValue = {
memoryFormData,
currentStep,
Expand Down
18 changes: 18 additions & 0 deletions apps/web/app/new/onboarding/welcome/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { analytics } from "@/lib/analytics"

export const WELCOME_STEPS = [
"input",
Expand Down Expand Up @@ -61,6 +62,7 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [showWelcomeContent, setShowWelcomeContent] = useState(false)
const isMountedRef = useRef(true)
const hasTrackedInitialStep = useRef(false)

useEffect(() => {
isMountedRef.current = true
Expand Down Expand Up @@ -89,6 +91,7 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
timers.push(
setTimeout(() => {
if (isMountedRef.current) {
analytics.onboardingStepViewed({ step: "welcome", trigger: "auto" })
router.replace("/new/onboarding/welcome?step=welcome")
}
}, 2000),
Expand All @@ -97,6 +100,10 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
timers.push(
setTimeout(() => {
if (isMountedRef.current) {
analytics.onboardingStepViewed({
step: "username",
trigger: "auto",
})
router.replace("/new/onboarding/welcome?step=username")
}
}, 2000),
Expand All @@ -108,8 +115,19 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
}
}, [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(`/new/onboarding/welcome?step=${step}`)
},
[router],
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/new/onboarding/welcome/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "./layout"
import { gapVariants, orbVariants } from "@/lib/variants"
import { authClient } from "@lib/auth"
import { analytics } from "@/lib/analytics"

function UserSupermemory({ name }: { name: string }) {
return (
Expand Down Expand Up @@ -88,6 +89,7 @@ export default function WelcomePage() {
console.error("Failed to update displayUsername:", error)
}

analytics.onboardingNameSubmitted({ name_length: name.trim().length })
goToStep("greeting")
setIsSubmitting(false)
}
Expand Down
60 changes: 47 additions & 13 deletions apps/web/app/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "motion/react"
import { useIsMobile } from "@hooks/use-mobile"
import { useProject } from "@/stores"
import { useQuickNoteDraftReset } from "@/stores/quick-note-draft"
import {
useQuickNoteDraftReset,
useQuickNoteDraft,
} from "@/stores/quick-note-draft"
import { analytics } from "@/lib/analytics"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
import { useQuery } from "@tanstack/react-query"
Expand All @@ -42,6 +45,7 @@ export default function NewPage() {
const [searchPrefill, setSearchPrefill] = useState("")

const resetDraft = useQuickNoteDraftReset(selectedProject)
const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "")

const { noteMutation } = useDocumentMutations({
onClose: () => {
Expand Down Expand Up @@ -92,47 +96,74 @@ export default function NewPage() {
})
useHotkeys("mod+k", (e) => {
e.preventDefault()
analytics.searchOpened({ source: "hotkey" })
setIsSearchOpen(true)
})
const [isChatOpen, setIsChatOpen] = useState(!isMobile)

const handleOpenDocument = useCallback((document: DocumentWithMemories) => {
if (document.id) {
analytics.documentModalOpened({ document_id: document.id })
}
setSelectedDocument(document)
setIsDocumentModalOpen(true)
}, [])

const handleQuickNoteSave = useCallback(
(content: string) => {
if (content.trim()) {
noteMutation.mutate({ content, project: selectedProject })
const hadPreviousContent = quickNoteDraft.trim().length > 0
noteMutation.mutate(
{ content, project: selectedProject },
{
onSuccess: () => {
if (hadPreviousContent) {
analytics.quickNoteEdited()
} else {
analytics.quickNoteCreated()
}
},
},
)
}
},
[selectedProject, noteMutation],
[selectedProject, noteMutation, quickNoteDraft],
)

const handleFullScreenSave = useCallback(
(content: string) => {
if (content.trim()) {
noteMutation.mutate({ content, project: selectedProject })
const hadInitialContent = fullscreenInitialContent.trim().length > 0
noteMutation.mutate(
{ content, project: selectedProject },
{
onSuccess: () => {
if (hadInitialContent) {
analytics.quickNoteEdited()
} else {
analytics.quickNoteCreated()
}
},
},
)
}
},
[selectedProject, noteMutation],
[selectedProject, noteMutation, fullscreenInitialContent],
)

const handleMaximize = useCallback(
(content: string) => {
setFullscreenInitialContent(content)
setIsFullScreenNoteOpen(true)
},
[],
)
const handleMaximize = useCallback((content: string) => {
analytics.fullscreenNoteModalOpened()
setFullscreenInitialContent(content)
setIsFullScreenNoteOpen(true)
}, [])

const handleHighlightsChat = useCallback((seed: string) => {
setQueuedChatSeed(seed)
setIsChatOpen(true)
}, [])

const handleHighlightsShowRelated = useCallback((query: string) => {
analytics.searchOpened({ source: "highlight_related" })
setSearchPrefill(query)
setIsSearchOpen(true)
}, [])
Expand All @@ -154,7 +185,10 @@ export default function NewPage() {
setIsMCPModalOpen(true)
}}
onOpenChat={() => setIsChatOpen(true)}
onOpenSearch={() => setIsSearchOpen(true)}
onOpenSearch={() => {
analytics.searchOpened({ source: "header" })
setIsSearchOpen(true)
}}
/>
<main
key={`main-container-${isChatOpen}`}
Expand Down
Loading
Loading