From 40d025e1785cbc93949530a3ce26b0d8384ed32b Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 12 Jun 2026 18:05:48 +0000 Subject: [PATCH 1/4] Fix onboarding source integrations and connector gating (#1105) Fixes the onboarding source-integrations step: corrects connector behavior, applies plan-based gating to connectors, and polishes the integrations layout. --- apps/web/app/(app)/onboarding/page.tsx | 1 + .../web/components/onboarding-brain/shell.tsx | 18 +- .../onboarding-brain/step-sources.tsx | 1241 +++++++++++++---- 3 files changed, 970 insertions(+), 290 deletions(-) diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 42bfce24b..64ee0299e 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -275,6 +275,7 @@ export default function BrainOnboardingPage() { -
-
{children}
+
+
+ {children} +
) diff --git a/apps/web/components/onboarding-brain/step-sources.tsx b/apps/web/components/onboarding-brain/step-sources.tsx index 6e77cab67..388fb2fd5 100644 --- a/apps/web/components/onboarding-brain/step-sources.tsx +++ b/apps/web/components/onboarding-brain/step-sources.tsx @@ -1,14 +1,10 @@ "use client" -import { useState } from "react" +import { useEffect, useState } from "react" +import Image from "next/image" import { Button } from "@ui/components/button" -import { - Drawer, - DrawerContent, - DrawerHeader, - DrawerTitle, -} from "@ui/components/drawer" -import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" +import { Dialog, DialogClose, DialogContent } from "@ui/components/dialog" +import { GoogleDrive, Granola, Notion, OneDrive } from "@ui/assets/icons" import { Logo } from "@ui/assets/Logo" import { Select, @@ -21,12 +17,16 @@ import { AlertTriangle, ArrowRight, Check, - Database, + ChevronLeft, + ChevronRight, + Coins, + ExternalLink, FolderOpen, Github, - Globe, - Mic, + LoaderIcon, + Lock, Plus, + X, } from "lucide-react" import { AppleShortcutsIcon, @@ -47,6 +47,19 @@ function XBookmarksIcon({ className }: { className?: string }) { ) } +function GrokIcon({ className }: { className?: string }) { + return ( + + ) +} + function GmailIcon({ className }: { className?: string }) { return ( = { + pro: "Pro", + max: "Max", +} + +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", + "All plugins (Claude Code, Cursor, Hermes...)", + "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 export interface SourcesValues { connected: Partial> @@ -95,6 +221,7 @@ export interface SourcesValues { interface Props { containerTag: string workspaceName: string + mode: BrainMode values: SourcesValues onChange: (next: SourcesValues) => void onContinue: () => void @@ -110,14 +237,69 @@ const inputBevelStyle = { "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, workspaceName, + mode, values, onChange, onContinue, }: Props) { const [moreOpen, setMoreOpen] = useState(false) + const [plansOpen, setPlansOpen] = useState(false) + const [requestedPlan, setRequestedPlan] = useState("pro") + const [requestedConnector, setRequestedConnector] = useState("This connector") + const autumn = useCustomer() + const hasPro = hasActivePlan(autumn.data?.subscriptions, "api_pro") + const hasMax = hasActivePlan(autumn.data?.subscriptions, "api_max") + const planLoading = autumn.isLoading + + useEffect(() => { + setMoreOpen(false) + }, []) + + const isLocked = (plan?: RequiredPlan) => { + if (!plan || planLoading) return false + return plan === "max" ? !hasMax : !hasPro + } const setState = (id: SourceId, state: SourceState) => { onChange({ ...values, connected: { ...values.connected, [id]: state } }) @@ -155,120 +337,734 @@ export function StepSources({ } } + const openExternal = (id: SourceId, url: string) => { + window.open(url, "_blank", "noopener,noreferrer") + setState(id, "connected") + } + + const guard = ( + plan: RequiredPlan | undefined, + title: string, + fn: () => void, + ) => { + return () => { + if (isLocked(plan) && plan) { + setRequestedPlan(plan) + setRequestedConnector(title) + setPlansOpen(true) + return + } + fn() + } + } + const connectedCount = Object.values(values.connected).filter( (s) => s === "connected" || s === "waitlist", ).length return ( -
-
-
-

+

+
+
+
+

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

+

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

+
+ +
+ +
+ } + 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)} + /> + {mode === "personal" ? ( + + ) : ( + )} - > - Connect your team's signals -

-

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

+
+ +
+
+ + +
+ + {moreOpen ? ( +
+ +
+ ) : null} +
- -
+ -
- } - state={values.connected.drive ?? "idle"} - ctaLabel="Connect" - perks={[ - "Docs, sheets, slides — all parsed", - "Stays in sync as files change", - "You pick what to share at sign-in", - ]} - onConnect={() => connectRealProvider("google-drive", "drive")} - headerNote={ - values.driveScope === "full" ? ( -

- - Full Drive can exhaust your monthly usage. -

- ) : null - } - footerLeft={ - onChange({ ...values, driveScope: s })} - /> - } - footerRight={} - /> - } - state={values.connected.notion ?? "idle"} - ctaLabel="Connect" - perks={[ - "Pages and database rows", - "Stays in sync when you edit", - "Pick which workspaces ingest", - ]} - onConnect={() => connectRealProvider("notion", "notion")} - footerRight={} - /> -
+ {moreOpen &&
} + +
+ ) +} -
- + + + + +
+
+

+ {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} +

+ +
+ - - More integrations - - (Gmail, GitHub, OneDrive, Granola…) + {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} - setMoreOpen(false)} - containerTag={containerTag} - connectRealProvider={connectRealProvider} - /> +
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + {action}
) } +function SourceActions({ + connectedCount, + onContinue, + className, +}: { + connectedCount: number + onContinue: () => void + className?: string +}) { + return ( +
+ + +
+ ) +} + +function GoogleDriveSourceCard({ + values, + onChange, + isLocked, + guard, + connectRealProvider, +}: { + values: SourcesValues + onChange: (next: SourcesValues) => void + isLocked: (plan?: RequiredPlan) => boolean + guard: ( + plan: RequiredPlan | undefined, + title: string, + fn: () => void, + ) => () => void + connectRealProvider: ( + provider: "google-drive" | "notion" | "onedrive", + id: SourceId, + ) => void +}) { + return ( + } + state={values.connected.drive ?? "idle"} + ctaLabel="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"), + )} + headerNote={ + values.driveScope === "full" ? ( +

+ + Full Drive can exhaust your monthly usage. +

+ ) : null + } + footerLeft={ + onChange({ ...values, driveScope: s })} + /> + } + footerRight={} + /> + ) +} + +function NotionSourceCard({ + values, + isLocked, + guard, + connectRealProvider, +}: { + values: SourcesValues + isLocked: (plan?: RequiredPlan) => boolean + guard: ( + plan: RequiredPlan | undefined, + title: string, + fn: () => void, + ) => () => 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 MoreSourcesGrid({ + mode, + values, + onChange, + isLocked, + guard, + setState, + openExternal, + connectRealProvider, +}: { + mode: BrainMode + values: SourcesValues + onChange: (next: SourcesValues) => void + isLocked: (plan?: RequiredPlan) => boolean + guard: ( + plan: RequiredPlan | undefined, + title: string, + fn: () => void, + ) => () => void + setState: (id: SourceId, state: SourceState) => void + openExternal: (id: SourceId, url: string) => void + connectRealProvider: ( + provider: "google-drive" | "notion" | "onedrive", + id: SourceId, + ) => 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" ? ( + + ) : ( + + )} + } + 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", () => setState("gmail", "waitlist"))} + /> + } + state={values.connected.github ?? "idle"} + ctaLabel="Connect" + locked={isLocked("max")} + requiredPlan="max" + perks={[ + "PRs and issues parsed", + "READMEs and docs indexed", + "Stays in sync with new activity", + ]} + onConnect={guard("max", "GitHub", () => setState("github", "waitlist"))} + /> + } + state="idle" + ctaLabel="Connect" + locked={isLocked("max")} + requiredPlan="max" + perks={[ + "Meeting notes auto-captured", + "Decisions and action items extracted", + "Synced after every meeting", + ]} + onConnect={guard("max", "Granola", () => + toast.info("Granola is coming soon."), + )} + /> + + ) +} + function RoutingChip({ workspaceName }: { workspaceName: string }) { return (
-
-
+
+
{icon}
-
-

{title}

-

- {blurb} -

- {headerNote} -
+
+
+

+ {title} +

+

+ {blurb} +

+ {headerNote}
{isDone ? ( - {state === "waitlist" ? "Requested" : "Connected"} + {state === "waitlist" ? "Requested" : (doneLabel ?? "Connected")} ) : ( )}
@@ -370,12 +1196,6 @@ function SourceCard({ ))} - {soft && !isDone && ( -

- OAuth lands shortly — request access and we'll auto-enable it. -

- )} - {(footerLeft || footerRight) && (
{footerLeft}
@@ -430,158 +1250,3 @@ function DriveScopePicker({ ) } - -function MoreDrawer({ - open, - onClose, - containerTag, - connectRealProvider, -}: { - open: boolean - onClose: () => void - containerTag: string - connectRealProvider: ( - provider: "google-drive" | "notion" | "onedrive", - id: SourceId, - ) => void -}) { - return ( - !o && onClose()}> - - - - More integrations - -

- Add any of these alongside your spotlight sources. Everything routes - to {containerTag}. -

-
-
- } - action="Request access" - soft - /> - } - action="Request access" - soft - /> - } - action="Connect" - onAction={() => connectRealProvider("onedrive", "onedrive")} - /> - } - action="Coming soon" - soft - /> - } - action="Connect" - soft - /> - } - action="Connect" - soft - /> - } - action="Install" - soft - /> - } - action="Install" - soft - /> - } - action="Install" - soft - /> - } - action="Import" - soft - /> -
-
-
- ) -} - -function MoreItem({ - title, - blurb, - icon, - action, - soft, - onAction, -}: { - title: string - blurb: string - icon: React.ReactNode - action: string - soft?: boolean - onAction?: () => void -}) { - return ( -
-
- {icon} -
-
-

- {title} -

-

- {blurb} -

-
- -
- ) -} From 81ae72224fcea448ebff4ae44dbacda7d69b35cb Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:51:37 +0000 Subject: [PATCH 2/4] feat(spaces): per-space entity context + edit-space modal (#1078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "What to remember" per-space context — set on create, on edit, and in the space profile; steers what gets extracted into memory - Edit-space modal (name + context) from the space pill's hover edit, mirroring the create modal - Manual saves respect a space's configured context instead of overwriting it with the generic default --- apps/web/components/add-space-modal.tsx | 118 +++++++++-- apps/web/components/edit-space-modal.tsx | 206 ++++++++++++++++++++ apps/web/components/header.tsx | 1 + apps/web/components/space-profile-panel.tsx | 117 +++++++++-- apps/web/components/space-selector.tsx | 198 ++++++++++++------- apps/web/hooks/use-document-mutations.ts | 30 ++- apps/web/hooks/use-space-context.ts | 115 +++++++++++ 7 files changed, 681 insertions(+), 104 deletions(-) create mode 100644 apps/web/components/edit-space-modal.tsx create mode 100644 apps/web/hooks/use-space-context.ts diff --git a/apps/web/components/add-space-modal.tsx b/apps/web/components/add-space-modal.tsx index 7c9f3cf3d..9f09da7cf 100644 --- a/apps/web/components/add-space-modal.tsx +++ b/apps/web/components/add-space-modal.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" -import { Dialog, DialogContent } from "@repo/ui/components/dialog" +import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog" import { cn } from "@lib/utils" import * as DialogPrimitive from "@radix-ui/react-dialog" import { XIcon, Loader2 } from "lucide-react" @@ -10,6 +10,7 @@ import { Button } from "@ui/components/button" import { useProjectMutations } from "@/hooks/use-project-mutations" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { analytics } from "@/lib/analytics" +import { $fetch } from "@lib/api" const EMOJI_LIST = [ "📁", @@ -62,6 +63,25 @@ const EMOJI_LIST = [ "🤍", ] +export const CONTEXT_PRESETS: { label: string; text: string }[] = [ + { + label: "Work project", + text: "Tracks a work project — decisions, owners, deadlines, and current status.", + }, + { + label: "Client", + text: "About a client — meetings, requirements, and account context.", + }, + { + label: "Research", + text: "Research notes — sources, key findings, and open questions.", + }, + { + label: "Personal", + text: "My personal space — notes, ideas, and things to remember.", + }, +] + export function AddSpaceModal({ isOpen, onClose, @@ -72,6 +92,8 @@ export function AddSpaceModal({ onCreated?: (containerTag: string) => void }) { const [spaceName, setSpaceName] = useState("") + const [spaceContext, setSpaceContext] = useState("") + const [showContext, setShowContext] = useState(false) const [emoji, setEmoji] = useState("📁") const [isEmojiOpen, setIsEmojiOpen] = useState(false) const { createProjectMutation } = useProjectMutations() @@ -79,6 +101,8 @@ export function AddSpaceModal({ const handleClose = () => { onClose() setSpaceName("") + setSpaceContext("") + setShowContext(false) setEmoji("📁") } @@ -89,11 +113,18 @@ export function AddSpaceModal({ createProjectMutation.mutate( { name: trimmedName, emoji: emoji || undefined }, { - onSuccess: (data) => { + onSuccess: async (data) => { analytics.spaceCreated() - if (data?.containerTag) { - onCreated?.(data.containerTag) + const tag = data?.containerTag + const context = showContext ? spaceContext.trim() : "" + if (tag && context) { + try { + await $fetch(`@patch/container-tags/${tag}`, { + body: { entityContext: context }, + }) + } catch {} } + if (tag) onCreated?.(tag) handleClose() }, }, @@ -132,21 +163,21 @@ export function AddSpaceModal({
-

- Create new space -

+ New space +

- Create spaces to organize your memories and documents and create - a context rich environment + Group related memories and give Nova context for this space.

+ {!showContext ? ( + + ) : ( +
+
+ + What to remember + + + Optional + +
+