feat(web): offer a setup call alongside Slack install (#1599)

- First-landing modal on the Company Brain home: book a 30-min setup call, or install Slack yourself. Dismissal stored in localStorage.
- Setup call also reachable from a header icon, the user menu, and the onboarding research rail.
- Drops the desktop Invite button from the header; invite stays in the stats row and mobile menu.
This commit is contained in:
MaheshtheDev 2026-08-26 07:43:48 +00:00
parent 3f7b9667c6
commit 9652478093
10 changed files with 311 additions and 27 deletions

View file

@ -9,6 +9,7 @@ import { useQueryState } from "nuqs"
import { useSettingsModal } from "@/components/settings/settings-modal"
import { useBrainTrial } from "@/hooks/use-brain-trial"
import { TrialSetupBanner } from "@/components/trial-setup-banner"
import { BrainSetupModal } from "@/components/brain-setup-modal"
import { useTrialStatus } from "@/hooks/use-trial-status"
import { dmSans125ClassName } from "@/lib/fonts"
import { useViewMode } from "@/lib/view-mode-context"
@ -172,7 +173,8 @@ export function BrainHomeView() {
const o = useBrainOverview()
const trial = useBrainTrial()
const board = useConnectionsBoard()
const { needsSetup } = useTrialStatus()
const { data: trialStatus } = useTrialStatus()
const { org: activeOrg } = useAuth()
// Rows with no reported state (older orgs, pre-Slack) don't count or render.
const milestones = [
...(o.researchStatus != null ? [o.researchStatus === "done"] : []),
@ -184,6 +186,10 @@ export function BrainHomeView() {
]
const milestonesDone = milestones.filter(Boolean).length
const milestonesTotal = milestones.length
// Positive gate: a slow trial request would otherwise prompt an expired org.
const showSetupPrompt = Boolean(
board.slack && !board.slack.connected && trialStatus?.active,
)
const showTimeline =
!o.loading && (trial.state !== "none" || milestonesDone < milestonesTotal)
@ -199,7 +205,7 @@ export function BrainHomeView() {
setupTotal={milestonesTotal}
lastUpdatedAt={o.lastUpdatedAt}
/>
{board.slack && !board.slack.connected && !needsSetup && <SlackBanner />}
<BrainSetupModal enabled={showSetupPrompt} orgId={activeOrg?.id} />
<div className="grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_340px]">
<div className="min-w-0 space-y-6">
{board.showBoard && <ConnectToolsCard board={board} />}
@ -221,6 +227,7 @@ export function BrainHomeView() {
<AskInSlackCard board={board} />
</div>
</div>
{showSetupPrompt && <SlackBanner />}
</div>
)
}

View file

@ -18,11 +18,11 @@ const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const MCP_BASE = `${BACKEND}/brain/mcp-connections`
const cardStyle = {
export const cardStyle = {
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 tileStyle = {
export const tileStyle = {
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)",
}

View file

@ -0,0 +1,178 @@
"use client"
import { Check } from "lucide-react"
import { useEffect, useState } from "react"
import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
import { cn } from "@lib/utils"
import { SlackMark } from "@/components/brain-connector-icons"
import { cardStyle, tileStyle } from "@/components/brain-home/connections-board"
import { analytics } from "@/lib/analytics"
import { COMPANY_BRAIN_CAL_HREF } from "@/lib/cal"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const dismissKey = (orgId: string) => `sm_brain_setup_modal_dismissed:${orgId}`
const CALL_FEATURES = [
"We install Slack with you, live",
"Connect Gmail, Notion, Linear and the rest",
"Wire up plugins and answer your questions",
]
export function BrainSetupModal({
enabled,
orgId,
}: {
enabled: boolean
orgId: string | undefined
}) {
const [open, setOpen] = useState(false)
useEffect(() => {
// Also closes if the org stops being eligible while the dialog is open.
if (!enabled || !orgId) {
setOpen(false)
return
}
let dismissed = false
try {
dismissed = localStorage.getItem(dismissKey(orgId)) === "1"
} catch {}
if (dismissed) return
setOpen(true)
analytics.brainSetupModalSeen()
}, [enabled, orgId])
const close = () => {
setOpen(false)
if (!orgId) return
try {
localStorage.setItem(dismissKey(orgId), "1")
} catch {}
}
return (
<Dialog open={open} onOpenChange={(next) => !next && close()}>
<DialogContent
showCloseButton={false}
className={cn(
"w-[94%]! max-w-[520px]! rounded-[22px] border border-white/[0.08] bg-[#1B1F24] p-5",
dmSansClassName(),
)}
style={cardStyle}
>
<DialogTitle className="sr-only">Get set up</DialogTitle>
<div>
<p
className={cn(
"text-[15px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Get set up
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
Your brain is ready. Let's get it working where your team is.
</p>
</div>
<div className="relative mt-4 overflow-hidden rounded-[14px] border border-[#2261CA66] bg-[#00173C] p-5 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<span className="absolute right-5 top-5 inline-flex h-[18px] items-center rounded-[3px] bg-[#4BA0FA] px-1.5 text-[10px] font-bold uppercase tracking-[0.36px] text-[#00171A]">
Fastest
</span>
<p className="font-mono text-[10px] font-medium uppercase tracking-[0.18em] text-[#7E9BC4]">
Setup call
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-2 text-[24px] font-bold leading-none tracking-[-0.34px] text-[#FAFAFA]",
)}
>
Do it with us
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-2 text-[13px] leading-snug text-[#C8D0DA]",
)}
>
30 minutes, live with our team. You leave with a working brain.
</p>
<ul className="mt-5 flex flex-col gap-3">
{CALL_FEATURES.map((feature) => (
<li
className="flex items-start gap-2 text-[13px] leading-snug text-[#C8D0DA]"
key={feature}
>
<Check className="mt-0.5 size-3.5 shrink-0 text-[#fafafa]" />
<span>{feature}</span>
</li>
))}
</ul>
<a
href={COMPANY_BRAIN_CAL_HREF}
target="_blank"
rel="noreferrer"
onClick={() => {
analytics.brainSetupCallClicked({ surface: "setup_modal" })
analytics.brainSetupModalPicked({ choice: "call" })
close()
}}
className={cn(
dmSans125ClassName(),
"mt-5 flex h-11 items-center justify-center rounded-[10px] bg-white text-[14px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.01]",
)}
>
Book a setup call
</a>
</div>
<div className="mt-3 overflow-hidden rounded-[12px] bg-[#14161A]">
<div className="flex min-h-[52px] items-center gap-3 px-3 py-2.5">
<div
className="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-[10px] border border-[rgba(82,89,102,0.2)] bg-[#080B0F]"
style={tileStyle}
>
<SlackMark className="size-5" />
</div>
<div className="min-w-0 flex-1">
<p className="text-[13px] font-semibold leading-none text-[#fafafa]">
Add to Slack
</p>
<p className="mt-1 truncate text-[11px] font-medium leading-none text-[#737373]">
Rather set it up yourself? Takes a minute.
</p>
</div>
<a
href={`${BACKEND}/brain/slack/oauth/install`}
onClick={() => {
analytics.brainSetupModalPicked({ choice: "slack" })
close()
}}
className={cn(
dmSans125ClassName(),
"flex w-[88px] shrink-0 items-center justify-center rounded-full bg-[#0D121A] px-3 py-1.5 text-[12px] font-medium text-[#fafafa] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80",
)}
>
Install
</a>
</div>
</div>
<button
type="button"
onClick={close}
className="mx-auto mt-4 block text-[12px] font-medium text-[#737373] transition-colors hover:text-[#A1A1AA]"
>
I'll do it later
</button>
</DialogContent>
</Dialog>
)
}

View file

@ -37,6 +37,7 @@ import { FeedbackModal } from "@/components/feedback-modal"
import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge"
import { SlackMark } from "@/components/brain-connector-icons"
import { BrainTrialPill } from "@/components/brain-trial-pill"
import { SetupCallLink } from "@/components/setup-call-link"
import { GraphIcon } from "@/components/integration-icons"
import { SpaceSelector } from "@/components/space-selector"
import { UserProfileMenu } from "@/components/user-profile-menu"
@ -538,29 +539,7 @@ export function CompanyBrainHeader({
</TooltipContent>
</Tooltip>
)}
{canInvite && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="headers"
className={cn(
"rounded-full! h-9! min-h-9 shrink-0",
"max-lg:w-9 max-lg:min-w-9 max-lg:justify-center max-lg:gap-0 max-lg:px-0",
"lg:min-w-0 lg:gap-1.5 lg:px-3 lg:font-medium",
dmSansClassName(),
)}
onClick={handleInvite}
aria-label="Invite teammates"
>
<UserPlus className="size-3.5 shrink-0 lg:size-4" />
<span className="max-lg:sr-only">Invite</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className={dmSansClassName()}>
Invite teammates
</TooltipContent>
</Tooltip>
)}
<SetupCallLink />
<Tooltip>
<TooltipTrigger asChild>
<Button

View file

@ -16,6 +16,7 @@ import {
import { useRouter } from "next/navigation"
import { useResearchStatus } from "@/hooks/use-research-status"
import { dmSans125ClassName } from "@/lib/fonts"
import { SetupCallButton } from "./setup-call-button"
import { cardSurfaceStyle, inputBevelStyle, inputClass } from "./step-about"
const BACKEND =
@ -480,6 +481,14 @@ export function ResearchActionRail({
})}
</ol>
)}
<div className="mt-6 border-t border-white/[0.06] pt-5">
<p className="mb-3 text-[12px] font-medium leading-[1.5] text-[#525D6E]">
Want us to wire it up live? Slack, connectors, plugins, and a
working walkthrough.
</p>
<SetupCallButton className="w-full" surface="research_rail" />
</div>
</div>
</div>
)

View file

@ -0,0 +1,32 @@
"use client"
import { cn } from "@lib/utils"
import { analytics } from "@/lib/analytics"
import { COMPANY_BRAIN_CAL_HREF, type SetupCallSurface } from "@/lib/cal"
import { dmSans125ClassName } from "@/lib/fonts"
export function SetupCallButton({
className,
surface,
children,
}: {
className?: string
surface: SetupCallSurface
children?: React.ReactNode
}) {
return (
<a
href={COMPANY_BRAIN_CAL_HREF}
target="_blank"
rel="noreferrer"
onClick={() => analytics.brainSetupCallClicked({ surface })}
className={cn(
dmSans125ClassName(),
"inline-flex items-center justify-center rounded-full border border-white/[0.08] bg-transparent px-4 py-2.5 text-[13px] font-medium text-[#E4E4E7] transition-colors hover:bg-white/[0.06] hover:text-[#FAFAFA]",
className,
)}
>
{children ?? "Set up Company Brain with us"}
</a>
)
}

View file

@ -0,0 +1,45 @@
"use client"
import { CalendarClock } from "lucide-react"
import { Button } from "@ui/components/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
import { cn } from "@lib/utils"
import { analytics } from "@/lib/analytics"
import { COMPANY_BRAIN_CAL_HREF } from "@/lib/cal"
import { useTrialStatus } from "@/hooks/use-trial-status"
import { dmSansClassName } from "@/lib/fonts"
export function SetupCallLink() {
const { data } = useTrialStatus()
if (!data?.active) return null
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
asChild
variant="headers"
className={cn(
"size-9! min-h-9 min-w-9 shrink-0 rounded-full! border-[#161F2C]/90 px-0! text-muted-foreground hover:text-foreground",
dmSansClassName(),
)}
aria-label="Book a setup call"
>
<a
href={COMPANY_BRAIN_CAL_HREF}
target="_blank"
rel="noreferrer"
onClick={() =>
analytics.brainSetupCallClicked({ surface: "header" })
}
>
<CalendarClock className="size-4 shrink-0" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className={dmSansClassName()}>
Book a setup call
</TooltipContent>
</Tooltip>
)
}

View file

@ -14,6 +14,7 @@ import { authClient } from "@lib/auth"
import { useRouter } from "next/navigation"
import {
Brain,
CalendarClock,
LogOut,
Settings,
Settings2,
@ -29,6 +30,7 @@ import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { useTokenUsage } from "@/hooks/use-token-usage"
import { useSettingsModal } from "@/components/settings/settings-modal"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { COMPANY_BRAIN_CAL_HREF } from "@/lib/cal"
import { useViewMode } from "@/lib/view-mode-context"
export function UserProfileMenu({
@ -213,6 +215,24 @@ export function UserProfileMenu({
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator className="mx-1 my-1.5 bg-white/[0.06]" />
{isCompanyBrain ? (
<DropdownMenuItem
asChild
className="gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-white/85 hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white cursor-pointer"
>
<a
href={COMPANY_BRAIN_CAL_HREF}
target="_blank"
rel="noreferrer"
onClick={() =>
analytics.brainSetupCallClicked({ surface: "user_menu" })
}
>
<CalendarClock className="size-4 text-[#737373]" />
Book a setup call
</a>
</DropdownMenuItem>
) : null}
<DropdownMenuItem
asChild
className="gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-white/85 hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white cursor-pointer"

View file

@ -1,5 +1,6 @@
import posthog from "posthog-js"
import type { BrainStep } from "@/components/onboarding-brain/types"
import type { SetupCallSurface } from "@/lib/cal"
const pendingEvents: Array<{
eventName: string
@ -276,4 +277,9 @@ export const analytics = {
brainTrialCheckoutStarted: () => safeCapture("brain_trial_checkout_started"),
brainTrialCheckoutAbandoned: () =>
safeCapture("brain_trial_checkout_abandoned"),
brainSetupCallClicked: (props: { surface: SetupCallSurface }) =>
safeCapture("brain_setup_call_clicked", props),
brainSetupModalSeen: () => safeCapture("brain_setup_modal_seen"),
brainSetupModalPicked: (props: { choice: "slack" | "call" }) =>
safeCapture("brain_setup_modal_picked", props),
}

8
apps/web/lib/cal.ts Normal file
View file

@ -0,0 +1,8 @@
export const COMPANY_BRAIN_CAL_HREF =
"https://cal.com/team/supermemory/company-brain"
export type SetupCallSurface =
| "research_rail"
| "setup_modal"
| "header"
| "user_menu"