mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-05 08:06:19 +00:00
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:
parent
d436792e77
commit
9ccd1b64c3
15 changed files with 80 additions and 853 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 ?? ""}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue