mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
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:
parent
3f7b9667c6
commit
9652478093
10 changed files with 311 additions and 27 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
}
|
||||
|
|
|
|||
178
apps/web/components/brain-setup-modal.tsx
Normal file
178
apps/web/components/brain-setup-modal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
32
apps/web/components/onboarding-brain/setup-call-button.tsx
Normal file
32
apps/web/components/onboarding-brain/setup-call-button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
45
apps/web/components/setup-call-link.tsx
Normal file
45
apps/web/components/setup-call-link.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
8
apps/web/lib/cal.ts
Normal 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"
|
||||
Loading…
Add table
Reference in a new issue