mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
feat(web): gate team onboarding behind company-brain-beta flag
This commit is contained in:
parent
293a774363
commit
db57cfd70b
4 changed files with 105 additions and 9 deletions
|
|
@ -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 ?? ""}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
14
apps/web/hooks/use-feature-flag.ts
Normal file
14
apps/web/hooks/use-feature-flag.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue