feat(web): add Slack connect card to home + space selector polish (#1137)

Adds the 'Add Supermemory to your Slack' card (status + install) on the home dashboard, and removes the border on the space-selector trigger.
This commit is contained in:
MaheshtheDev 2026-06-22 18:20:09 +00:00
parent a6f7f346e2
commit f28e974609
13 changed files with 1816 additions and 417 deletions

View file

@ -16,6 +16,7 @@ import {
type SourcesValues,
} from "@/components/onboarding-brain/step-sources"
import { StepIngest } from "@/components/onboarding-brain/step-ingest"
import { useFeatureFlagEnabled } from "posthog-js/react"
import {
StepTeam,
type TeamValues,
@ -68,6 +69,8 @@ export default function BrainOnboardingPage() {
[user?.email],
)
// Team (Company Brain) onboarding is gated behind a private-beta flag.
const allowTeam = useFeatureFlagEnabled("company-brain-beta") ?? false
const [mode, setMode] = useState<BrainMode>(detectedMode)
const [about, setAbout] = useState<AboutValues>({
name: user?.name ?? "",
@ -168,6 +171,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 () => {
analytics.onboardingCompleted({
mode,
@ -189,15 +199,23 @@ export default function BrainOnboardingPage() {
}, [router, mode, sources, team, forceCreate])
const goNext = useCallback(() => {
const idx = BRAIN_STEPS.indexOf(step)
const idx = steps.indexOf(step)
analytics.onboardingStepCompleted({ step, index: idx })
const next = BRAIN_STEPS[idx + 1]
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)
@ -206,13 +224,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() } : {}),
}
@ -247,6 +266,7 @@ export default function BrainOnboardingPage() {
about,
suggestedWorkspaceName,
mode,
allowTeam,
domain,
containerTag,
setActiveOrg,
@ -332,6 +352,7 @@ export default function BrainOnboardingPage() {
return (
<BrainShell
step={step}
steps={steps}
domain={mode === "team" ? about.workspaceDomain || domain : null}
>
{step === "about" && (
@ -341,6 +362,7 @@ export default function BrainOnboardingPage() {
analytics.onboardingModeSelected({ mode: m })
setMode(m)
}}
allowTeam={allowTeam}
domain={domain}
suggestedWorkspaceName={suggestedWorkspaceName}
defaultName={user?.name ?? ""}
@ -361,7 +383,13 @@ export default function BrainOnboardingPage() {
onContinue={goNext}
/>
)}
{step === "ingest" && <StepIngest mcpUrl={mcpUrl} onContinue={goNext} />}
{step === "ingest" && (
<StepIngest
mode={allowTeam ? mode : "personal"}
mcpUrl={mcpUrl}
onContinue={goNext}
/>
)}
{step === "team" && (
<StepTeam
mode={mode}

View file

@ -15,6 +15,8 @@ import { MobileBottomNav } from "@/components/bottom-nav"
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 { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { MemoriesGrid } from "@/components/memories-grid"
import { GraphLayoutView } from "@/components/graph-layout-view"
import { IntegrationsView, DetailWrapper } from "@/components/integrations-view"
@ -130,6 +132,27 @@ export default function NewPage() {
: undefined
const { viewMode, setViewMode } = useViewMode()
const isCompanyBrain = useHasCompanyBrain()
// Slack OAuth redirects back here with ?slack=connected — toast then clean up.
useEffect(() => {
const sp = new URLSearchParams(window.location.search)
if (sp.get("slack") !== "connected") return
const team = sp.get("team")
toast.success(
team
? `Supermemory added to ${team} on Slack`
: "Supermemory added to your Slack",
)
sp.delete("slack")
sp.delete("team")
const qs = sp.toString()
window.history.replaceState(
null,
"",
window.location.pathname + (qs ? `?${qs}` : ""),
)
}, [])
const queryClient = useQueryClient()
const [highlightsForceAt, setHighlightsForceAt] = useState(0)
@ -754,6 +777,10 @@ export default function NewPage() {
}}
/>
</div>
) : isCompanyBrain ? (
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto p-4 pt-2! pb-[180px] md:p-6">
<BrainHomeView />
</div>
) : (
<DashboardView
spaceLabel={dashboardSpaceLabel}

View file

@ -0,0 +1,357 @@
"use client"
import { $fetch } from "@lib/api"
import { useAuth } from "@lib/auth-context"
import { cn } from "@lib/utils"
import { useQuery } from "@tanstack/react-query"
import { ArrowRight, Check, FileText, Loader2 } from "lucide-react"
import Link from "next/link"
import { dmSans125ClassName } from "@/lib/fonts"
import { ConnectionsBoard } from "./connections-board"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const cardStyle = {
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}
type RecentDoc = {
id?: string
title?: string | null
createdAt?: string | Date | null
}
function useBrainOverview() {
const { user, org } = useAuth()
const enabled = !!user && !!org?.id
const docs = useQuery({
queryKey: ["brain-recents", org?.id],
queryFn: async () => {
const res = await $fetch("@post/documents/documents", {
body: {
page: 1,
limit: 6,
sort: "createdAt",
order: "desc",
containerTags: [],
},
disableValidation: true,
})
if (res.error) throw new Error(res.error?.message)
return res.data as unknown as {
documents?: RecentDoc[]
pagination?: { totalItems?: number }
}
},
staleTime: 60_000,
enabled,
})
const connectors = useQuery({
queryKey: ["brain-home", "connectors"],
queryFn: async () => {
const res = await $fetch("@post/connections/list", {
body: { containerTags: [] },
})
if (res.error) return [] as Array<{ provider?: string }>
return (res.data ?? []) as Array<{ provider?: string }>
},
staleTime: 30_000,
enabled,
})
const brain = useQuery({
queryKey: ["brain-connections"],
queryFn: async () => {
const [c, s] = await Promise.all([
fetch(`${BACKEND}/brain/connections`, { credentials: "include" }),
fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }),
])
const toolkits = c.ok
? ((await c.json()) as { toolkits: { org: boolean; user: boolean }[] })
.toolkits
: []
const slack = s.ok
? ((await s.json()) as { connected: boolean }).connected
: false
return {
activeCount: toolkits.filter((t) => t.org || t.user).length,
slack,
}
},
staleTime: 30_000,
enabled,
})
const mcp = useQuery({
queryKey: ["mcp-status"],
queryFn: async () => {
const res = await $fetch("@get/mcp/has-login")
if (res.error) return false
return Boolean((res.data as { previousLogin?: boolean })?.previousLogin)
},
staleTime: 60_000,
enabled,
})
const memoriesCount = docs.data?.pagination?.totalItems ?? 0
const connectedCount =
(brain.data?.activeCount ?? 0) +
(brain.data?.slack ? 1 : 0) +
(connectors.data?.length ?? 0)
return {
loading: docs.isPending,
recentDocs: docs.data?.documents ?? [],
memoriesCount,
connectedCount,
hasSource: connectedCount > 0,
hasAgent: mcp.data ?? false,
hasMemory: memoriesCount > 0,
}
}
export function BrainHomeView() {
const o = useBrainOverview()
const stepsDone = [o.hasSource, o.hasAgent, o.hasMemory].filter(
Boolean,
).length
return (
<div className="mx-auto max-w-[1080px] space-y-6">
<StatsRow
memories={o.memoriesCount}
connected={o.connectedCount}
setupDone={stepsDone}
/>
<ConnectionsBoard />
<div className="grid gap-6 lg:grid-cols-[1fr_340px]">
<RecentMemories docs={o.recentDocs} loading={o.loading} />
<GettingStarted
hasSource={o.hasSource}
hasAgent={o.hasAgent}
hasMemory={o.hasMemory}
/>
</div>
</div>
)
}
function StatsRow({
memories,
connected,
setupDone,
}: {
memories: number
connected: number
setupDone: number
}) {
const tiles = [
{ label: "Memories", value: memories.toLocaleString() },
{ label: "Connected sources", value: String(connected) },
{ label: "Setup", value: `${setupDone}/3` },
]
return (
<section
className="grid grid-cols-3 divide-x divide-white/[0.04] rounded-[16px] bg-[#1B1F24]"
style={cardStyle}
>
{tiles.map((t) => (
<div key={t.label} className="px-5 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[#737373]">
{t.label}
</p>
<p
className={cn(
"mt-1.5 text-[22px] font-semibold leading-none tabular-nums text-[#fafafa]",
dmSans125ClassName(),
)}
>
{t.value}
</p>
</div>
))}
</section>
)
}
function RecentMemories({
docs,
loading,
}: {
docs: RecentDoc[]
loading: boolean
}) {
return (
<section className="rounded-[18px] bg-[#1B1F24] p-5" style={cardStyle}>
<p
className={cn(
"mb-3 text-[15px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Recent memories
</p>
{loading ? (
<div className="flex items-center gap-2 py-6 text-[13px] font-medium text-[#737373]">
<Loader2 className="size-4 animate-spin" />
Loading
</div>
) : docs.length === 0 ? (
<div className="flex items-center gap-3 rounded-[12px] bg-[#14161A] px-4 py-5">
<div className="flex size-9 shrink-0 items-center justify-center rounded-[10px] bg-[#0F1217] text-[#525D6E]">
<FileText className="size-4" />
</div>
<div className="min-w-0">
<p className="text-[13px] font-medium text-[#fafafa]">
No memories yet
</p>
<p className="mt-0.5 text-[12px] font-medium leading-[1.5] text-[#737373]">
Connect a source or ask your brain below what you save shows up
here.
</p>
</div>
</div>
) : (
<ul className="divide-y divide-white/[0.04]">
{docs.map((doc, i) => (
<li
key={doc.id ?? i}
className="flex items-center gap-3 px-1 py-2.5"
>
<div className="flex size-8 shrink-0 items-center justify-center rounded-[8px] bg-[#0F1217] text-[#737373]">
<FileText className="size-3.5" />
</div>
<p className="min-w-0 flex-1 truncate text-[13px] font-medium text-[#fafafa]">
{doc.title?.trim() || "Untitled memory"}
</p>
<span className="shrink-0 text-[11px] font-medium text-[#737373]">
{formatWhen(doc.createdAt)}
</span>
</li>
))}
</ul>
)}
</section>
)
}
function GettingStarted({
hasSource,
hasAgent,
hasMemory,
}: {
hasSource: boolean
hasAgent: boolean
hasMemory: boolean
}) {
const steps = [
{
done: hasSource,
title: "Connect a source",
hint: "GitHub, Linear, Drive or Slack.",
href: "/settings/integrations",
},
{
done: hasAgent,
title: "Install a coding agent",
hint: "Claude Code, Codex or Cursor.",
href: "/settings/integrations",
},
{
done: hasMemory,
title: "Add your first memory",
hint: "Save a doc, or ask your brain below.",
},
]
return (
<section
className="relative h-fit overflow-hidden rounded-[18px] bg-[#1B1F24] 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)",
}}
/>
<p
className={cn(
"text-[15px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Getting started
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
A few steps to make your brain useful.
</p>
<ul className="mt-4 space-y-2.5">
{steps.map((step) => (
<li key={step.title} className="flex items-start gap-3">
<span
aria-hidden
className={cn(
"mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border",
step.done
? "border-[#4BA0FA] bg-[#4BA0FA]"
: "border-[rgba(82,89,102,0.4)]",
)}
>
{step.done && <Check className="size-3 text-white" />}
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p
className={cn(
"text-[13px] font-medium",
step.done
? "text-[#737373] line-through"
: "text-[#fafafa]",
)}
>
{step.title}
</p>
{!step.done && step.href && (
<Link
href={step.href}
className="inline-flex shrink-0 items-center gap-0.5 text-[12px] font-medium text-[#4BA0FA] transition-opacity hover:opacity-80"
>
Set up
<ArrowRight className="size-3" />
</Link>
)}
</div>
{!step.done && (
<p className="mt-0.5 text-[12px] font-medium leading-[1.4] text-[#737373]">
{step.hint}
</p>
)}
</div>
</li>
))}
</ul>
</section>
)
}
function formatWhen(value?: string | Date | null): string {
if (!value) return ""
const d = new Date(value)
if (Number.isNaN(d.getTime())) return ""
const min = Math.round((Date.now() - d.getTime()) / 60000)
if (min < 1) return "just now"
if (min < 60) return `${min}m`
const hr = Math.round(min / 60)
if (hr < 24) return `${hr}h`
const day = Math.round(hr / 24)
if (day < 7) return `${day}d`
return d.toLocaleDateString()
}

View file

@ -0,0 +1,393 @@
"use client"
import { $fetch } from "@lib/api"
import { cn } from "@lib/utils"
import { useQuery } from "@tanstack/react-query"
import { GoogleDrive, Notion } from "@ui/assets/icons"
import { Cloud, ExternalLink, Loader2 } from "lucide-react"
import Link from "next/link"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { dmSans125ClassName } from "@/lib/fonts"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const cardStyle = {
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}
const tileStyle = {
boxShadow:
"0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)",
}
type ConnRow = { toolkit: string; org: boolean; user: boolean }
export function ConnectionsBoard() {
const [brainRows, setBrainRows] = useState<ConnRow[] | null>(null)
const [slack, setSlack] = useState<{
connected: boolean
teamName: string | null
} | null>(null)
const [busy, setBusy] = useState<string | null>(null)
const loadBrain = useCallback(async () => {
try {
const [c, s] = await Promise.all([
fetch(`${BACKEND}/brain/connections`, { credentials: "include" }),
fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }),
])
if (c.ok)
setBrainRows(((await c.json()) as { toolkits: ConnRow[] }).toolkits)
if (s.ok) setSlack(await s.json())
} catch {}
}, [])
useEffect(() => {
void loadBrain()
const onFocus = () => void loadBrain()
window.addEventListener("focus", onFocus)
return () => window.removeEventListener("focus", onFocus)
}, [loadBrain])
const { data: connectors } = useQuery({
queryKey: ["brain-home", "connectors"],
queryFn: async () => {
const res = await $fetch("@post/connections/list", {
body: { containerTags: [] },
})
if (res.error) return [] as Array<{ provider?: string }>
return (res.data ?? []) as Array<{ provider?: string }>
},
staleTime: 30_000,
})
const connectorConnected = (provider: string) =>
Boolean(connectors?.some((c) => c.provider === provider))
const connectBrain = async (toolkit: string) => {
setBusy(`brain:${toolkit}`)
try {
const res = await fetch(
`${BACKEND}/brain/connections/${toolkit}/link?scope=user`,
{ method: "POST", credentials: "include" },
)
if (!res.ok) {
toast.error("Couldn't start the connection.")
return
}
const data = (await res.json()) as { url?: string }
if (data.url) window.open(data.url, "_blank", "noopener")
else toast.error("Couldn't start the connection.")
} catch {
toast.error("Couldn't start the connection.")
} finally {
setBusy(null)
}
}
const connectConnector = async (
provider: "google-drive" | "notion" | "onedrive",
) => {
setBusy(`conn:${provider}`)
try {
const res = await $fetch("@post/connections/:provider", {
params: { provider },
body: { redirectUrl: window.location.href, containerTags: [] },
})
const data = res.data as { authLink?: string } | undefined
if (data?.authLink) window.location.href = data.authLink
else toast.error("Couldn't start the connection.")
} catch {
toast.error("Couldn't start the connection.")
} finally {
setBusy(null)
}
}
const brainConnected = (toolkit: string) =>
Boolean(brainRows?.find((r) => r.toolkit === toolkit)?.org) ||
Boolean(brainRows?.find((r) => r.toolkit === toolkit)?.user)
return (
<div className="space-y-4">
{slack && !slack.connected && <SlackBanner />}
<div className="grid gap-4 lg:grid-cols-2">
<Group
title="Tool integrations"
subtitle="Apps your agents can act on."
>
<AppCard
icon={<GithubMark className="size-5 text-[#fafafa]" />}
name="GitHub"
subtitle="Repos, pull requests and issues."
connected={brainConnected("github")}
busy={busy === "brain:github"}
onConnect={() => connectBrain("github")}
/>
<AppCard
icon={<LinearMark className="size-5 text-[#5E6AD2]" />}
name="Linear"
subtitle="Issues, projects and cycles."
connected={brainConnected("linear")}
busy={busy === "brain:linear"}
onConnect={() => connectBrain("linear")}
/>
</Group>
<Group
title="Connectors"
subtitle="Sync documents into your brain."
cta={
<Link
href="/settings/integrations"
className="inline-flex items-center gap-1 text-[12px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]"
>
All connectors
<ExternalLink className="size-3" aria-hidden />
</Link>
}
>
<AppCard
icon={<GoogleDrive className="size-5" />}
name="Google Drive"
subtitle="Docs, sheets and slides."
connected={connectorConnected("google-drive")}
busy={busy === "conn:google-drive"}
onConnect={() => connectConnector("google-drive")}
/>
<AppCard
icon={<Notion className="size-5" />}
name="Notion"
subtitle="Pages, databases and blocks."
connected={connectorConnected("notion")}
busy={busy === "conn:notion"}
onConnect={() => connectConnector("notion")}
/>
<AppCard
icon={<Cloud className="size-5 text-[#0F6CBD]" />}
name="OneDrive"
subtitle="Files from Microsoft 365."
connected={connectorConnected("onedrive")}
busy={busy === "conn:onedrive"}
onConnect={() => connectConnector("onedrive")}
/>
</Group>
</div>
</div>
)
}
function SlackBanner() {
return (
<section
className="relative overflow-hidden rounded-[18px] bg-[#1B1F24] 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 flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3.5">
<div
className="flex size-12 shrink-0 items-center justify-center rounded-[12px] border border-[rgba(82,89,102,0.2)] bg-[#080B0F]"
style={tileStyle}
>
<SlackMark className="size-7" />
</div>
<div className="min-w-0">
<p
className={cn(
"text-[16px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Company Brain in Slack
</p>
<p className="mt-0.5 text-[13px] font-medium leading-[1.5] text-[#737373]">
Install Supermemory so your team can{" "}
<span className="text-[#A1A1AA]">@supermemory</span> in any
channel.
</p>
</div>
</div>
<a
href={`${BACKEND}/brain/slack/oauth/install`}
className="inline-flex shrink-0 items-center gap-2 self-start rounded-lg bg-white px-4 py-2.5 text-[14px] font-semibold text-[#1D1C1D] transition-transform hover:scale-[1.02] sm:self-auto"
>
<SlackMark className="size-[18px]" />
Add to Slack
</a>
</div>
</section>
)
}
function Group({
title,
subtitle,
accent,
cta,
children,
}: {
title: string
subtitle: string
accent?: boolean
cta?: React.ReactNode
children: React.ReactNode
}) {
return (
<section
className="relative flex flex-col gap-2.5 overflow-hidden rounded-[18px] bg-[#1B1F24] p-5"
style={cardStyle}
>
{accent && (
<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="mb-1 flex items-start justify-between gap-3">
<div>
<p
className={cn(
"text-[15px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
{title}
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
{subtitle}
</p>
</div>
{cta}
</div>
{children}
</section>
)
}
function AppCard({
icon,
name,
subtitle,
connected,
busy,
onConnect,
}: {
icon: React.ReactNode
name: string
subtitle: string
connected: boolean
busy: boolean
onConnect: () => void
}) {
return (
<div className="flex items-center gap-3 rounded-[12px] bg-[#14161A] p-3">
<div
className="flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-[rgba(82,89,102,0.2)] bg-[#080B0F]"
style={tileStyle}
>
{icon}
</div>
<div className="min-w-0 flex-1">
<p className="text-[14px] font-semibold leading-tight text-[#fafafa]">
{name}
</p>
<p className="mt-0.5 truncate text-[12px] font-medium text-[#737373]">
{subtitle}
</p>
</div>
{connected ? (
<span className="flex shrink-0 items-center gap-1.5 text-[12px] font-medium text-[#fafafa]">
<span className="size-[7px] rounded-full bg-[#00AC3F]" />
Connected
</span>
) : (
<button
type="button"
onClick={onConnect}
disabled={busy}
className={cn(
dmSans125ClassName(),
"flex shrink-0 items-center gap-1.5 rounded-full bg-[#0D121A] px-3.5 py-2 text-[13px] font-medium text-[#fafafa] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80 disabled:opacity-50",
)}
>
{busy && <Loader2 className="size-3.5 animate-spin" />}
Connect
</button>
)}
</div>
)
}
function GithubMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
)
}
function LinearMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
<title>Linear</title>
<path d="M3.084 12.866a8.916 8.916 0 0 0 8.05 8.05.27.27 0 0 0 .222-.46l-7.812-7.812a.27.27 0 0 0-.46.222Zm-.044-1.955a.27.27 0 0 0 .078.21l9.76 9.76c.06.06.142.087.21.078a8.87 8.87 0 0 0 1.273-.218.27.27 0 0 0 .127-.453L3.712 9.51a.27.27 0 0 0-.453.127 8.87 8.87 0 0 0-.218 1.273Zm.69-2.706a.27.27 0 0 0 .06.29l11.715 11.716a.27.27 0 0 0 .29.06 8.96 8.96 0 0 0 .837-.384.27.27 0 0 0 .066-.439L4.553 7.302a.27.27 0 0 0-.44.066 8.96 8.96 0 0 0-.383.837Zm1.11-1.798a.27.27 0 0 1-.017-.366A8.948 8.948 0 0 1 18.07 18.69a.27.27 0 0 1-.366-.017L4.94 6.407Z" />
</svg>
)
}
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>
)
}

View file

@ -31,6 +31,7 @@ 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 { GoogleDrive, Notion, MCPIcon } from "@ui/assets/icons"
import { analytics } from "@/lib/analytics"
import type { IntegrationParamValue } from "@/lib/search-params"
@ -1331,6 +1332,7 @@ export function DashboardView({
)}
>
<div className="mx-auto w-full max-w-4xl space-y-4 md:space-y-5">
<SlackConnectCard />
{headerNotice ? <div className="space-y-2">{headerNotice}</div> : null}
{/* Header */}

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

@ -1,21 +1,89 @@
"use client"
import { useState, useEffect } from "react"
import { useEffect, useMemo, useState } from "react"
import type { ReactNode } from "react"
import Image from "next/image"
import { useQueryState, parseAsString } from "nuqs"
import Link from "next/link"
import { Button } from "@ui/components/button"
import { MCPIcon } from "@ui/assets/icons"
import { ArrowRight, Check, Copy, EyeOff, Eye } from "lucide-react"
import {
ArrowRight,
Check,
Copy,
ExternalLink,
Loader2,
Plug,
} from "lucide-react"
import { cn } from "@lib/utils"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { dmSans125ClassName } from "@/lib/fonts"
import { toast } from "sonner"
import { MCPSteps } from "@/components/mcp-modal/mcp-detail-view"
import { PLUGIN_CATALOG } from "@/lib/plugin-catalog"
import { analytics } from "@/lib/analytics"
import type { BrainMode } from "./types"
interface Props {
mcpUrl: string
onContinue: () => void
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const TEST_PROMPT = "What do we know about [topic]?"
type FlowToolId = "slack" | "mcp" | "codex" | "claude-code"
type FlowToolKind = "slack" | "mcp" | "plugin"
type FlowTool = {
id: FlowToolId
label: string
blurb: string
kind: FlowToolKind
pluginId?: string
recommended?: boolean
}
const TOOL_OPTIONS: Record<BrainMode, [FlowTool, ...FlowTool[]]> = {
team: [
{
id: "slack",
label: "Slack",
blurb: "Ask questions in-channel.",
kind: "slack",
recommended: true,
},
{
id: "claude-code",
label: "Claude Code",
blurb: "Shared context in your terminal.",
kind: "plugin",
pluginId: "claude_code",
},
{
id: "codex",
label: "Codex",
blurb: "OpenAI's coding agent.",
kind: "plugin",
pluginId: "codex",
},
],
personal: [
{
id: "mcp",
label: "MCP",
blurb: "Use the universal URL in any client.",
kind: "mcp",
recommended: true,
},
{
id: "codex",
label: "Codex",
blurb: "OpenAI's coding agent.",
kind: "plugin",
pluginId: "codex",
},
{
id: "claude-code",
label: "Claude Code",
blurb: "Context in your terminal.",
kind: "plugin",
pluginId: "claude_code",
},
],
}
const modalCardStyle = {
@ -28,102 +96,24 @@ 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)",
}
type AgentCategory = "coding" | "productivity"
type Agent = {
key: string
name: string
tagline: string
category: AgentCategory
pluginId?: string
interface Props {
mode: BrainMode
mcpUrl: string
onContinue: () => void
}
const AGENTS: Agent[] = [
{
key: "cursor",
name: "Cursor",
tagline: "Persistent context across coding sessions.",
category: "coding",
},
{
key: "claude-code",
name: "Claude Code",
tagline: "Memory and decisions across CLI sessions.",
category: "coding",
pluginId: "claude_code",
},
{
key: "vscode",
name: "VS Code",
tagline: "Inline context while you write.",
category: "coding",
},
{
key: "cline",
name: "Cline",
tagline: "Agentic dev tasks with your memory.",
category: "coding",
},
{
key: "codex",
name: "Codex",
tagline: "OpenAI Codex with persistent memory.",
category: "coding",
pluginId: "codex",
},
{
key: "gemini-cli",
name: "Gemini CLI",
tagline: "Gemini in your terminal, brain-aware.",
category: "coding",
},
{
key: "claude",
name: "Claude Desktop",
tagline: "Memory across every Claude conversation.",
category: "productivity",
},
{
key: "chatgpt",
name: "ChatGPT",
tagline: "Custom GPT backed by your brain.",
category: "productivity",
},
]
const CATEGORY_ORDER: { id: AgentCategory; label: string }[] = [
{ id: "coding", label: "Coding" },
{ id: "productivity", label: "Productivity" },
]
function agentIcon(agent: Agent) {
if (agent.pluginId) {
const plugin = PLUGIN_CATALOG[agent.pluginId]
if (plugin) return plugin.icon
}
const file = agent.key === "claude-code" ? "claude" : agent.key
return `/mcp-supported-tools/${file}.png`
}
export function StepIngest({ mcpUrl, onContinue }: Props) {
const [activeCategory, setActiveCategory] = useState<AgentCategory>("coding")
const [selectedKey, setSelectedKey] = useState<string>("cursor")
const [, setMcpClient] = useQueryState("mcpClient", parseAsString)
const selectedAgent = AGENTS.find((a) => a.key === selectedKey) ?? AGENTS[0]
export function StepIngest({ mode, mcpUrl, onContinue }: Props) {
const tools = TOOL_OPTIONS[mode]
const [selected, setSelected] = useState<FlowToolId>(tools[0].id)
useEffect(() => {
if (selectedAgent && !selectedAgent.pluginId) {
setMcpClient(selectedAgent.key)
} else {
setMcpClient(null)
}
}, [selectedAgent, setMcpClient])
setSelected(tools[0].id)
}, [tools])
const selectAgent = (agent: Agent) => {
analytics.onboardingAgentSelected({ agent: agent.key })
setSelectedKey(agent.key)
}
const activeTool = useMemo(
() => tools.find((t) => t.id === selected) ?? tools[0],
[tools, selected],
)
const handleContinue = () => {
analytics.onboardingIngestCompleted()
@ -135,79 +125,411 @@ export function StepIngest({ mcpUrl, onContinue }: Props) {
onContinue()
}
const filtered = AGENTS.filter((a) => a.category === activeCategory)
return (
<div className="space-y-5">
<div className="px-1">
<p
className={cn(
"font-semibold text-[#fafafa] text-[22px]",
dmSans125ClassName(),
)}
>
Use your brain anywhere
</p>
<p className="text-[#737373] font-medium text-[15px] leading-[1.4] mt-1.5">
Now plug it into the tools you already use to write code, chat, think.
</p>
</div>
<div className="mx-auto w-full max-w-[900px] pb-10">
<section className="relative py-4">
<div className="mb-6 px-1">
<p
className={cn(
"text-[22px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Use your brain where you work
</p>
<p className="mt-1.5 text-[15px] font-medium leading-[1.4] text-[#737373]">
{mode === "team"
? "Pick where your team asks questions, then set it up."
: "Pick the tool you open every day — about 60 seconds."}
</p>
</div>
<McpHero url={mcpUrl} />
<div className="grid lg:grid-cols-[300px_1fr] gap-4 h-[560px]">
<aside
className="rounded-[16px] bg-[#1B1F24] p-3 flex flex-col gap-3 overflow-hidden h-full"
style={modalCardStyle}
>
<CategoryTabs value={activeCategory} onChange={setActiveCategory} />
<div className="flex-1 min-h-0 overflow-y-auto space-y-1 scrollbar-thin pr-1">
{filtered.map((agent) => (
<AgentRow
key={agent.key}
agent={agent}
active={selectedKey === agent.key}
onClick={() => selectAgent(agent)}
<div className="grid items-start gap-4 lg:grid-cols-[250px_minmax(0,1fr)]">
{/* Left rail: pick a tool */}
<div className="flex flex-col gap-2">
{tools.map((tool) => (
<FlowToolRow
key={tool.id}
tool={tool}
active={selected === tool.id}
onSelect={() => {
analytics.onboardingAgentSelected({ agent: tool.id })
setSelected(tool.id)
}}
/>
))}
<Link
href="/settings/integrations"
className="mt-1 inline-flex items-center gap-1.5 px-2 py-1.5 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]"
>
More tools
<span className="text-[#525D6E]">(full catalog)</span>
<ExternalLink className="size-3.5" aria-hidden />
</Link>
</div>
</aside>
<section
className="rounded-[16px] bg-[#1B1F24] p-5 md:p-6 overflow-hidden flex flex-col h-full"
style={modalCardStyle}
>
{selectedAgent?.pluginId ? (
<PluginSteps pluginId={selectedAgent.pluginId} />
) : (
<MCPSteps variant="embedded" />
)}
</section>
{/* Right pane: setup detail */}
<div
className="relative flex min-h-[300px] flex-col overflow-hidden rounded-[22px] bg-[#1B1F24] p-6 md:p-7"
style={modalCardStyle}
>
<div
aria-hidden
className="absolute -top-px right-10 left-10 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.4), transparent)",
}}
/>
<div className="flex flex-1 flex-col">
<div className="mb-5 flex items-center gap-3">
<div
className="flex size-10 shrink-0 items-center justify-center rounded-[12px] border border-[rgba(82,89,102,0.2)] bg-[#14161A]"
style={inputBevelStyle}
>
<ToolIcon id={activeTool.id} />
</div>
<div>
<p className="text-[18px] font-semibold text-[#fafafa]">
Set up {activeTool.label}
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
{activeTool.blurb}
</p>
</div>
</div>
{activeTool.kind === "slack" ? (
<SlackSetupPanel />
) : activeTool.kind === "mcp" ? (
<McpGenericSetup mcpUrl={mcpUrl} />
) : activeTool.pluginId ? (
<PluginSetup
key={activeTool.pluginId}
pluginId={activeTool.pluginId}
/>
) : null}
</div>
<div className="mt-6 flex items-center justify-end gap-[22px] border-t border-white/[0.06] pt-5">
<button
type="button"
onClick={handleSkip}
className="text-[14px] font-medium text-[#737373] transition-colors hover:text-[#999]"
>
Skip for now
</button>
<Button
variant="insideOut"
onClick={handleContinue}
className="rounded-full px-5 py-[10px] text-[13px] font-medium text-[#fafafa]"
>
Continue
<ArrowRight className="size-3.5" />
</Button>
</div>
</div>
</div>
</section>
</div>
)
}
function ToolIcon({ id, className }: { id: FlowToolId; className?: string }) {
if (id === "slack") return <SlackMark className={className ?? "size-5"} />
if (id === "mcp")
return <Plug className={cn("text-[#4BA0FA]", className ?? "size-5")} />
const file = id === "claude-code" ? "claude" : id
return (
<Image
src={`/mcp-supported-tools/${file}.png`}
alt=""
width={28}
height={28}
unoptimized
className={cn("object-contain", className ?? "size-5")}
/>
)
}
function FlowToolRow({
tool,
active,
onSelect,
}: {
tool: FlowTool
active: boolean
onSelect: () => void
}) {
return (
<button
type="button"
onClick={onSelect}
aria-pressed={active}
className={cn(
"group relative flex w-full items-center gap-3 overflow-hidden rounded-[14px] p-3 text-left transition-all duration-150",
active
? "bg-[#10151D] ring-2 ring-[#4BA0FA]/45"
: "bg-[#1B1F24] ring-1 ring-white/[0.05] hover:ring-white/[0.12]",
)}
style={modalCardStyle}
>
{active && (
<div
aria-hidden
className="absolute -top-px right-5 left-5 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.55), transparent)",
}}
/>
)}
<div
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-[10px] border bg-[#14161A] transition-colors",
active ? "border-[#2261CA]/45" : "border-[rgba(82,89,102,0.2)]",
)}
style={inputBevelStyle}
>
<ToolIcon id={tool.id} />
</div>
<div className="flex flex-wrap items-center justify-end gap-[22px] px-1 pt-2">
<button
type="button"
onClick={handleSkip}
className="text-[#737373] font-medium text-[14px] hover:text-[#999] transition-colors"
>
Skip for now
</button>
<Button
variant="insideOut"
onClick={handleContinue}
className="rounded-full px-5 py-[10px] text-[13px] font-medium text-[#fafafa]"
>
Continue
<ArrowRight className="size-3.5" />
</Button>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-[14px] font-semibold leading-tight text-[#fafafa]">
{tool.label}
</p>
{tool.recommended && (
<span className="shrink-0 rounded-full bg-[#4BA0FA]/12 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[#4BA0FA]">
Recommended
</span>
)}
</div>
<p className="mt-0.5 truncate text-[12px] font-medium text-[#737373]">
{tool.blurb}
</p>
</div>
<span
aria-hidden
className={cn(
"flex size-[18px] shrink-0 items-center justify-center rounded-full border transition-colors",
active
? "border-[#4BA0FA] bg-[#4BA0FA]"
: "border-[rgba(82,89,102,0.4)] group-hover:border-[rgba(115,115,115,0.5)]",
)}
>
{active && <Check className="size-3 text-white" />}
</span>
</button>
)
}
function StepRow({
index,
title,
done,
children,
}: {
index: number
title: ReactNode
done?: boolean
children?: ReactNode
}) {
return (
<div className="flex gap-3">
<span
aria-hidden
className={cn(
"mt-0.5 flex size-[22px] shrink-0 items-center justify-center rounded-full text-[12px] font-semibold transition-colors",
done
? "bg-[#4BA0FA] text-white"
: "border border-[rgba(82,89,102,0.3)] bg-[#14161A] text-[#737373]",
)}
>
{done ? <Check className="size-3" /> : index}
</span>
<div className="min-w-0 flex-1 pt-0.5">
<div className="text-[13px] font-medium leading-[1.5] text-[#fafafa]">
{title}
</div>
{children ? <div className="mt-2.5">{children}</div> : null}
</div>
</div>
)
}
function McpHero({ url }: { url: string }) {
// Coding-agent plugins (Codex, Claude Code) auto-login via OAuth, so the
// "Save your API key" step is dropped — we render the remaining install steps.
function PluginSetup({ pluginId }: { pluginId: string }) {
const plugin = PLUGIN_CATALOG[pluginId]
const steps = (plugin?.installSteps ?? []).filter(
(s) => !s.secret && !s.code?.includes("sm_..."),
)
return (
<div className="space-y-4">
{steps.map((step, i) => (
<StepRow key={step.title} index={i + 1} title={step.title}>
{step.description ? (
<p className="mb-2 text-[12px] font-medium leading-[1.5] text-[#737373]">
{step.description}
</p>
) : null}
{step.code ? <CopyCodeBlock code={step.code} /> : null}
</StepRow>
))}
<StepRow index={steps.length + 1} title="Ask your brain to test it">
<CopyCodeBlock code={TEST_PROMPT} />
</StepRow>
</div>
)
}
function McpGenericSetup({ mcpUrl }: { mcpUrl: string }) {
return (
<div className="space-y-4">
<StepRow index={1} title="Copy your universal MCP URL">
<McpUrlRow url={mcpUrl} />
</StepRow>
<StepRow index={2} title="Paste it into any MCP client">
<Link
href="/settings/integrations"
className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]"
>
Per-client setup guides
<ExternalLink className="size-3.5" aria-hidden />
</Link>
</StepRow>
<StepRow index={3} title="Ask your brain to test it">
<CopyCodeBlock code={TEST_PROMPT} />
</StepRow>
</div>
)
}
function SlackSetupPanel() {
const [status, setStatus] = useState<{
connected: boolean
teamName: string | null
} | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let active = true
;(async () => {
try {
const res = await fetch(`${BACKEND}/brain/slack/status`, {
credentials: "include",
})
if (active && res.ok) {
setStatus(
(await res.json()) as {
connected: boolean
teamName: string | null
},
)
}
} finally {
if (active) setLoading(false)
}
})()
return () => {
active = false
}
}, [])
const connected = status?.connected ?? false
return (
<div className="space-y-4">
<StepRow
index={1}
done={connected}
title={
connected
? `Connected to ${status?.teamName ?? "your workspace"}`
: "Add Supermemory to your Slack workspace"
}
>
{!connected &&
(loading ? (
<span className="inline-flex items-center gap-2 text-[12px] font-medium text-[#737373]">
<Loader2 className="size-3.5 animate-spin" />
Checking
</span>
) : (
<Button
variant="insideOut"
asChild
className="h-9 gap-2 rounded-full px-4 text-[13px] font-medium text-[#fafafa]"
>
<a href={`${BACKEND}/brain/slack/oauth/install`}>
<SlackMark className="size-4" />
Add to Slack
</a>
</Button>
))}
</StepRow>
<StepRow
index={2}
title={
<>
Mention <span className="text-[#4BA0FA]">@supermemory</span> in{" "}
<span className="font-mono text-[12px]">#general</span>
</>
}
/>
<StepRow index={3} title="Ask your brain to test it">
<CopyCodeBlock code="@supermemory what do we know about [topic]?" />
</StepRow>
</div>
)
}
function CopyCodeBlock({ code }: { code: string }) {
const [copied, setCopied] = useState(false)
const copy = async () => {
try {
await navigator.clipboard.writeText(code)
setCopied(true)
toast.success("Copied")
setTimeout(() => setCopied(false), 1500)
} catch {
toast.error("Could not copy")
}
}
return (
<div
className="flex min-w-0 flex-1 items-start gap-2 rounded-[12px] border border-[rgba(82,89,102,0.2)] bg-[#0F1217] p-3"
style={inputBevelStyle}
>
<pre className="min-w-0 flex-1 overflow-x-auto whitespace-pre font-mono text-[11px] text-[#fafafa]">
{code}
</pre>
<button
type="button"
onClick={copy}
className="flex size-7 shrink-0 items-center justify-center rounded-md text-[#737373] transition-colors hover:text-[#fafafa]"
aria-label="Copy"
>
{copied ? (
<Check className="size-3.5 text-[#4BA0FA]" />
) : (
<Copy className="size-3.5" />
)}
</button>
</div>
)
}
function McpUrlRow({ url }: { url: string }) {
const [copied, setCopied] = useState(false)
const copy = async () => {
try {
await navigator.clipboard.writeText(url)
@ -220,36 +542,22 @@ function McpHero({ url }: { url: string }) {
}
return (
<section
className="rounded-[16px] bg-[#1B1F24] px-5 py-3 flex items-center gap-3 relative overflow-hidden"
style={modalCardStyle}
>
<div className="flex flex-wrap items-center gap-3">
<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)",
}}
/>
<div
className="size-9 rounded-[10px] bg-[#14161A] border border-[rgba(82,89,102,0.2)] flex items-center justify-center shrink-0"
className="min-w-0 flex-1 rounded-[12px] border border-[rgba(82,89,102,0.2)] bg-[#0F1217] px-4 py-3"
style={inputBevelStyle}
>
<MCPIcon className="size-5" />
</div>
<div className="min-w-0 flex-1">
<p className="text-[10px] uppercase tracking-[0.08em] text-[#737373] font-semibold">
<p className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#525D6E]">
Universal MCP URL
</p>
<p className="text-[13px] text-[#fafafa] font-mono truncate mt-0.5">
<p className="mt-0.5 truncate font-mono text-[13px] text-[#fafafa]">
{url}
</p>
</div>
<Button
variant="insideOut"
onClick={copy}
className="rounded-full h-9 px-4 text-[12px] font-medium text-[#fafafa] shrink-0"
className="h-9 shrink-0 rounded-full px-4 text-[12px] font-medium text-[#fafafa]"
>
{copied ? (
<Check className="size-3.5" />
@ -258,235 +566,46 @@ function McpHero({ url }: { url: string }) {
)}
{copied ? "Copied" : "Copy URL"}
</Button>
</section>
)
}
function PluginSteps({ pluginId }: { pluginId: string }) {
const plugin = PLUGIN_CATALOG[pluginId]
if (!plugin) return null
const steps = plugin.installSteps ?? []
return (
<div className="h-full overflow-y-auto pr-1 scrollbar-thin">
<div className="flex items-center gap-3 mb-4">
<div className="size-10 rounded-[10px] bg-[#080B0F] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)] flex items-center justify-center shrink-0 overflow-hidden">
<Image
src={plugin.icon}
alt={plugin.name}
width={28}
height={28}
unoptimized
className="size-6 object-contain"
/>
</div>
<div className="min-w-0 flex-1">
<p className="text-[16px] font-semibold text-[#fafafa] leading-tight">
Set up {plugin.name}
</p>
<p className="text-[12px] text-[#A1A1AA] font-medium mt-0.5">
{plugin.tagline}
</p>
</div>
{plugin.docsUrl && (
<a
href={plugin.docsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[12px] text-[#A1A1AA] hover:text-[#fafafa] transition-colors font-medium shrink-0"
>
Docs
</a>
)}
</div>
<div className="space-y-3">
{steps.map((step, i) => (
<PluginStep key={step.title} idx={i + 1} step={step} />
))}
</div>
<div className="mt-4 rounded-[10px] border border-[#4BA0FA]/20 bg-[#4BA0FA]/[0.04] p-3 flex items-start gap-2">
<div className="text-[11px] text-[#A1A1AA] leading-[1.5] font-medium">
Your <span className="text-[#fafafa]">API key</span> is minted in
Settings Integrations Plugins. Mint it once and paste into the
step above.
</div>
</div>
</div>
)
}
function PluginStep({
idx,
step,
}: {
idx: number
step: import("@/lib/plugin-catalog").InstallStep
}) {
const [revealed, setRevealed] = useState(false)
const [copied, setCopied] = useState(false)
const copy = async () => {
if (!step.code) return
try {
await navigator.clipboard.writeText(step.code)
setCopied(true)
toast.success("Copied")
setTimeout(() => setCopied(false), 1500)
} catch {
toast.error("Could not copy")
}
}
function SlackMark({ className }: { className?: string }) {
return (
<div className="flex gap-3">
<div className="flex flex-col items-center shrink-0 pt-1">
<div className="size-5 rounded-full bg-[#4BA0FA]/15 text-[#4BA0FA] flex items-center justify-center text-[10px] font-semibold">
{idx}
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-semibold text-[#fafafa]">
{step.title}
{step.optional && (
<span className="ml-2 text-[10px] uppercase tracking-[0.08em] text-[#525D6E]">
Optional
</span>
)}
</p>
{step.description && (
<p className="text-[12px] text-[#A1A1AA] mt-1 leading-[1.5] font-medium">
{step.description}
</p>
)}
{step.code && (
<div
className="mt-2 rounded-[10px] bg-[#0F1217] border border-[rgba(82,89,102,0.2)] p-3 flex items-start gap-2"
style={inputBevelStyle}
>
<pre
className={cn(
"flex-1 min-w-0 text-[11px] text-[#fafafa] font-mono overflow-x-auto whitespace-pre",
step.secret && !revealed && "blur-[3px] select-none",
)}
>
{step.code}
</pre>
<div className="flex items-center gap-1 shrink-0">
{step.secret && (
<button
type="button"
onClick={() => setRevealed((v) => !v)}
className="size-7 rounded-md text-[#737373] hover:text-[#fafafa] flex items-center justify-center transition-colors"
aria-label={revealed ? "Hide" : "Reveal"}
>
{revealed ? (
<EyeOff className="size-3.5" />
) : (
<Eye className="size-3.5" />
)}
</button>
)}
<button
type="button"
onClick={copy}
className="size-7 rounded-md text-[#737373] hover:text-[#fafafa] flex items-center justify-center transition-colors"
aria-label="Copy"
>
{copied ? (
<Check className="size-3.5 text-[#4BA0FA]" />
) : (
<Copy className="size-3.5" />
)}
</button>
</div>
</div>
)}
</div>
</div>
)
}
function CategoryTabs({
value,
onChange,
}: {
value: AgentCategory
onChange: (c: AgentCategory) => void
}) {
const counts: Record<AgentCategory, number> = {
coding: 0,
productivity: 0,
}
for (const a of AGENTS) counts[a.category] += 1
return (
<div className="scrollbar-none flex items-center gap-0.5 overflow-x-auto rounded-full bg-[#0D121A] p-0.5 shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.5)] w-full">
{CATEGORY_ORDER.map((cat) => {
const isActive = value === cat.id
return (
<button
key={cat.id}
type="button"
onClick={() => onChange(cat.id)}
className={cn(
dmSansClassName(),
"flex flex-1 h-7 shrink-0 items-center justify-center gap-1.5 rounded-full px-3 text-[12px] font-medium leading-none transition-colors",
isActive
? "bg-white/[0.10] text-[#FAFAFA]"
: "text-[#A1A1AA] hover:text-[#FAFAFA]",
)}
>
<span className="leading-none">{cat.label}</span>
<span
className={cn(
"text-[10px] font-semibold tabular-nums leading-none",
isActive ? "text-[#A1A1AA]" : "text-[#525D6E]",
)}
>
{counts[cat.id]}
</span>
</button>
)
})}
</div>
)
}
function AgentRow({
agent,
active,
onClick,
}: {
agent: Agent
active: boolean
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
dmSansClassName(),
"w-full text-left flex items-center gap-2.5 rounded-[10px] px-2.5 py-2 transition-colors",
active
? "bg-white/[0.08] text-[#fafafa]"
: "text-[#A1A1AA] hover:bg-white/[0.04] hover:text-[#fafafa]",
)}
>
<div className="size-8 rounded-[8px] bg-[#080B0F] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)] flex items-center justify-center shrink-0 overflow-hidden">
<Image
src={agentIcon(agent)}
alt={agent.name}
width={24}
height={24}
unoptimized
className="size-5 object-contain"
/>
</div>
<div className="min-w-0 flex-1">
<p className="text-[13px] font-medium truncate">{agent.name}</p>
<p className="text-[11px] text-[#737373] truncate font-medium">
{agent.tagline}
</p>
</div>
</button>
<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>
)
}

View file

@ -0,0 +1,297 @@
"use client"
import { authClient } from "@lib/auth"
import { cn } from "@lib/utils"
import { useQuery } from "@tanstack/react-query"
import { Check, Loader2, Lock } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { dmSans125ClassName } from "@/lib/fonts"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { PillButton } from "../integrations/install-steps"
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
type ConnRow = { toolkit: string; org: boolean; user: boolean }
function GithubMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
)
}
function LinearMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
<title>Linear</title>
<path d="M3.084 12.866a8.916 8.916 0 0 0 8.05 8.05.27.27 0 0 0 .222-.46l-7.812-7.812a.27.27 0 0 0-.46.222Zm-.044-1.955a.27.27 0 0 0 .078.21l9.76 9.76c.06.06.142.087.21.078a8.87 8.87 0 0 0 1.273-.218.27.27 0 0 0 .127-.453L3.712 9.51a.27.27 0 0 0-.453.127 8.87 8.87 0 0 0-.218 1.273Zm.69-2.706a.27.27 0 0 0 .06.29l11.715 11.716a.27.27 0 0 0 .29.06 8.96 8.96 0 0 0 .837-.384.27.27 0 0 0 .066-.439L4.553 7.302a.27.27 0 0 0-.44.066 8.96 8.96 0 0 0-.383.837Zm1.11-1.798a.27.27 0 0 1-.017-.366A8.948 8.948 0 0 1 18.07 18.69a.27.27 0 0 1-.366-.017L4.94 6.407Z" />
</svg>
)
}
const TOOLKITS: Record<
string,
{ label: string; subtitle: string; icon: React.ReactNode }
> = {
github: {
label: "GitHub",
subtitle: "Repos, pull requests and issues",
icon: <GithubMark className="size-5 text-[#FAFAFA]" />,
},
linear: {
label: "Linear",
subtitle: "Issues, projects and cycles",
icon: <LinearMark className="size-5 text-[#5E6AD2]" />,
},
}
function StatusDot({ connected }: { connected: boolean }) {
return (
<span
className={cn(
dmSans125ClassName(),
"flex items-center gap-1.5 text-[13px] font-medium",
connected ? "text-[#FAFAFA]" : "text-[#737373]",
)}
>
<span
className={cn(
"size-[7px] shrink-0 rounded-full",
connected ? "bg-[#00AC3F]" : "bg-[#3A4150]",
)}
/>
{connected ? "Connected" : "Not connected"}
</span>
)
}
function AppCard({
toolkit,
connected,
canConnect,
lockedHint,
busy,
onConnect,
}: {
toolkit: string
connected: boolean
canConnect: boolean
lockedHint?: string
busy: boolean
onConnect: () => void
}) {
const meta = TOOLKITS[toolkit] ?? {
label: toolkit,
subtitle: "",
icon: null,
}
return (
<div className="flex items-center justify-between gap-4 rounded-[14px] bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)] sm:p-5">
<div className="flex min-w-0 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-[#080B0F] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)]">
{meta.icon}
</div>
<div className="min-w-0">
<p
className={cn(
dmSans125ClassName(),
"font-semibold text-[15px] tracking-[-0.15px] text-[#FAFAFA]",
)}
>
{meta.label}
</p>
<p
className={cn(
dmSans125ClassName(),
"truncate text-[12px] font-medium text-[#737373]",
)}
>
{meta.subtitle}
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-3">
<StatusDot connected={connected} />
{!connected &&
(canConnect ? (
<PillButton onClick={onConnect} disabled={busy}>
{busy && <Loader2 className="size-3.5 animate-spin" />}
Connect
</PillButton>
) : lockedHint ? (
<span className="flex items-center gap-1 text-[12px] font-medium text-[#737373]">
<Lock className="size-3" />
{lockedHint}
</span>
) : null)}
</div>
</div>
)
}
function Section({
title,
description,
children,
}: {
title: string
description: string
children: React.ReactNode
}) {
return (
<div className="space-y-3">
<div className="px-1">
<p
className={cn(
dmSans125ClassName(),
"font-semibold text-[15px] tracking-[-0.15px] text-[#FAFAFA]",
)}
>
{title}
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-0.5 text-[13px] font-medium text-[#737373]",
)}
>
{description}
</p>
</div>
{children}
</div>
)
}
function CardSkeleton() {
return (
<div className="flex items-center gap-3 rounded-[14px] bg-[#14161A] p-5 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="size-10 animate-pulse rounded-[10px] bg-[#1c1f24]" />
<div className="space-y-2">
<div className="h-3.5 w-24 animate-pulse rounded bg-[#1c1f24]" />
<div className="h-3 w-40 animate-pulse rounded bg-[#1c1f24]" />
</div>
</div>
)
}
export default function CompanyBrainConnections() {
const isCompanyBrain = useHasCompanyBrain()
const [rows, setRows] = useState<ConnRow[] | null>(null)
const [busy, setBusy] = useState<string | null>(null)
const roleQuery = useQuery({
queryKey: ["company-brain-connections", "role"],
queryFn: async () =>
(await authClient.organization.getActiveMember()).data?.role ?? null,
staleTime: 60_000,
enabled: isCompanyBrain,
})
const role = (roleQuery.data ?? "").toLowerCase()
const isAdmin = role === "owner" || role === "admin"
const load = useCallback(async () => {
const res = await fetch(`${BACKEND}/brain/connections`, {
credentials: "include",
})
if (res.ok)
setRows(((await res.json()) as { toolkits: ConnRow[] }).toolkits)
}, [])
useEffect(() => {
if (!isCompanyBrain) return
void load()
const onFocus = () => void load()
window.addEventListener("focus", onFocus)
return () => window.removeEventListener("focus", onFocus)
}, [isCompanyBrain, load])
const connect = async (toolkit: string, scope: "user" | "org") => {
setBusy(`${toolkit}:${scope}`)
try {
const res = await fetch(
`${BACKEND}/brain/connections/${toolkit}/link?scope=${scope}`,
{ method: "POST", credentials: "include" },
)
if (res.status === 403) {
toast.error("Only admins can connect the shared org account.")
return
}
if (!res.ok) {
toast.error("Couldn't start the connection.")
return
}
const data = (await res.json()) as { url?: string; error?: string }
if (data.url) window.open(data.url, "_blank", "noopener")
else toast.error(data.error ?? "Couldn't start the connection.")
} catch {
toast.error("Couldn't start the connection.")
} finally {
setBusy(null)
}
}
if (!isCompanyBrain) {
return (
<p
className={cn(
dmSans125ClassName(),
"text-[14px] font-medium text-[#737373]",
)}
>
Company Brain isn't enabled for this organization.
</p>
)
}
const loading = rows === null
return (
<div className="space-y-7">
<Section
title="Organization (shared)"
description="Connected by admins. Used for reads when you haven't connected your own."
>
{loading ? (
<CardSkeleton />
) : (
rows.map((row) => (
<AppCard
key={`org-${row.toolkit}`}
toolkit={row.toolkit}
connected={row.org}
canConnect={isAdmin}
lockedHint="Admin only"
busy={busy === `${row.toolkit}:org`}
onConnect={() => connect(row.toolkit, "org")}
/>
))
)}
</Section>
<Section
title="Your connections"
description="Your personal accounts — used for your actions and your reads."
>
{loading ? (
<CardSkeleton />
) : (
rows.map((row) => (
<AppCard
key={`user-${row.toolkit}`}
toolkit={row.toolkit}
connected={row.user}
canConnect
busy={busy === `${row.toolkit}:user`}
onConnect={() => connect(row.toolkit, "user")}
/>
))
)}
</Section>
</div>
)
}

View file

@ -10,6 +10,7 @@ import Account from "@/components/settings/account"
import Billing from "@/components/settings/billing"
import Integrations from "@/components/settings/integrations"
import ConnectionsMCP from "@/components/settings/connections-mcp"
import CompanyBrainConnections from "@/components/settings/company-brain-connections"
import Support from "@/components/settings/support"
import { ErrorBoundary } from "@/components/error-boundary"
import { useRouter } from "next/navigation"
@ -44,6 +45,7 @@ export const TABS = [
"billing",
"integrations",
"connections",
"company-brain",
"support",
] as const
export type SettingsTab = (typeof TABS)[number]
@ -80,6 +82,12 @@ const NAV_ITEMS: NavItem[] = [
description: "Drive, Notion, OneDrive, MCP",
icon: <Zap className="size-[18px]" />,
},
{
id: "company-brain",
label: "Company Brain",
description: "GitHub & Linear — org and personal",
icon: <Building2 className="size-[18px]" />,
},
{
id: "support",
label: "Support & Help",
@ -421,6 +429,7 @@ export function SettingsContent({
{activeTab === "billing" && <Billing />}
{activeTab === "integrations" && <Integrations />}
{activeTab === "connections" && <ConnectionsMCP />}
{activeTab === "company-brain" && <CompanyBrainConnections />}
{activeTab === "support" && <Support />}
</ErrorBoundary>
</section>

View file

@ -0,0 +1,106 @@
"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 [loading, setLoading] = useState(true)
useEffect(() => {
if (!isCompanyBrain) return
let active = true
;(async () => {
try {
const res = await fetch(`${BACKEND}/brain/slack/status`, {
credentials: "include",
})
if (active && res.ok) setStatus((await res.json()) as SlackStatus)
} 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>
) : (
<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>
)}
</div>
)
}

View file

@ -61,8 +61,8 @@ export interface SpaceSelectorProps {
const triggerVariants = {
default:
"h-10 min-h-10 shrink-0 rounded-full border border-[#161F2C] bg-muted px-3 gap-2 " +
"hover:bg-white/5 hover:border-[#2261CA33] " +
"h-10 min-h-10 shrink-0 rounded-full bg-muted px-3 gap-2 " +
"hover:bg-white/5 " +
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2261CA33]/35",
insideOut:
"h-10 min-h-10 gap-2 px-3 rounded-full bg-[#0D121A] shadow-inside-out hover:bg-[#121820]",

View file

@ -2,6 +2,7 @@
import { usePathname, useSearchParams } from "next/navigation"
import posthog from "posthog-js"
import { PostHogProvider as PHProvider } from "posthog-js/react"
import { Suspense, useEffect } from "react"
import { useSession } from "./auth"
@ -65,12 +66,12 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
}, [session?.user])
return (
<>
<PHProvider client={posthog}>
<Suspense fallback={null}>
{process.env.NODE_ENV === "production" && <PostHogPageTracking />}
</Suspense>
{children}
</>
</PHProvider>
)
}