feat(web): gate team onboarding behind company-brain-beta flag

This commit is contained in:
Mahesh Sanikommu 2026-06-20 02:13:57 -07:00
parent 293a774363
commit db57cfd70b
4 changed files with 105 additions and 9 deletions

View file

@ -15,6 +15,7 @@ import {
type SourcesValues,
} from "@/components/onboarding-brain/step-sources"
import { StepIngest } from "@/components/onboarding-brain/step-ingest"
import { useFeatureFlag } from "@/hooks/use-feature-flag"
import {
StepTeam,
type TeamValues,
@ -64,6 +65,8 @@ export default function BrainOnboardingPage() {
[user?.email],
)
// Team (Company Brain) onboarding is gated behind a private-beta flag.
const allowTeam = useFeatureFlag("company-brain-beta")
const [mode, setMode] = useState<BrainMode>(detectedMode)
const [about, setAbout] = useState<AboutValues>({
name: user?.name ?? "",
@ -132,6 +135,13 @@ export default function BrainOnboardingPage() {
return plan === "scale" || plan === "scale_yearly"
}, [org])
// Personal onboarding has no team step — drop it from the flow + stepper.
const steps = useMemo<BrainStep[]>(
() =>
mode === "team" ? BRAIN_STEPS : BRAIN_STEPS.filter((s) => s !== "team"),
[mode],
)
const finish = useCallback(async () => {
try {
localStorage.removeItem(STORAGE_KEY)
@ -140,14 +150,22 @@ export default function BrainOnboardingPage() {
}, [router])
const goNext = useCallback(() => {
const idx = BRAIN_STEPS.indexOf(step)
const next = BRAIN_STEPS[idx + 1]
const idx = steps.indexOf(step)
const next = steps[idx + 1]
if (!next) {
finish()
return
}
setStepAndUrl(next)
}, [step, setStepAndUrl, finish])
}, [step, steps, setStepAndUrl, finish])
// If the current step isn't valid for the mode (e.g. switched to personal),
// fall back to the last valid step.
useEffect(() => {
if (!steps.includes(step)) {
setStepAndUrl(steps[steps.length - 1] ?? "about")
}
}, [steps, step, setStepAndUrl])
const [creatingOrg, setCreatingOrg] = useState(false)
const creatingOrgRef = useRef(false)
@ -156,13 +174,14 @@ export default function BrainOnboardingPage() {
if (!forceCreate && organizations && organizations.length > 0) return
const name = (about.workspaceName || suggestedWorkspaceName).trim()
const slug = generateOrgSlug(name)
const effectiveMode = allowTeam ? mode : "personal"
const metadata: BrainMetadata & { signupSource: string } = {
signupSource: "consumer",
brainOnboardingVersion: "v1",
brainMode: mode,
brainMode: effectiveMode,
brainWorkspaceName: name,
brainWorkspaceDomain:
mode === "team" ? about.workspaceDomain || domain : null,
effectiveMode === "team" ? about.workspaceDomain || domain : null,
brainContainerTag: containerTag,
...(about.about.trim() ? { brainAbout: about.about.trim() } : {}),
}
@ -185,6 +204,7 @@ export default function BrainOnboardingPage() {
about,
suggestedWorkspaceName,
mode,
allowTeam,
domain,
containerTag,
setActiveOrg,
@ -261,12 +281,14 @@ export default function BrainOnboardingPage() {
return (
<BrainShell
step={step}
steps={steps}
domain={mode === "team" ? about.workspaceDomain || domain : null}
>
{step === "about" && (
<StepAbout
mode={mode}
onModeChange={setMode}
allowTeam={allowTeam}
domain={domain}
suggestedWorkspaceName={suggestedWorkspaceName}
defaultName={user?.name ?? ""}

View file

@ -9,11 +9,12 @@ import { BRAIN_STEPS, BRAIN_STEP_LABELS, type BrainStep } from "./types"
interface ShellProps {
step: BrainStep
domain?: string | null
steps?: BrainStep[]
children: React.ReactNode
}
export function BrainShell({ step, children }: ShellProps) {
const visibleSteps: BrainStep[] = BRAIN_STEPS
export function BrainShell({ step, steps, children }: ShellProps) {
const visibleSteps: BrainStep[] = steps ?? BRAIN_STEPS
return (
<div

View file

@ -11,6 +11,7 @@ import {
Building2,
LayoutGrid,
Loader2,
Mail,
Plug,
Terminal,
User2,
@ -31,6 +32,7 @@ export interface AboutValues {
interface Props {
mode: BrainMode
onModeChange: (m: BrainMode) => void
allowTeam: boolean
domain: string | null
suggestedWorkspaceName: string
defaultName: string
@ -58,6 +60,7 @@ const inputClass =
export function StepAbout({
mode,
onModeChange,
allowTeam,
domain,
suggestedWorkspaceName,
defaultName,
@ -80,8 +83,11 @@ export function StepAbout({
if (Object.keys(patch).length > 0) onChange({ ...values, ...patch })
}, [defaultName, suggestedWorkspaceName, domain])
const teamGated = mode === "team" && !allowTeam
const canContinue =
values.name.trim().length > 0 && values.workspaceName.trim().length > 0
!teamGated &&
values.name.trim().length > 0 &&
values.workspaceName.trim().length > 0
return (
<div className="space-y-5">
@ -146,7 +152,9 @@ export function StepAbout({
<ModeToggle mode={mode} onChange={onModeChange} />
<div className="mt-6">
{mode === "team" ? (
{teamGated ? (
<TeamBetaGate onUsePersonal={() => onModeChange("personal")} />
) : mode === "team" ? (
<TeamWorkspaceCard
domain={values.workspaceDomain || domain || ""}
onDomainChange={(d) =>
@ -358,6 +366,57 @@ function PersonalWorkspaceCard({
)
}
function TeamBetaGate({ onUsePersonal }: { onUsePersonal: () => void }) {
return (
<div
className="relative overflow-hidden rounded-[14px] bg-[#1B1F24] p-5 md:p-6"
style={cardSurfaceStyle}
>
<div
aria-hidden
className="absolute -top-px left-0 right-0 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.3), transparent)",
}}
/>
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[#4BA0FA]">
Private beta
</p>
<p
className={cn(
"mt-1.5 text-[15px] font-semibold leading-snug text-[#fafafa]",
dmSans125ClassName(),
)}
>
Team workspaces are invite-only
</p>
<p className="mt-1.5 text-[13px] leading-relaxed text-[#737373]">
We're onboarding teams to Company Brain one at a time. Email us for
access — or start with a personal workspace and invite your team later.
</p>
<div className="mt-4 flex w-full flex-col items-center gap-2.5">
<a
href="mailto:support@supermemory.com?subject=Company%20Brain%20beta%20access"
className="group inline-flex items-center gap-2 rounded-[10px] border border-[rgba(82,89,102,0.2)] bg-[#14161A] px-3.5 py-2 text-[13px] font-medium text-[#fafafa] transition-colors hover:border-[rgba(75,160,250,0.4)]"
>
<Mail className="size-3.5 text-[#4BA0FA]" />
support@supermemory.com
<ArrowRight className="size-3.5 text-[#525D6E] transition-colors group-hover:text-[#fafafa]" />
</a>
<button
type="button"
onClick={onUsePersonal}
className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]"
>
Start with a personal workspace
<ArrowRight className="size-3.5" />
</button>
</div>
</div>
)
}
function ModeToggle({
mode,
onChange,

View file

@ -0,0 +1,14 @@
import posthog from "posthog-js"
import { useEffect, useState } from "react"
// Reactive PostHog boolean flag via the global singleton (the app uses a custom
// provider, so posthog-js/react hooks have no context). Unloaded/undefined = false.
export function useFeatureFlag(key: string): boolean {
const [enabled, setEnabled] = useState(false)
useEffect(() => {
const sync = () => setEnabled(posthog.isFeatureEnabled?.(key) === true)
sync()
return posthog.onFeatureFlags(() => sync())
}, [key])
return enabled
}