"use client" import { useEffect, useRef, useState } from "react" import { useQuery } from "@tanstack/react-query" import { useQueryState } from "nuqs" import Image from "next/image" import { Button } from "@ui/components/button" import { Dialog, DialogClose, DialogContent } from "@ui/components/dialog" import { Gmail, GoogleDrive, Granola, Notion, OneDrive } from "@ui/assets/icons" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@ui/components/select" import { AlertTriangle, ArrowRight, Check, ChevronLeft, ChevronRight, Coins, ExternalLink, FolderOpen, Github, LoaderIcon, Lock, Plus, X, } from "lucide-react" import { AppleShortcutsIcon, ChromeIcon, RaycastIcon, } from "@/components/integration-icons" function XBookmarksIcon({ className }: { className?: string }) { return ( ) } function GrokIcon({ className }: { className?: string }) { return ( ) } import { cn } from "@lib/utils" import { PLAN_DISPLAY_NAMES, PLAN_RANK, type PlanType, useTokenUsage, } from "@/hooks/use-token-usage" import { GranolaConnectModal } from "@/components/granola-connect-modal" import { dmSans125ClassName } from "@/lib/fonts" import { $fetch } from "@lib/api" import { useAuth } from "@lib/auth-context" import { useConnectorAccess } from "@/hooks/use-connector-access" import { ADD_MEMORY_SHORTCUT_URL, CHROME_EXTENSION_URL, RAYCAST_EXTENSION_URL, } from "@lib/constants" import { useCustomer } from "autumn-js/react" import { toast } from "sonner" import { connectorPause } from "@/lib/connector-availability" import { useConnectorNotify } from "@/lib/connector-notify" import { analytics } from "@/lib/analytics" import type { BrainMode } from "./types" import { usePromoCode } from "@/hooks/use-promo-code" type SourceId = | "drive" | "notion" | "gmail" | "github" | "onedrive" | "granola" | "bookmarks" | "chatapps" | "chrome" | "shortcuts" | "raycast" type SourceState = "idle" | "connecting" | "connected" | "waitlist" type DriveScope = "selective" | "full" type RequiredPlan = "pro" | "max" | "scale" const PROVIDER_TO_SOURCE: Record = { "google-drive": "drive", notion: "notion", onedrive: "onedrive", granola: "granola", } const SOURCE_LABEL: Partial> = { drive: "Google Drive", notion: "Notion", onedrive: "OneDrive", granola: "Granola", } const PLAN_LABELS: Record = { pro: "Pro", max: "Max", scale: "Scale", } const BOOK_CALL_HREF = "https://cal.com/maheshthedev/15min" type PlanCardDefinition = { id: PlanType name: string price: string period: string credits: string productId: "api_free" | "api_pro" | "api_max" | "api_scale" | "api_enterprise" description: string includesFrom?: string features: string[] isContactSales?: boolean mostPopular?: boolean } type CheckoutPlanId = Extract< PlanCardDefinition["productId"], "api_pro" | "api_max" | "api_scale" > const PLAN_CARDS: PlanCardDefinition[] = [ { id: "pro", name: "Pro", price: "$19", period: "/mo", credits: "$20", productId: "api_pro", description: "For people building with AI memory", features: ["Auto top-up when balance runs low", "Priority support"], }, { id: "max", name: "Max", price: "$100", period: "/mo", credits: "$130", productId: "api_max", description: "For power users who outgrow Pro", includesFrom: "Pro", mostPopular: true, features: ["6x the credits of Pro", "Gmail connector", "Priority support"], }, { id: "scale", name: "Scale", price: "$399", period: "/mo", credits: "$600", productId: "api_scale", description: "For teams and production workloads", includesFrom: "Max", features: [ "Auto top-up & spend caps", "S3 & Web Crawler connectors", "Dedicated support", ], }, { id: "enterprise", name: "Enterprise", price: "Custom", period: "", credits: "Unlimited", productId: "api_enterprise", description: "Custom deployments with dedicated engineering", includesFrom: "Scale", features: [ "Custom metering & billing", "Custom integrations & SSO", "Forward-deployed engineer", ], isContactSales: true, }, ] const PLAN_CARD_SCROLL_STEP = 406 const countsAsConnected = (state: SourceState | undefined) => state === "connected" || state === "waitlist" export interface SourcesValues { connected: Partial> driveScope: DriveScope } interface Props { containerTag: string mode: BrainMode values: SourcesValues onChange: (next: SourcesValues) => void onContinue: () => void } const modalCardStyle = { boxShadow: "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset", } const inputBevelStyle = { boxShadow: "0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)", } function ChatAppsIconCluster() { const tileClass = "absolute flex size-[28px] items-center justify-center rounded-[10px] border border-white/10 bg-[#0D1117] shadow-[0_4px_10px_rgba(0,0,0,0.28)]" const imageClass = "size-4 object-contain" return ( ) } export function StepSources({ containerTag, mode, values, onChange, onContinue, }: Props) { const [moreOpen, setMoreOpen] = useState(false) const [plansOpen, setPlansOpen] = useState(false) const [granolaOpen, setGranolaOpen] = useState(false) const [requestedPlan, setRequestedPlan] = useState("pro") const [requestedConnector, setRequestedConnector] = useState("This connector") const { hasMax, hasScale, connectorAccess, loading: planLoading, } = useConnectorAccess() const { org, isRestoring } = useAuth() useEffect(() => { setMoreOpen(false) }, []) const valuesRef = useRef(values) valuesRef.current = values // dedupe toasts across the param + reconcile paths const announced = useRef(new Set()) const seeded = useRef(false) const markConnected = (ids: SourceId[]) => { const current = valuesRef.current const updates: Partial> = {} for (const id of ids) { if (current.connected[id] !== "connected") updates[id] = "connected" } if (Object.keys(updates).length > 0) { onChange({ ...current, connected: { ...current.connected, ...updates }, }) } } // keyed by org so it re-runs once the active org restores on reload const { data: liveConnections, refetch: refetchConnections } = useQuery({ queryKey: ["onboarding-connections", org?.id], queryFn: async () => { const res = await $fetch("@post/connections/list", { body: { containerTags: [] }, }) if (res.error) return [] as Array<{ provider?: string }> return (res.data ?? []) as Array<{ provider?: string }> }, enabled: !isRestoring && !!org?.id, staleTime: 10_000, refetchOnWindowFocus: true, }) // biome-ignore lint/correctness/useExhaustiveDependencies: reconcile on fetched connections only useEffect(() => { if (!liveConnections) return const ids = liveConnections .map((c) => (c.provider ? PROVIDER_TO_SOURCE[c.provider] : undefined)) .filter((id): id is SourceId => Boolean(id)) markConnected(ids) // first load: seed without toasting pre-existing connections if (!seeded.current) { seeded.current = true for (const id of ids) announced.current.add(id) return } for (const id of ids) { if (!announced.current.has(id)) { announced.current.add(id) toast.success(`${SOURCE_LABEL[id] ?? "Source"} connected`) } } }, [liveConnections]) // Post-OAuth redirect lands with ?connected= — confirm it instantly. const [connectedParam, setConnectedParam] = useQueryState("connected") // biome-ignore lint/correctness/useExhaustiveDependencies: run when the param arrives useEffect(() => { if (!connectedParam) return const id = PROVIDER_TO_SOURCE[connectedParam] if (id) { markConnected([id]) if (!announced.current.has(id)) { announced.current.add(id) toast.success(`${SOURCE_LABEL[id] ?? "Source"} connected`) } } setConnectedParam(null) const t1 = setTimeout(() => void refetchConnections(), 1500) const t2 = setTimeout(() => void refetchConnections(), 4000) return () => { clearTimeout(t1) clearTimeout(t2) } }, [connectedParam]) // company_brain unlocks pro connectors; max and scale stay gated, and a // higher tier satisfies a lower requirement. const isLocked = (plan?: RequiredPlan) => { if (!plan || planLoading) return false if (plan === "scale") return !hasScale if (plan === "max") return !(hasMax || hasScale) return !connectorAccess } const notify = useConnectorNotify() const setState = (id: SourceId, state: SourceState) => { onChange({ ...values, connected: { ...values.connected, [id]: state } }) } const connectRealProvider = async ( provider: "google-drive" | "notion" | "onedrive", id: SourceId, ) => { analytics.onboardingIntegrationClicked({ integration: provider }) if (connectorPause(provider)) { notify.request(provider) setState(id, "waitlist") return } setState(id, "connecting") try { const metadata: Record = {} if (provider === "google-drive") { metadata.scope = values.driveScope } const res = await $fetch("@post/connections/:provider", { params: { provider }, body: { redirectUrl: window.location.href, containerTags: [containerTag], metadata: Object.keys(metadata).length > 0 ? metadata : undefined, }, }) const data = "data" in res ? res.data : null if (data && "authLink" in data && data.authLink) { window.location.href = data.authLink return } throw new Error("No auth link returned") } catch (err) { setState(id, "idle") toast.error( err instanceof Error ? err.message : "Could not start connection", ) } } const openExternal = (id: SourceId, url: string) => { analytics.onboardingIntegrationClicked({ integration: id }) window.open(url, "_blank", "noopener,noreferrer") setState(id, "connected") } const requestWaitlist = (id: SourceId) => { analytics.onboardingIntegrationClicked({ integration: id }) setState(id, "waitlist") } // Paused beats locked: upgrading cannot unlock a connector nobody can connect. const guard = ( plan: RequiredPlan | undefined, title: string, fn: () => void, provider?: string, ) => { return () => { if (provider && connectorPause(provider)) { fn() return } if (isLocked(plan) && plan) { setRequestedPlan(plan) setRequestedConnector(title) setPlansOpen(true) return } fn() } } const connectedCount = Object.values(values.connected).filter( countsAsConnected, ).length const handleContinue = () => { analytics.onboardingSourcesCompleted({ connected_count: connectedCount }) onContinue() } return (

{mode === "personal" ? "Bring your context together" : "Connect your team's signals"}

Start with the sources that carry the most context. Add more anytime.

{mode === "personal" ? ( <> } state={values.connected.bookmarks ?? "idle"} ctaLabel="Connect" doneLabel="Opened" perks={[ "Bookmarks become searchable memories", "One-click import from the X bookmarks tab", "Works via the Chrome extension", ]} onConnect={() => openExternal("bookmarks", CHROME_EXTENSION_URL) } /> } bareIconFrame state={values.connected.chatapps ?? "idle"} ctaLabel="Connect" doneLabel="Opened" perks={[ "Sync your ChatGPT memories", "Carry context across every assistant", "Import once, recall anywhere", ]} onConnect={() => openExternal("chatapps", CHROME_EXTENSION_URL) } /> ) : ( <> setGranolaOpen(true)} /> )}
{moreOpen ? (
setGranolaOpen(true)} />
) : null}
{moreOpen &&
} { announced.current.add("granola") markConnected(["granola"]) }} />
) } function OnboardingPlansModal({ open, onOpenChange, requestedConnector, requestedPlan, }: { open: boolean onOpenChange: (open: boolean) => void requestedConnector: string requestedPlan: RequiredPlan }) { const autumn = useCustomer() const promoCode = usePromoCode() const { currentPlan, isLoading } = useTokenUsage(autumn) const [upgradingPlan, setUpgradingPlan] = useState( null, ) const [planIndex, setPlanIndex] = useState(0) const lastPlanIndex = Math.max(0, PLAN_CARDS.length - 2) useEffect(() => { if (open) { setPlanIndex(0) } }, [open]) const handleUpgrade = async (planId: CheckoutPlanId) => { setUpgradingPlan(planId) try { const result = await autumn.attach({ planId, discounts: promoCode.getDiscounts(), successUrl: window.location.href, }) promoCode.clear() if ((result as { paymentUrl?: string })?.paymentUrl) { window.location.href = (result as { paymentUrl: string }).paymentUrl return } autumn.refetch?.() } catch (error) { console.error(error) toast.error("Failed to start checkout. Please try again.") } finally { setUpgradingPlan(null) } } const scrollPlans = (direction: -1 | 1) => { setPlanIndex((index) => Math.min(lastPlanIndex, Math.max(0, index + direction)), ) } return (

Choose a plan

{requestedConnector} requires{" "} {PLAN_LABELS[requestedPlan]} .

{PLAN_CARDS.map((plan) => { const isCurrent = currentPlan === plan.id const isIncluded = !isCurrent && PLAN_RANK[currentPlan] > PLAN_RANK[plan.id] const isBusy = upgradingPlan === plan.productId const checkoutPlanId = plan.productId === "api_pro" || plan.productId === "api_max" || plan.productId === "api_scale" ? plan.productId : null return (
Book a call ) : ( ) } />
) })}
) } function OnboardingPlanCard({ action, plan, }: { action: React.ReactNode plan: PlanCardDefinition }) { return (
{plan.mostPopular ? ( Most popular ) : null}

{plan.name}

{plan.price} {plan.period ? ( {plan.period} ) : null}

{plan.description}

{plan.isContactSales ? null : (

{plan.credits}

of usage included

)} {plan.includesFrom ? (
Everything in {plan.includesFrom}, plus
) : null}
    {plan.features.map((feature) => (
  • {feature}
  • ))}
{action}
) } function SourceActions({ connectedCount, onContinue, className, }: { connectedCount: number onContinue: () => void className?: string }) { return (
) } function GoogleDriveSourceCard({ mode, values, onChange, isLocked, guard, connectRealProvider, }: { mode: BrainMode values: SourcesValues onChange: (next: SourcesValues) => void isLocked: (plan?: RequiredPlan) => boolean guard: ( plan: RequiredPlan | undefined, title: string, fn: () => void, provider?: string, ) => () => void connectRealProvider: ( provider: "google-drive" | "notion" | "onedrive", id: SourceId, ) => void }) { const pause = connectorPause("google-drive") return ( } state={values.connected.drive ?? "idle"} ctaLabel={pause ? "Notify me" : "Connect"} locked={isLocked("pro")} requiredPlan="pro" perks={[ "Docs, sheets, slides — all parsed", "Stays in sync as files change", "You pick what to share at sign-in", ]} onConnect={guard( "pro", "Google Drive", () => connectRealProvider("google-drive", "drive"), "google-drive", )} headerNote={ pause ? (

{pause.message}

) : values.driveScope === "full" ? (

Full Drive can exhaust your monthly usage.

) : null } footerLeft={ onChange({ ...values, driveScope: s })} /> } footerRight={ } /> ) } function NotionSourceCard({ mode, values, isLocked, guard, connectRealProvider, }: { mode: BrainMode values: SourcesValues isLocked: (plan?: RequiredPlan) => boolean guard: ( plan: RequiredPlan | undefined, title: string, fn: () => void, provider?: string, ) => () => void connectRealProvider: ( provider: "google-drive" | "notion" | "onedrive", id: SourceId, ) => void }) { return ( } state={values.connected.notion ?? "idle"} ctaLabel="Connect" locked={isLocked("pro")} requiredPlan="pro" perks={[ "Pages and database rows", "Stays in sync when you edit", "Pick which workspaces ingest", ]} onConnect={guard("pro", "Notion", () => connectRealProvider("notion", "notion"), )} footerRight={ } /> ) } function GranolaSourceCard({ state, isLocked, guard, onOpen, }: { state: SourceState isLocked: (plan?: RequiredPlan) => boolean guard: ( plan: RequiredPlan | undefined, title: string, fn: () => void, provider?: string, ) => () => void onOpen: () => void }) { return ( } state={state} ctaLabel="Connect" locked={isLocked("pro")} requiredPlan="pro" perks={[ "Meeting notes auto-captured", "Decisions and action items extracted", "Synced after every meeting", ]} onConnect={guard("pro", "Granola", () => { analytics.onboardingIntegrationClicked({ integration: "granola" }) onOpen() })} /> ) } function MoreSourcesGrid({ mode, values, onChange, isLocked, guard, openExternal, requestWaitlist, connectRealProvider, onOpenGranola, }: { mode: BrainMode values: SourcesValues onChange: (next: SourcesValues) => void isLocked: (plan?: RequiredPlan) => boolean guard: ( plan: RequiredPlan | undefined, title: string, fn: () => void, provider?: string, ) => () => void openExternal: (id: SourceId, url: string) => void requestWaitlist: (id: SourceId) => void connectRealProvider: ( provider: "google-drive" | "notion" | "onedrive", id: SourceId, ) => void onOpenGranola: () => void }) { return ( <> } state={values.connected.raycast ?? "idle"} ctaLabel="Connect" doneLabel="Opened" perks={[ "Add memories without leaving Raycast", "Search your brain from anywhere", "Fast keyboard-first capture", ]} onConnect={() => openExternal("raycast", RAYCAST_EXTENSION_URL)} /> } state={values.connected.shortcuts ?? "idle"} ctaLabel="Connect" doneLabel="Opened" perks={[ "Save from the iOS share sheet", "Capture text, links and photos", "Works on iPhone, iPad and Mac", ]} onConnect={() => openExternal("shortcuts", ADD_MEMORY_SHORTCUT_URL)} /> } state={values.connected.chrome ?? "idle"} ctaLabel="Connect" doneLabel="Opened" perks={[ "Save any page in one click", "Import your X bookmarks", "Sync ChatGPT memories", ]} onConnect={() => openExternal("chrome", CHROME_EXTENSION_URL)} /> {mode === "personal" ? ( ) : null} } state={values.connected.onedrive ?? "idle"} ctaLabel="Connect" locked={isLocked("pro")} requiredPlan="pro" perks={[ "Word, Excel, PowerPoint — all parsed", "Stays in sync as files change", "You pick what to share at sign-in", ]} onConnect={guard("pro", "OneDrive", () => connectRealProvider("onedrive", "onedrive"), )} /> } state={values.connected.gmail ?? "idle"} ctaLabel="Connect" locked={isLocked("max")} requiredPlan="max" perks={[ "Threads become searchable context", "Decisions and follow-ups surfaced", "You control which labels sync", ]} onConnect={guard("max", "Gmail", () => requestWaitlist("gmail"))} /> } state={values.connected.github ?? "idle"} ctaLabel="Connect" locked={isLocked("scale")} requiredPlan="scale" perks={[ "PRs and issues parsed", "READMEs and docs indexed", "Stays in sync with new activity", ]} onConnect={guard("scale", "GitHub", () => requestWaitlist("github"))} /> {mode === "personal" ? ( ) : null} ) } function SourceCard({ title, blurb, icon, state, ctaLabel, doneLabel, perks, bareIconFrame, disabled, locked, requiredPlan, headerNote, footerLeft, footerRight, onConnect, }: { title: string blurb: string icon: React.ReactNode state: SourceState ctaLabel: string doneLabel?: string perks: string[] bareIconFrame?: boolean disabled?: boolean locked?: boolean requiredPlan?: RequiredPlan headerNote?: React.ReactNode footerLeft?: React.ReactNode footerRight?: React.ReactNode onConnect: () => void }) { const isDone = state === "connected" || state === "waitlist" return (
{icon}

{title}

{blurb}

{headerNote}
{isDone ? ( {state === "waitlist" ? "We'll email you" : (doneLabel ?? "Connected")} ) : ( )}
    {perks.map((p) => (
  • {p}
  • ))}
{(footerLeft || footerRight) && (
{footerLeft}
{footerRight}
)}
) } function SpaceChip({ name }: { name: string }) { return (
Saves to {name}
) } function DriveScopePicker({ value, onChange, }: { value: DriveScope onChange: (s: DriveScope) => void }) { return ( ) }