chore(web): clean up onboarding and dashboard surfaces (#1614)

Removes stale promo cards, banners, and the setup modal. Simplifies the onboarding entry so all new workspaces go through one flow.
This commit is contained in:
MaheshtheDev 2026-08-29 04:57:09 +00:00
parent d436792e77
commit 9ccd1b64c3
15 changed files with 80 additions and 853 deletions

View file

@ -5,24 +5,14 @@ import { useRouter } from "next/navigation"
import { LogoFull } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
import { AlertTriangle, ChevronRight, Loader2, RotateCw } from "lucide-react"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { SHARED_TEAM_BRAIN_TAG } from "@lib/constants"
import { cn } from "@lib/utils"
import { analytics } from "@/lib/analytics"
import {
type BrainEntryOrganization,
resolveCompanyBrainEntry,
} from "@/lib/company-brain-entry"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import {
detectModeFromEmail,
generateOrgSlug,
workspaceDomainFromEmail,
workspaceNameFromDomain,
workspaceNameFromEmail,
type BrainMetadata,
} from "@/components/onboarding-brain/types"
import { generateOrgSlug } from "@/components/onboarding-brain/types"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
@ -37,21 +27,13 @@ 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)",
}
// No forms: sign up → org auto-created → Slack install.
// After OAuth, mono attaches api_scale (14d trial) + company_brain (200 credits).
export default function BrainEntryPage() {
const router = useRouter()
const {
user,
org,
organizations,
isRestoring,
setActiveOrg,
refetchOrganizations,
} = useAuth()
const { user, org, organizations, isRestoring, setActiveOrg } = useAuth()
const { email = null } = user ?? {}
const [error, setError] = useState<string | null>(null)
const [choices, setChoices] = useState<BrainEntryOrganization[] | null>(null)
const [closed, setClosed] = useState(false)
const [attempt, setAttempt] = useState(0)
const startedRef = useRef(false)
@ -75,43 +57,6 @@ export default function BrainEntryPage() {
[org?.id, router, setActiveOrg],
)
const createCompanyBrain = useCallback(async () => {
// Personal email → shell org; the Slack workspace resolves identity later.
const domain =
detectModeFromEmail(email) === "team"
? workspaceDomainFromEmail(email)
: null
const name =
(domain
? workspaceNameFromDomain(domain)
: workspaceNameFromEmail(email)) || "Company Brain"
const metadata: BrainMetadata & { signupSource: string } = {
signupSource: "consumer",
brainOnboardingVersion: "v1",
brainMode: "team",
brainWorkspaceName: name,
brainWorkspaceDomain: domain,
// Always the shared Team Brain; the CB UI never selects a slug space.
brainContainerTag: SHARED_TEAM_BRAIN_TAG,
}
const result = await authClient.organization.create({
name,
slug: generateOrgSlug(name),
metadata,
})
if (result.error || !result.data?.slug) {
throw new Error(result.error?.message || "Could not create workspace.")
}
await setActiveOrg(result.data.slug)
await refetchOrganizations()
analytics.onboardingWorkspaceCreated({
mode: "team",
has_about: false,
has_domain: Boolean(domain),
})
window.location.href = `${BACKEND}/brain/slack/oauth/install`
}, [email, refetchOrganizations, setActiveOrg])
const run = useCallback(async () => {
const organizationsWithActiveMetadata = (organizations ?? []).map(
(organization) =>
@ -132,8 +77,8 @@ export default function BrainEntryPage() {
setChoices(decision.organizations)
return
}
await createCompanyBrain()
}, [continueWithOrganization, createCompanyBrain, org, organizations])
setClosed(true)
}, [continueWithOrganization, org, organizations])
const handleChoice = useCallback(
(organization: BrainEntryOrganization) => {
@ -164,7 +109,32 @@ export default function BrainEntryPage() {
return (
<EntryShell>
{choices ? (
{closed ? (
<section
className="w-full max-w-md rounded-[22px] bg-[#1B1F24] p-8 text-center"
style={modalCardStyle}
>
<p
className={cn(
"text-[20px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
New signups are paused
</p>
<p className="mt-2 text-[14px] font-medium leading-[1.5] text-[#737373]">
Company Brain isn't accepting new workspaces right now. If you have
questions, reach us at support@supermemory.com.
</p>
<Button
variant="insideOut"
onClick={() => router.replace("/")}
className="mt-6 rounded-full px-5 py-[10px] text-[13px] font-medium text-[#fafafa]"
>
Go to Supermemory
</Button>
</section>
) : choices ? (
<section
className="w-full max-w-md rounded-[22px] bg-[#1B1F24] p-6 text-left md:p-8"
style={modalCardStyle}

View file

@ -8,6 +8,7 @@ import { useAuth } from "@lib/auth-context"
import { authClient } from "@lib/auth"
import { SHARED_TEAM_BRAIN_TAG } from "@lib/constants"
import { analytics } from "@/lib/analytics"
import { hasCompanyBrain } from "@/lib/billing-utils"
import { resolveCompanyBrainEntry } from "@/lib/company-brain-entry"
import { BrainShell } from "@/components/onboarding-brain/shell"
import {
@ -107,6 +108,22 @@ export default function BrainOnboardingPage() {
[user?.email],
)
const [mode, setMode] = useState<BrainMode>(detectedMode)
const hasCompanyBrainOrg = useMemo(
() =>
(organizations ?? []).some((o) =>
hasCompanyBrain(
(o as { metadata?: Record<string, unknown> | string | null })
.metadata,
),
),
[organizations],
)
useEffect(() => {
if (mode === "team" && organizations != null && !hasCompanyBrainOrg) {
setMode("personal")
}
}, [mode, organizations, hasCompanyBrainOrg])
const [about, setAbout] = useState<AboutValues>({
name: user?.name ?? "",
about: "",
@ -280,12 +297,9 @@ export default function BrainOnboardingPage() {
const metadata: BrainMetadata & { signupSource: string } = {
signupSource: "consumer",
brainOnboardingVersion: "v1",
brainMode: mode,
brainMode: "personal",
brainWorkspaceName: name,
brainWorkspaceDomain:
mode === "team"
? domainOverride || about.workspaceDomain || domain
: null,
brainWorkspaceDomain: null,
brainContainerTag: containerTag,
...(about.about.trim() ? { brainAbout: about.about.trim() } : {}),
}
@ -368,7 +382,7 @@ export default function BrainOnboardingPage() {
setCreatingOrg(false)
}
}, [ensureOrg, goNext, forceCreate, organizations, router])
const isCompanyBrain = mode === "team"
const isCompanyBrain = mode === "team" && hasCompanyBrainOrg
const handleBrainConfirm = useCallback(
async (
@ -385,10 +399,17 @@ export default function BrainOnboardingPage() {
workspaceDomain: confirmedDomain,
workspaceName: workspaceName || a.workspaceName,
}))
let orgCreated = false
const signupsPaused = () => {
toast.error("New Company Brain signups are paused", {
description: "Questions? Reach us at support@supermemory.com.",
})
return { ok: false as const }
}
const orgCreated = false
if (forceCreate) {
orgCreated = await ensureOrg(confirmedDomain, true)
} else if (organizationId) {
return signupsPaused()
}
if (organizationId) {
const selected = organizations?.find(
(organization) => organization.id === organizationId,
)
@ -418,7 +439,7 @@ export default function BrainOnboardingPage() {
if (decision.action === "switch") {
await setActiveOrg(decision.organization.slug)
} else if (decision.action === "create") {
orgCreated = await ensureOrg(confirmedDomain, true)
return signupsPaused()
}
}
// Re-entering onboarding on an existing org ("Try onboarding") must
@ -475,15 +496,7 @@ export default function BrainOnboardingPage() {
setCreatingOrg(false)
}
},
[
ensureOrg,
forceCreate,
org,
organizations,
queryClient,
setActiveOrg,
router,
],
[forceCreate, org, organizations, queryClient, setActiveOrg, router],
)
const [sendingInvites, setSendingInvites] = useState(false)
@ -541,6 +554,10 @@ export default function BrainOnboardingPage() {
const mcpUrl = "https://mcp.supermemory.ai/mcp"
if (mode === "team" && organizations == null) {
return null
}
if (isCompanyBrain) {
return (
<CompanyBrainOnboarding
@ -565,10 +582,6 @@ export default function BrainOnboardingPage() {
{step === "about" && (
<StepAbout
mode={mode}
onModeChange={(m) => {
analytics.onboardingModeSelected({ mode: m })
setMode(m)
}}
domain={domain}
suggestedWorkspaceName={suggestedWorkspaceName}
defaultName={user?.name ?? ""}

View file

@ -16,7 +16,6 @@ import { ChatSidebar, HomeChatComposer } from "@/components/chat"
import type { ChatAttachmentDraft } from "@/components/chat/attachments"
import { DashboardView } from "@/components/dashboard-view"
import { BrainHomeView } from "@/components/brain-home/brain-home-view"
import { CompanyBrainPromo } from "@/components/company-brain-promo"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { MemoriesGrid } from "@/components/memories-grid"
import { GraphLayoutView } from "@/components/graph-layout-view"
@ -830,7 +829,6 @@ export function AppExperience() {
) : (
<DashboardView
spaceLabel={dashboardSpaceLabel}
headerNotice={<CompanyBrainPromo />}
highlights={highlightsData?.highlights ?? []}
isLoadingHighlights={isLoadingHighlights}
onAddMemory={handleAddMemory}

View file

@ -8,8 +8,6 @@ import { ArrowRight, Check, FileText, Loader2, UserPlus } from "lucide-react"
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"
@ -17,7 +15,6 @@ import {
AskInSlackCard,
CONNECT_TOOLS_CARD_ID,
ConnectToolsCard,
SlackBanner,
useConnectionsBoard,
} from "./connections-board"
@ -173,8 +170,6 @@ export function BrainHomeView() {
const o = useBrainOverview()
const trial = useBrainTrial()
const board = useConnectionsBoard()
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"] : []),
@ -186,16 +181,11 @@ 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)
return (
<div className="mx-auto max-w-[1080px] space-y-6">
<TrialSetupBanner />
<StatsRow
memories={o.memoriesCount}
connected={o.connectedCount}
@ -205,7 +195,6 @@ export function BrainHomeView() {
setupTotal={milestonesTotal}
lastUpdatedAt={o.lastUpdatedAt}
/>
<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} />}
@ -227,7 +216,6 @@ export function BrainHomeView() {
<AskInSlackCard board={board} />
</div>
</div>
{showSetupPrompt && <SlackBanner />}
</div>
)
}

View file

@ -442,71 +442,3 @@ function TileSkeleton({ showDivider = false }: { showDivider?: boolean }) {
</div>
)
}
export function SlackBanner() {
const { needsSetup } = useTrialStatus()
return (
<section
className="relative overflow-hidden rounded-[18px] bg-[#1B1F24] p-3.5 sm:p-5"
style={cardStyle}
>
<div
aria-hidden
className="absolute -top-px right-8 left-8 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.45), transparent)",
}}
/>
<div className="flex items-center justify-between gap-3 sm:gap-4">
<div className="flex min-w-0 items-center gap-3 sm:gap-3.5">
<div
className="flex size-10 shrink-0 items-center justify-center rounded-[12px] border border-[rgba(82,89,102,0.2)] bg-[#080B0F] sm:size-12"
style={tileStyle}
>
<SlackMark className="size-6 sm:size-7" />
</div>
<div className="min-w-0">
<p
className={cn(
"truncate text-[15px] font-semibold leading-tight text-[#fafafa] sm:text-[16px]",
dmSans125ClassName(),
)}
>
<span className="sm:hidden">Slack agent</span>
<span className="hidden sm:inline">Company Brain in Slack</span>
</p>
<p className="mt-1 truncate text-[12px] font-medium leading-[1.45] text-[#737373] sm:mt-0.5 sm:text-[13px] sm:leading-[1.5]">
<span className="sm:hidden">
Ask <span className="text-[#A1A1AA]">@supermemory</span> from
any channel.
</span>
<span className="hidden sm:inline">
Install Supermemory so your team can{" "}
<span className="text-[#A1A1AA]">@supermemory</span> in any
channel.
</span>
</p>
</div>
</div>
<a
href={
needsSetup ? "/onboarding" : `${BACKEND}/brain/slack/oauth/install`
}
className="inline-flex shrink-0 items-center justify-center rounded-lg bg-white px-3 py-1.5 text-[13px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02] sm:gap-2 sm:px-4 sm:py-2.5 sm:text-[14px]"
>
{needsSetup ? (
<span>Start trial</span>
) : (
<>
<SlackMark className="hidden sm:block sm:size-[18px]" />
<span className="sm:hidden">Add</span>
<span className="hidden sm:inline">Add to Slack</span>
</>
)}
</a>
</div>
</section>
)
}

View file

@ -1,178 +0,0 @@
"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

@ -462,7 +462,7 @@ export function CompanyBrainHeader({
<Sun className="size-4 text-[#737373]" />
Integrations
</DropdownMenuItem>
{slackConnected ? (
{slackConnected && (
<DropdownMenuItem
onClick={goConfigure}
className={menuItemClass}
@ -470,13 +470,6 @@ export function CompanyBrainHeader({
<SlackMark className="size-4" />
Slack connected
</DropdownMenuItem>
) : (
<DropdownMenuItem asChild className={menuItemClass}>
<a href={`${BACKEND}/brain/slack/oauth/install`}>
<SlackMark className="size-4" />
Add to Slack
</a>
</DropdownMenuItem>
)}
<DropdownMenuSeparator className="bg-[#2E3033]" />
{canInvite && (
@ -582,28 +575,9 @@ function SlackNavButton({
active: boolean
onManage: () => void
}) {
const label = connected
? `Slack${teamName ? ` · ${teamName}` : ""}`
: "Add to Slack"
const label = `Slack${teamName ? ` · ${teamName}` : ""}`
if (!connected) {
return (
<Tooltip>
<TooltipTrigger asChild>
<a
href={`${BACKEND}/brain/slack/oauth/install`}
aria-label="Add to Slack"
className={circleNavClass(false)}
>
<SlackMark className="size-4" />
</a>
</TooltipTrigger>
<TooltipContent side="bottom" className={dmSansClassName()}>
Add to Slack
</TooltipContent>
</Tooltip>
)
}
if (!connected) return null
return (
<Tooltip>

View file

@ -1,91 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { ArrowRight, XIcon } from "lucide-react"
import { Logo } from "@ui/assets/Logo"
import { Button } from "@repo/ui/components/button"
import { cn } from "@lib/utils"
import { analytics } from "@/lib/analytics"
import { dmSansClassName } from "@/lib/fonts"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
const DISMISS_KEY = "supermemory-company-brain-promo-dismissed-v1"
export function CompanyBrainPromo() {
const router = useRouter()
const hasCompanyBrain = useHasCompanyBrain()
const [dismissed, setDismissed] = useState(true)
useEffect(() => {
if (hasCompanyBrain) return
try {
setDismissed(localStorage.getItem(DISMISS_KEY) === "1")
} catch {
setDismissed(false)
}
}, [hasCompanyBrain])
const visible = !hasCompanyBrain && !dismissed
useEffect(() => {
if (visible) analytics.companyBrainPromoSeen()
}, [visible])
if (!visible) return null
const dismiss = () => {
setDismissed(true)
try {
localStorage.setItem(DISMISS_KEY, "1")
} catch {}
analytics.companyBrainPromoDismissed()
}
return (
<div
className={cn(
"relative flex items-start gap-3 rounded-xl bg-surface-card/60 px-4 py-4 backdrop-blur-md sm:items-center sm:gap-4",
"shadow-[0_12px_40px_rgba(0,0,0,0.22)]",
dmSansClassName(),
)}
>
<div className="mt-1 flex size-10 shrink-0 items-center justify-center rounded-lg bg-[#0562ef] sm:mt-0">
<Logo className="h-4 w-5" />
</div>
<div className="flex min-w-0 flex-1 flex-col gap-2 sm:contents">
<div className="min-w-0 pt-1 pr-7 sm:flex-1 sm:pt-0 sm:pr-0">
<p className="text-[15px] font-semibold text-[#fafafa]">
Give your team a Company Brain
</p>
<p className="text-[11px] leading-snug text-[#a1a1a1] sm:text-[13px] sm:leading-normal">
Lives in your Slack. Answers from your team's tools, and brings
things up before you ask.
</p>
</div>
<Button
className={cn(
"h-9! min-h-9 w-fit shrink-0 self-end gap-1.5 rounded-full! px-3 font-medium sm:self-auto",
dmSansClassName(),
)}
onClick={() => {
analytics.companyBrainPromoClicked({ source: "dashboard_card" })
router.push("/onboarding?new=1&mode=team")
}}
variant="headers"
>
Set it up
<ArrowRight className="size-4 shrink-0" />
</Button>
</div>
<button
aria-label="Dismiss"
type="button"
onClick={dismiss}
className="absolute right-2.5 top-2.5 shrink-0 rounded-full p-1.5 text-[#737373] transition-colors hover:text-[#fafafa] sm:static"
>
<XIcon className="size-4" />
</button>
</div>
)
}

View file

@ -31,8 +31,6 @@ import {
import { StaticGraphPreview } from "@/components/memory-graph/graph-card"
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip"
import { ChromeIcon, RaycastIcon } from "@/components/integration-icons"
import { SlackConnectCard } from "@/components/slack-connect-card"
import { TrialSetupBanner } from "@/components/trial-setup-banner"
import { GoogleDrive, Notion, MCPIcon } from "@ui/assets/icons"
import { analytics } from "@/lib/analytics"
import type { IntegrationParamValue } from "@/lib/search-params"
@ -1345,8 +1343,6 @@ export function DashboardView({
)}
>
<div className="mx-auto w-full max-w-4xl space-y-4 md:space-y-5">
<TrialSetupBanner />
<SlackConnectCard />
{headerNotice ? <div className="space-y-2">{headerNotice}</div> : null}
{/* Header */}

View file

@ -33,7 +33,6 @@ export interface AboutValues {
interface Props {
mode: BrainMode
onModeChange: (m: BrainMode) => void
domain: string | null
suggestedWorkspaceName: string
defaultName: string
@ -60,7 +59,6 @@ export const inputClass =
export function StepAbout({
mode,
onModeChange,
domain,
suggestedWorkspaceName,
defaultName,
@ -165,9 +163,7 @@ export function StepAbout({
className="rounded-[22px] bg-[#1B1F24] p-6 md:p-8"
style={cardSurfaceStyle}
>
<ModeToggle mode={mode} onChange={onModeChange} />
<div className="mt-7 flex items-center gap-4">
<div className="flex items-center gap-4">
<UserAvatar
url={avatarUrl}
name={values.name || defaultName}
@ -407,53 +403,6 @@ export function DomainLogo({ domain }: { domain: string }) {
)
}
function ModeToggle({
mode,
onChange,
}: {
mode: BrainMode
onChange: (m: BrainMode) => void
}) {
const items: { id: BrainMode; label: string }[] = [
{ id: "personal", label: "Personal" },
{ id: "team", label: "Team" },
]
return (
<div
className="relative grid grid-cols-2 items-center rounded-full bg-[#0D121A] border border-[rgba(115,115,115,0.2)] p-1 text-[13px] font-medium w-full"
style={{
boxShadow: "inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
}}
>
<motion.span
aria-hidden
className="absolute inset-y-1 left-1 w-[calc(50%-4px)] rounded-full"
style={{ background: "#00173C", border: "1px solid #2261CA66" }}
animate={{ x: mode === "team" ? "100%" : "0%" }}
transition={{ type: "spring", stiffness: 420, damping: 38, mass: 0.8 }}
/>
{items.map((item) => {
const isActive = mode === item.id
return (
<button
key={item.id}
type="button"
onClick={() => onChange(item.id)}
className={cn(
"relative z-10 h-8 rounded-full text-center transition-colors duration-200",
isActive
? "text-[#fafafa]"
: "text-[#737373] hover:text-[#fafafa]",
)}
>
{item.label}
</button>
)
})}
</div>
)
}
export function UserAvatar({
url,
name,

View file

@ -2,36 +2,12 @@
import { Gmail, GoogleDrive, Granola, MCPIcon, Notion } from "@ui/assets/icons"
import { GradientLogo } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { ArrowRight, Loader2, ShieldCheck } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner"
import { SlackMark } from "@/components/brain-connector-icons"
import { analytics } from "@/lib/analytics"
import { dmSans125ClassName } from "@/lib/fonts"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
export const CHECKOUT_RETURN_PARAM = "brainTrial"
const TRIAL_DAYS = 14
/** The only reminder that lands before the charge; 15 and 17 are post-trial. */
const REMINDER_DAY = 12
const MONTHLY_PRICE = "$100"
function checkoutReturnUrl(): string {
const url = new URL(window.location.href)
url.searchParams.set(CHECKOUT_RETURN_PARAM, "complete")
return url.toString()
}
function dayOffset(days: number): string {
const at = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
return at.toLocaleDateString(undefined, { month: "short", day: "numeric" })
}
const ORBIT = [
{ key: "slack", r: 74, deg: 0, node: <SlackMark className="size-4" /> },
{ key: "gmail", r: 74, deg: 128, node: <Gmail className="size-4" /> },
@ -85,91 +61,11 @@ function BrainPanel() {
)
}
function TimelineRow({
date,
title,
value,
current,
}: {
date: string
title: string
value?: string
current?: boolean
}) {
return (
<li className="relative flex items-start gap-3 pl-[18px]">
<span
aria-hidden="true"
className={cn(
"absolute left-0 top-[5px] size-[7px] rounded-full",
current
? "bg-[#fafafa] ring-4 ring-[#fafafa]/10"
: "bg-[#2b3138] ring-1 ring-white/15",
)}
/>
<div className="flex min-w-0 flex-1 items-baseline justify-between gap-3">
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-[13px] font-medium text-[#fafafa]">{date}</span>
<span className="text-[12px] leading-snug text-[#8b8b8b]">
{title}
</span>
</div>
{value ? (
<span className="shrink-0 text-[14px] font-medium text-[#fafafa] tabular-nums">
{value}
</span>
) : null}
</div>
</li>
)
}
export function StepTrial({ onActive }: { onActive: () => void }) {
const [starting, setStarting] = useState(false)
const start = async () => {
if (starting) return
setStarting(true)
analytics.brainTrialCheckoutStarted()
try {
const res = await fetch(`${BACKEND}/brain/trial/start`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ successUrl: checkoutReturnUrl() }),
})
const data = (await res.json()) as {
checkoutUrl?: string | null
status?: string
error?: string
}
if (res.status === 409 || data.error === "trial_unavailable") {
throw new Error(
"This workspace has already used its free trial. Upgrade from billing to continue.",
)
}
if (!res.ok) throw new Error(data.error ?? "Couldn't start the trial.")
if (data.checkoutUrl) {
window.location.href = data.checkoutUrl
return
}
if (data.status === "already_active" || data.status === "attached") {
onActive()
return
}
throw new Error("Couldn't start the trial.")
} catch (error) {
console.error("Failed to start trial:", error)
toast.error(
error instanceof Error ? error.message : "Couldn't start the trial.",
)
setStarting(false)
}
}
// Trial checkout is disabled while new Company Brain signups are paused.
export function StepTrial(_props: { onActive: () => void }) {
return (
<div className="flex gap-6">
<div className="flex min-w-0 flex-1 flex-col gap-5">
<div className="flex min-w-0 flex-1 flex-col justify-center gap-5">
<div className="flex flex-col gap-1.5">
<h2
className={cn(
@ -177,58 +73,11 @@ export function StepTrial({ onActive }: { onActive: () => void }) {
"text-[22px] leading-tight font-medium text-[#fafafa]",
)}
>
Start your {TRIAL_DAYS}-day free trial
New signups are paused
</h2>
<p className="text-[13px] leading-relaxed text-[#8b8b8b]">
Add a payment method to start. You will not be charged today. We
will email you before your first payment.
</p>
</div>
<ol className="relative flex flex-col gap-5 py-1">
<span
aria-hidden="true"
className="absolute left-[3px] top-2.5 bottom-[22px] w-px bg-white/10"
/>
<TimelineRow
date="Today"
title="Full access to Company Brain"
value="$0"
current
/>
<TimelineRow
date={dayOffset(REMINDER_DAY)}
title="We email you before the charge"
/>
<TimelineRow
date={dayOffset(TRIAL_DAYS)}
title="Trial ends"
value={`${MONTHLY_PRICE}/mo`}
/>
</ol>
<div className="flex flex-col items-center gap-3">
<Button
variant="insideOut"
onClick={start}
disabled={starting}
className="w-full justify-center rounded-full px-5 py-[11px] text-[13px] font-medium text-[#fafafa]"
>
{starting ? (
<>
Opening checkout
<Loader2 className="size-3.5 animate-spin" />
</>
) : (
<>
Start free trial
<ArrowRight className="size-3.5" />
</>
)}
</Button>
<p className="flex items-center gap-1.5 text-[12px] text-[#737373]">
<ShieldCheck className="size-3.5" />
Secured by Stripe · Cancel in one click
Company Brain isn't accepting new workspaces right now. If you have
questions, reach us at support@supermemory.com.
</p>
</div>
</div>

View file

@ -1,120 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
type SlackStatus = { connected: boolean; teamName: string | null }
function SlackMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 122.8 122.8" className={className} aria-hidden="true">
<title>Slack</title>
<path
d="M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9z"
fill="#E01E5A"
/>
<path
d="M32.3 77.6c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z"
fill="#E01E5A"
/>
<path
d="M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2z"
fill="#36C5F0"
/>
<path
d="M45.2 32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z"
fill="#36C5F0"
/>
<path
d="M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2z"
fill="#2EB67D"
/>
<path
d="M90.5 45.2c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z"
fill="#2EB67D"
/>
<path
d="M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9z"
fill="#ECB22E"
/>
<path
d="M77.6 90.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z"
fill="#ECB22E"
/>
</svg>
)
}
export function SlackConnectCard() {
const isCompanyBrain = useHasCompanyBrain()
const [status, setStatus] = useState<SlackStatus | null>(null)
const [trialActive, setTrialActive] = useState(true)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!isCompanyBrain) return
let active = true
;(async () => {
try {
const [slackRes, trialRes] = await Promise.all([
fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }),
fetch(`${BACKEND}/brain/trial/status`, { credentials: "include" }),
])
if (!active) return
if (slackRes.ok) setStatus((await slackRes.json()) as SlackStatus)
if (trialRes.ok) {
const trial = (await trialRes.json()) as { active?: boolean }
setTrialActive(Boolean(trial.active))
}
} finally {
if (active) setLoading(false)
}
})()
return () => {
active = false
}
}, [isCompanyBrain])
if (!isCompanyBrain || loading) return null
const connected = status?.connected
return (
<div className="flex items-center justify-between gap-4 rounded-[14px] bg-[#191D24] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)] sm:px-5">
<div className="min-w-0">
<p className="text-sm font-semibold text-fg-primary">
Add Supermemory to your Slack
</p>
<p className="mt-0.5 truncate text-[12px] text-fg-muted">
{connected
? `Connected to ${status?.teamName ?? "your workspace"}.`
: "Answer from your company brain and act on connected apps — right inside Slack."}
</p>
</div>
{connected ? (
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-surface-skeleton px-3 py-1.5 text-[12px] font-medium text-fg-muted ring-1 ring-surface-border">
<span className="size-1.5 rounded-full bg-[#2EB67D]" />
Connected
</span>
) : trialActive ? (
<a
href={`${BACKEND}/brain/slack/oauth/install`}
className="inline-flex shrink-0 items-center gap-2 rounded-lg bg-white px-3.5 py-2 text-[13px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02]"
>
<SlackMark className="size-4" />
Add to Slack
</a>
) : (
<a
href="/onboarding"
className="inline-flex shrink-0 items-center gap-2 rounded-lg bg-white/10 px-3.5 py-2 text-[13px] font-semibold text-fg-primary ring-1 ring-surface-border transition-colors hover:bg-white/15"
>
Finish setting up
</a>
)}
</div>
)
}

View file

@ -1,41 +0,0 @@
"use client"
import { ArrowRight, CreditCard } from "lucide-react"
import Link from "next/link"
import { useTrialStatus } from "@/hooks/use-trial-status"
export function TrialSetupBanner() {
const { needsSetup, data } = useTrialStatus()
if (!needsSetup) return null
const endedTrial = data?.reason === "trial_ended"
return (
<div className="flex items-center justify-between gap-4 rounded-[14px] bg-[#191D24] px-4 py-3 ring-1 ring-[#4BA0FA]/20 sm:px-5">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-[#4BA0FA]/12">
<CreditCard className="size-4 text-[#4BA0FA]" />
</span>
<div className="min-w-0">
<p className="text-sm font-semibold text-fg-primary">
{endedTrial
? "Your Company Brain trial has ended"
: "Finish setting up Company Brain"}
</p>
<p className="mt-0.5 truncate text-[12px] text-fg-muted">
{endedTrial
? "Move to Max or Scale to switch the brain back on."
: "Add a card to start your 14-day trial. $0 today."}
</p>
</div>
</div>
<Link
href={endedTrial ? "/?settings=billing" : "/onboarding"}
className="inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-white px-3.5 py-2 text-[13px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02]"
>
{endedTrial ? "Upgrade" : "Add card"}
<ArrowRight className="size-3.5" />
</Link>
</div>
)
}

View file

@ -13,7 +13,6 @@ import {
import { authClient } from "@lib/auth"
import { useRouter } from "next/navigation"
import {
Brain,
CalendarClock,
LogOut,
Settings,
@ -168,18 +167,6 @@ export function UserProfileMenu({
<Settings className="size-4 text-[#737373]" />
Settings
</DropdownMenuItem>
{isCompanyBrain ? null : (
<DropdownMenuItem
onClick={() => {
analytics.companyBrainPromoClicked({ source: "profile_menu" })
router.push("/onboarding?new=1&mode=team")
}}
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"
>
<Brain className="size-4 text-[#737373]" />
Set up Company Brain
</DropdownMenuItem>
)}
{isCompanyBrain ? (
<DropdownMenuItem
onClick={() => void setViewMode("configure")}

View file

@ -1,4 +1,4 @@
import { getBrainWorkspaceDomain, isCompanyBrainOrg } from "./billing-utils"
import { getBrainWorkspaceDomain, hasCompanyBrain } from "./billing-utils"
export type BrainEntryOrganization = {
id: string
@ -13,11 +13,12 @@ export type CompanyBrainEntryDecision =
| { action: "choose"; organizations: BrainEntryOrganization[] }
| { action: "create" }
// Paid add-on or concierge override only; never-activated shell orgs fall to "create".
export function getCompanyBrainOrganizations(
organizations: BrainEntryOrganization[],
): BrainEntryOrganization[] {
return organizations.filter((organization) =>
isCompanyBrainOrg(organization.metadata),
hasCompanyBrain(organization.metadata),
)
}