feat(web): revamp Company Brain configure page (#1306)

- New Configure tab with sidebar sections: Integrations, Models, Automations
- Merge org/personal connection tabs into one grid: per-card scope chips, admin scope dropdown, Slack as a card, custom MCP via modal
- Automations: always-visible template cards with dashed blank-create tile
- Home: Connect your tools only shows unconnected apps

Fixes ENG-1075
This commit is contained in:
MaheshtheDev 2026-07-18 18:45:02 +00:00
parent 7c848a5da3
commit 20e585cd00
13 changed files with 639 additions and 496 deletions

View file

@ -20,6 +20,7 @@ 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"
import { ConfigureView } from "@/components/configure-view"
import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
import { ChromeDetail } from "@/components/integrations/chrome-detail"
@ -698,6 +699,10 @@ export function AppExperience() {
onOpenDocument={handleOpenDocument}
/>
</div>
) : viewMode === "configure" ? (
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto p-4 pt-2! md:p-6">
<ConfigureView />
</div>
) : viewMode === "mcp" ? (
<MCPDetailView
onBack={() => void setViewMode("integrations")}

View file

@ -5,11 +5,11 @@ import { ArrowRight, Loader2 } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { dmSans125ClassName } from "@/lib/fonts"
import { useSettingsModal } from "@/components/settings/settings-modal"
import { useViewMode } from "@/lib/view-mode-context"
import { brainConnectorIcon, SlackMark } from "../brain-connector-icons"
// Apps surfaced on the dashboard; the rest live behind "More" in settings.
const FEATURED_SLUGS = ["linear", "granola", "sentry"] as const
// Preferred ordering for the dashboard; only unconnected apps are surfaced.
const FEATURED_SLUGS: readonly string[] = ["linear", "granola", "sentry"]
// Example prompts on the right card — can include apps not shown on the left.
const PREVIEW_PROMPT_SLUGS = ["linear", "granola", "github", "sentry"] as const
@ -155,76 +155,91 @@ export function ConnectionsBoard() {
}
}
const { openSettings } = useSettingsModal()
const { setViewMode } = useViewMode()
const apps = catalog ?? []
const loading = catalog === null
const featured = FEATURED_SLUGS.map((slug) =>
apps.find((a) => a.slug === slug),
).filter((a): a is CatalogEntry => Boolean(a))
const unconnected = apps.filter((a) => !isConnected(a.slug))
const featured = [
...FEATURED_SLUGS.map((slug) =>
unconnected.find((a) => a.slug === slug),
).filter((a): a is CatalogEntry => Boolean(a)),
...unconnected.filter((a) => !FEATURED_SLUGS.includes(a.slug)),
].slice(0, 3)
const overflow = unconnected.filter((a) => !featured.includes(a))
const previewApps = PREVIEW_PROMPT_SLUGS.map((slug) =>
apps.find((a) => a.slug === slug),
).filter((a): a is CatalogEntry => Boolean(a))
const remainingCount = Math.max(apps.length - featured.length, 0)
const connectedCount = apps.filter((a) => isConnected(a.slug)).length
const showBoard = loading || unconnected.length > 0
return (
<div className="space-y-4">
{slack && !slack.connected && <SlackBanner />}
<div className="grid items-start gap-4 lg:grid-cols-5">
<section
className="relative flex h-fit min-w-0 flex-col gap-2 overflow-hidden rounded-[18px] bg-[#1B1F24] p-5 lg:col-span-3"
style={cardStyle}
>
<div>
<p
className={cn(
"text-[15px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Connect your tools
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
Give your Slack agent live access to the apps your team already
uses.
</p>
</div>
<div className="overflow-hidden rounded-[12px] bg-[#14161A]">
{loading ? (
Array.from({ length: 3 }).map((_, i) => (
<TileSkeleton key={i} showDivider={i < 2} />
))
) : (
<>
{featured.map((entry, i) => (
<AppTile
key={entry.slug}
icon={brainConnectorIcon(entry.slug, entry.name, "size-5")}
name={entry.name}
subtitle={titleCase(entry.category)}
connected={isConnected(entry.slug)}
busy={busy === entry.slug}
onConnect={() => connect(entry)}
showDivider={i < featured.length - 1 || remainingCount > 0}
/>
))}
{remainingCount > 0 && (
<MoreTile
count={remainingCount}
onClick={() => openSettings("company-brain")}
/>
{showBoard ? (
<section
className="relative flex h-fit min-w-0 flex-col gap-2 overflow-hidden rounded-[18px] bg-[#1B1F24] p-5 lg:col-span-3"
style={cardStyle}
>
<div>
<p
className={cn(
"text-[15px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
</>
)}
</div>
</section>
>
Connect your tools
</p>
<p className="mt-0.5 text-[12px] font-medium text-[#737373]">
Give your Slack agent live access to the apps your team already
uses.
</p>
</div>
<div className="overflow-hidden rounded-[12px] bg-[#14161A]">
{loading ? (
Array.from({ length: 3 }).map((_, i) => (
<TileSkeleton key={i} showDivider={i < 2} />
))
) : (
<>
{featured.map((entry, i) => (
<AppTile
key={entry.slug}
icon={brainConnectorIcon(
entry.slug,
entry.name,
"size-5",
)}
name={entry.name}
subtitle={titleCase(entry.category)}
connected={isConnected(entry.slug)}
busy={busy === entry.slug}
onConnect={() => connect(entry)}
showDivider={
i < featured.length - 1 || overflow.length > 0
}
/>
))}
{overflow.length > 0 && (
<MoreTile
count={overflow.length}
names={overflow.slice(0, 3).map((a) => a.name)}
onClick={() => void setViewMode("configure")}
/>
)}
</>
)}
</div>
</section>
) : null}
<AgentPreview
apps={previewApps}
isConnected={isConnected}
connectedCount={connectedCount}
wide={!showBoard}
/>
</div>
</div>
@ -235,10 +250,12 @@ function AgentPreview({
apps,
isConnected,
connectedCount,
wide = false,
}: {
apps: CatalogEntry[]
isConnected: (slug: string) => boolean
connectedCount: number
wide?: boolean
}) {
const prompts = apps
.filter((a) => AGENT_PROMPTS[a.slug])
@ -252,7 +269,10 @@ function AgentPreview({
return (
<section
className="relative flex h-fit min-w-0 flex-col gap-2 overflow-hidden rounded-[18px] bg-[#1B1F24] p-5 lg:col-span-2"
className={cn(
"relative flex h-fit min-w-0 flex-col gap-2 overflow-hidden rounded-[18px] bg-[#1B1F24] p-5",
wide ? "lg:col-span-5" : "lg:col-span-2",
)}
style={cardStyle}
>
<div className="flex items-center gap-2">
@ -359,7 +379,15 @@ function AppTile({
)
}
function MoreTile({ count, onClick }: { count: number; onClick: () => void }) {
function MoreTile({
count,
names,
onClick,
}: {
count: number
names: string[]
onClick: () => void
}) {
return (
<button
type="button"
@ -380,7 +408,8 @@ function MoreTile({ count, onClick }: { count: number; onClick: () => void }) {
{count} more {count === 1 ? "app" : "apps"}
</p>
<p className="mt-1 truncate text-[11px] font-medium leading-none text-[#737373]">
Notion, PostHog, Plain and more
{names.join(", ")}
{count > names.length ? " and more" : ""}
</p>
</div>
<div className="flex w-[88px] shrink-0 justify-end">

View file

@ -19,11 +19,11 @@ import {
ExternalLink,
Home,
LifeBuoy,
Link2,
LayoutGrid,
MenuIcon,
SearchIcon,
Settings,
Settings2,
UserPlus,
ChevronRight,
Sun,
@ -35,7 +35,7 @@ import { DomainLogo } from "@/components/onboarding-brain/step-about"
import { FeedbackModal } from "@/components/feedback-modal"
import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge"
import { SlackMark } from "@/components/brain-connector-icons"
import { GraphIcon, IntegrationsIcon } from "@/components/integration-icons"
import { GraphIcon } from "@/components/integration-icons"
import { SpaceSelector } from "@/components/space-selector"
import { UserProfileMenu } from "@/components/user-profile-menu"
import { useTokenUsage } from "@/hooks/use-token-usage"
@ -121,7 +121,6 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
feedbackParam,
)
const [, setInvite] = useQueryState("invite")
const [settingsTab] = useQueryState("settings")
const { data: slackStatus } = useSlackStatus()
const planByOrgId = new Map(
@ -140,10 +139,10 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
?.role?.toLowerCase()
const canInvite = memberRole === "owner" || memberRole === "admin"
const isOverview = viewMode === "dashboard" && settingsTab !== "company-brain"
const isOverview = viewMode === "dashboard"
const isGraph = viewMode === "graph"
const isMemories = viewMode === "list"
const isConnections = settingsTab === "company-brain"
const isConfigure = viewMode === "configure"
const slackConnected = slackStatus?.connected ?? false
const selectOrg = useCallback(
@ -166,9 +165,9 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
void setViewMode("list")
}, [setViewMode])
const goConnections = useCallback(() => {
openSettings("company-brain")
}, [openSettings])
const goConfigure = useCallback(() => {
void setViewMode("configure")
}, [setViewMode])
const goIntegrations = useCallback(() => {
void setViewMode("integrations")
@ -184,8 +183,8 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
}, [setFeedbackOpen])
return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-1 px-2 py-2 md:gap-2 md:p-3">
<div className="z-10! flex min-w-0 flex-1 shrink items-center justify-start gap-1.5 md:flex-none md:justify-center md:gap-3">
<div className="relative z-10 flex shrink-0 items-center justify-between gap-1 px-2 py-2 md:grid md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:gap-2 md:p-3">
<div className="z-10! flex min-w-0 flex-1 shrink items-center justify-start gap-1.5 md:justify-self-start md:gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
@ -264,9 +263,9 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
Home
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={goConnections} className={menuItemClass}>
<Link2 className="size-4 text-[#737373]" />
Connections
<DropdownMenuItem onClick={goConfigure} className={menuItemClass}>
<Settings2 className="size-4 text-[#737373]" />
Configure
</DropdownMenuItem>
<DropdownMenuItem
onClick={goIntegrations}
@ -318,7 +317,7 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
</div>
{!isMobile && (
<div className="z-10! flex min-w-0 max-w-full flex-1 items-center justify-center gap-1.5 overflow-hidden px-1">
<div className="z-10! flex min-w-0 max-w-full items-center justify-center gap-1.5 overflow-hidden px-1 md:justify-self-center">
<Tooltip>
<TooltipTrigger asChild>
<button
@ -364,24 +363,24 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
<button
type="button"
role="tab"
aria-selected={isConnections}
onClick={goConnections}
className={tabClass(isConnections)}
aria-selected={isConfigure}
onClick={goConfigure}
className={tabClass(isConfigure)}
>
<IntegrationsIcon className="size-3.5 shrink-0 sm:size-4" />
Connections
<Settings2 className="size-3.5 shrink-0 sm:size-4" />
Configure
</button>
</div>
<SlackNavButton
connected={slackConnected}
teamName={slackStatus?.teamName ?? null}
active={isConnections && slackConnected}
onManage={goConnections}
active={isConfigure && slackConnected}
onManage={goConfigure}
/>
</div>
)}
<div className="z-10! flex min-w-0 shrink-0 items-center gap-1.5">
<div className="z-10! flex min-w-0 shrink-0 items-center gap-1.5 md:justify-self-end">
{isMobile ? (
<>
<SpaceSelector
@ -431,11 +430,11 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
Memories
</DropdownMenuItem>
<DropdownMenuItem
onClick={goConnections}
onClick={goConfigure}
className={menuItemClass}
>
<IntegrationsIcon className="size-4 text-[#737373]" />
Connections
<Settings2 className="size-4 text-[#737373]" />
Configure
</DropdownMenuItem>
<DropdownMenuItem
onClick={goIntegrations}
@ -446,7 +445,7 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) {
</DropdownMenuItem>
{slackConnected ? (
<DropdownMenuItem
onClick={goConnections}
onClick={goConfigure}
className={menuItemClass}
>
<SlackMark className="size-4" />

View file

@ -0,0 +1,129 @@
"use client"
import { cn } from "@lib/utils"
import { Blocks, CalendarClock, Cpu } from "lucide-react"
import { useState } from "react"
import CompanyBrainConnections from "@/components/settings/company-brain-connections"
import CompanyBrainModels from "@/components/settings/company-brain-models"
import Proactiveness from "@/components/settings/proactiveness"
import { ErrorBoundary } from "@/components/error-boundary"
import { dmSans125ClassName } from "@/lib/fonts"
type ConfigureSection = "company-brain" | "models" | "automations"
const SECTIONS: {
id: ConfigureSection
label: string
description: string
icon: typeof Blocks
}[] = [
{
id: "company-brain",
label: "Integrations",
description:
"Connect the tools your brain works with. Your account covers your own actions and reads; workspace accounts are a shared fallback.",
icon: Blocks,
},
{
id: "models",
label: "Models",
description: "Choose the models used for reasoning, triage, and research.",
icon: Cpu,
},
{
id: "automations",
label: "Automations",
description:
"Read-only scheduled summaries posted to Slack channels or DMs. You manage the ones you create.",
icon: CalendarClock,
},
]
export function ConfigureView() {
const [activeSection, setActiveSection] =
useState<ConfigureSection>("company-brain")
const active = SECTIONS.find((section) => section.id === activeSection)
if (!active) return null
return (
<div
className={cn(
dmSans125ClassName(),
"mx-auto flex min-h-full w-full max-w-[88rem] flex-col",
)}
>
<section
aria-label="Configure Company Brain"
className="flex flex-1 flex-col rounded-[14px] bg-[#191D24] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)] sm:p-6"
>
<div className="flex flex-1 flex-col gap-5 md:flex-row md:gap-8">
<nav
aria-label="Configure sections"
className="scrollbar-none flex shrink-0 gap-1 overflow-x-auto md:w-52 md:flex-col md:overflow-x-visible"
>
<p className="hidden px-3 pb-1.5 font-semibold text-[11px] text-[#5B6675] uppercase tracking-[0.08em] md:block">
Configure
</p>
{SECTIONS.map((section) => {
const isActive = section.id === activeSection
const Icon = section.icon
return (
<button
key={section.id}
type="button"
aria-current={isActive ? "page" : undefined}
onClick={() => setActiveSection(section.id)}
className={cn(
"flex shrink-0 items-center gap-2.5 rounded-[8px] px-3 py-2 text-left text-[13px] font-medium transition-colors",
isActive
? "bg-white/[0.08] text-[#FAFAFA]"
: "text-[#8B929E] hover:bg-white/[0.04] hover:text-[#FAFAFA]",
)}
>
<Icon
className={cn(
"size-4 shrink-0",
isActive ? "text-[#FAFAFA]" : "text-[#737B87]",
)}
/>
{section.label}
</button>
)
})}
</nav>
<div className="min-w-0 flex-1">
<header className="mb-5">
<h2
id="configure-section-title"
className="text-[14px] font-semibold tracking-[-0.1px] text-[#FAFAFA]"
>
{active.label}
</h2>
<p className="mt-1 text-[12px] leading-5 text-[#737B87]">
{active.description}
</p>
</header>
<ErrorBoundary
key={activeSection}
fallback={
<p className="py-6 text-center text-[13px] text-[#8B929E]">
Something went wrong loading this section.
</p>
}
>
{activeSection === "company-brain" ? (
<CompanyBrainConnections />
) : activeSection === "models" ? (
<CompanyBrainModels showHeading={false} />
) : (
<Proactiveness />
)}
</ErrorBoundary>
</div>
</div>
</section>
</div>
)
}

View file

@ -13,7 +13,7 @@ import {
brainConnectorIcon,
SlackMark,
} from "@/components/brain-connector-icons"
import { useSettingsModal } from "@/components/settings/settings-modal"
import { useRouter } from "next/navigation"
import { useResearchStatus } from "@/hooks/use-research-status"
import { dmSans125ClassName } from "@/lib/fonts"
import { cardSurfaceStyle, inputBevelStyle, inputClass } from "./step-about"
@ -83,7 +83,7 @@ export function ResearchActionRail({
onStatsChange?: (stats: ParallelSetupStats) => void
}) {
const { org } = useAuth()
const { openSettings } = useSettingsModal()
const router = useRouter()
const { events } = useResearchStatus()
const [catalog, setCatalog] = useState<CatalogEntry[] | null>(null)
const [rows, setRows] = useState<ConnRow[]>([])
@ -448,7 +448,7 @@ export function ResearchActionRail({
onConnect={connect}
onBrowse={() => {
pauseRotation()
openSettings("company-brain")
router.push("/?view=configure")
}}
/>
)}

View file

@ -8,26 +8,19 @@ import {
CalendarClock,
ChevronDown,
ChevronUp,
FilePlus2,
FileText,
GitPullRequest,
Info,
LifeBuoy,
ListTodo,
Loader2,
Lock,
MessageCircleQuestion,
Plus,
Radar,
Trash2,
} from "lucide-react"
import { useRef, useState } from "react"
import { toast } from "sonner"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import {
Select,
SelectContent,
@ -42,7 +35,6 @@ import {
TooltipTrigger,
} from "@ui/components/tooltip"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { useOrgMemberRole } from "@/hooks/use-org-member-role"
import { dmSans125ClassName } from "@/lib/fonts"
const BACKEND =
@ -167,7 +159,7 @@ const PRESETS: Preset[] = [
{
id: "standup",
category: "team",
label: "Daily standup",
label: "Morning checkup",
description:
"Shipped work, decisions, blockers & open questions from the last 24h.",
icon: CalendarClock,
@ -179,7 +171,7 @@ const PRESETS: Preset[] = [
{
id: "weekly-recap",
category: "team",
label: "Weekly team recap",
label: "Company progress",
description:
"Decisions, shipped work & unresolved threads from the past week.",
icon: CalendarClock,
@ -236,6 +228,18 @@ const PRESETS: Preset[] = [
frequency: "daily",
time: "09:00",
},
{
id: "competitor-check",
category: "product",
label: "Competitor check",
description: "What competitors shipped, announced, or changed this week.",
icon: Radar,
prompt:
"Check what our competitors shipped, announced, or changed recently — launches, pricing changes, and anything the team should react to.",
frequency: "weekly",
weekday: 1,
time: "09:00",
},
{
id: "release-notes",
category: "product",
@ -273,38 +277,12 @@ function sortPresets(connected: Set<string>): Preset[] {
return [...PRESETS].sort((a, b) => rank(a) - rank(b))
}
// Up to 3 presets spanning distinct categories (connection-preferred order).
function galleryPresets(sorted: Preset[]): Preset[] {
const seen = new Set<Category>()
const out: Preset[] = []
for (const p of sorted) {
if (seen.has(p.category)) continue
seen.add(p.category)
out.push(p)
if (out.length === 3) break
}
return out
}
function cadenceLabel(p: Preset): string {
if (p.frequency === "weekly")
return `Weekly · ${WEEKDAYS[p.weekday ?? 1] ?? "Mon"} ${p.time}`
return `Daily · ${p.time}`
}
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<p
className={cn(
dmSans125ClassName(),
"font-semibold text-[14px] tracking-[-0.14px] text-[#FAFAFA]",
)}
>
{children}
</p>
)
}
const controlClass = cn(
dmSans125ClassName(),
"h-9 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] px-3 text-[13px] text-[#FAFAFA] outline-none disabled:opacity-50",
@ -318,9 +296,6 @@ const selectItemClass =
"cursor-pointer rounded-[8px] text-[13px] text-[#FAFAFA] hover:bg-white/10 hover:text-white data-[highlighted]:bg-white/10 data-[highlighted]:text-white focus:bg-white/10 focus:text-white"
const DM_VALUE = "__dm__"
const menuItemClass =
"cursor-pointer gap-2.5 rounded-lg px-2.5 py-2 text-[13px] font-medium text-white/85 hover:bg-white/[0.06] hover:text-white data-[highlighted]:bg-white/[0.06] data-[highlighted]:text-white focus:bg-white/[0.06] focus:text-white"
function AutomationCard({
initial,
id,
@ -830,7 +805,7 @@ function AutomationRow({
)
}
function PresetTile({
function PresetCard({
preset,
onPick,
}: {
@ -844,19 +819,31 @@ function PresetTile({
onClick={onPick}
className={cn(
dmSans125ClassName(),
"flex items-center gap-3 rounded-[10px] border border-white/[0.06] bg-[#14161A] px-3 py-2.5 text-left transition-colors hover:border-white/[0.12] hover:bg-[#171A1F]",
"flex min-w-0 cursor-pointer flex-col justify-between gap-3 rounded-xl bg-[#14161A] p-4 text-left",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)] transition-colors hover:bg-[#171A1F]",
)}
title={preset.description}
>
<Icon className="size-4 shrink-0 text-[#9A9A9A]" />
<span className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-[13px] font-medium text-[#FAFAFA]">
{preset.label}
</span>
<span className="truncate text-[11px] text-[#6B6B6B]">
<div className="flex min-w-0 items-start 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)]">
<Icon className="size-4 text-[#9A9A9A]" />
</div>
<div className="min-w-0 pt-0.5">
<p className="truncate font-semibold text-[14px] tracking-[-0.15px] text-[#FAFAFA]">
{preset.label}
</p>
<p className="mt-1 line-clamp-2 break-words text-[12px] font-medium leading-5 text-[#737373]">
{preset.description}
</p>
</div>
</div>
<div className="flex min-h-9 items-center justify-between gap-3 border-[#1E293B]/50 border-t pt-3">
<span className="text-[12px] font-medium text-[#737373]">
{cadenceLabel(preset)}
</span>
</span>
<span className="text-[12px] font-medium text-[#8B929E]">
Use template
</span>
</div>
</button>
)
}
@ -873,8 +860,6 @@ export default function CompanyBrainAutomations() {
const removeDraft = (key: number) =>
setDrafts((d) => d.filter((x) => x.key !== key))
const { isAdmin } = useOrgMemberRole(isCompanyBrain)
const listQuery = useQuery({
queryKey: ["company-brain-automations", "list", org?.id],
queryFn: async () => {
@ -926,71 +911,12 @@ export default function CompanyBrainAutomations() {
queryKey: ["company-brain-automations", "list"],
})
}
const showGallery = automations.length === 0 && drafts.length === 0
const usedTitles = new Set(automations.map((a) => a.title))
const availablePresets = presets.filter((p) => !usedTitles.has(p.label))
const hasList = automations.length > 0 || drafts.length > 0
return (
<section className="flex flex-col gap-3 px-1">
<div className="flex flex-col gap-0.5">
<div className="flex items-center justify-between gap-3">
<SectionTitle>Channel automations</SectionTitle>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
dmSans125ClassName(),
"inline-flex h-8 items-center gap-1.5 rounded-full border border-white/10 px-3 text-[12px] font-medium text-[#9A9A9A] transition-colors hover:bg-white/[0.04] hover:text-[#FAFAFA]",
)}
>
<Plus className="size-3.5" />
New automation
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={cn(
dmSans125ClassName(),
"min-w-[240px] rounded-xl border-white/[0.08] bg-[#1B1F24] p-1.5",
)}
>
<DropdownMenuItem
onClick={() => addDraft(emptyDraft())}
className={menuItemClass}
>
<FilePlus2 className="size-4 text-[#737373]" />
Blank automation
</DropdownMenuItem>
{presets.map((p) => {
const Icon = p.icon
return (
<DropdownMenuItem
key={p.id}
onClick={() => addDraft(presetToDraft(p))}
className={menuItemClass}
>
<Icon className="size-4 text-[#737373]" />
{p.label}
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<span
className={cn(
dmSans125ClassName(),
"flex items-center gap-1.5 text-[12px] text-[#6B6B6B]",
)}
>
<Lock className="size-3 shrink-0" />
Read-only scheduled summaries posted to a channel.{" "}
{isAdmin
? "You manage all across the org."
: "You manage the ones you create."}
</span>
</div>
<div className="flex flex-col gap-3">
{automations.map((a) =>
openId === a.id ? (
@ -1035,40 +961,37 @@ export default function CompanyBrainAutomations() {
/>
))}
{showGallery ? (
<div className="flex flex-col gap-2">
<p
className={cn(dmSans125ClassName(), "text-[12px] text-[#6B6B6B]")}
>
Start from a template:
</p>
<div className="flex flex-col gap-2">
{galleryPresets(presets).map((p) => (
<PresetTile
key={p.id}
preset={p}
onPick={() => addDraft(presetToDraft(p))}
/>
))}
<button
type="button"
onClick={() => addDraft(emptyDraft())}
className={cn(
dmSans125ClassName(),
"flex items-center justify-center gap-2 rounded-[10px] border border-dashed border-white/[0.1] bg-transparent px-3 py-2.5 text-[13px] text-[#9A9A9A] transition-colors hover:border-white/20 hover:text-[#FAFAFA]",
)}
>
<Plus className="size-4" />
Start from scratch
</button>
</div>
<p
className={cn(dmSans125ClassName(), "text-[11px] text-[#5A5A5A]")}
>
More templates in the New automation menu.
</p>
</div>
{hasList ? (
<p
className={cn(
dmSans125ClassName(),
"pt-2 text-[12px] font-medium text-[#6B6B6B]",
)}
>
Templates
</p>
) : null}
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{availablePresets.map((p) => (
<PresetCard
key={p.id}
preset={p}
onPick={() => addDraft(presetToDraft(p))}
/>
))}
<button
type="button"
onClick={() => addDraft(emptyDraft())}
className={cn(
dmSans125ClassName(),
"flex min-h-[104px] cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#2A313C] border-dashed",
"text-[13px] font-medium text-[#737B87] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]",
)}
>
<Plus className="size-4" />
New automation
</button>
</div>
</div>
</section>
)

View file

@ -2,8 +2,21 @@
import { useOrgMemberRole } from "@/hooks/use-org-member-role"
import { cn } from "@lib/utils"
import { Loader2, Lock } from "lucide-react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { ChevronDown, Loader2, Plus, XIcon } from "lucide-react"
import { useCallback, useEffect, useState } from "react"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@ui/components/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import { toast } from "sonner"
import { dmSans125ClassName } from "@/lib/fonts"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
@ -32,7 +45,6 @@ type ConnRow = {
userId: string | null
}
type SlackStatus = { connected: boolean; teamName: string | null }
type Scope = "org" | "user"
function titleCase(s: string) {
return s.replace(/\b\w/g, (c) => c.toUpperCase())
@ -47,28 +59,20 @@ function slugifyMcpName(value: string) {
.slice(0, 63)
}
function SecondaryButton({
children,
href,
}: {
children: React.ReactNode
href: string
}) {
return (
<a
href={href}
className={cn(
dmSans125ClassName(),
"inline-flex shrink-0 items-center justify-center gap-2 rounded-full border border-[#1E293B] bg-[#0D121A] px-4 h-9",
"text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#1E293B]",
)}
>
{children}
</a>
)
}
const pillLinkClass = cn(
"relative flex h-8 min-w-[94px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 sm:h-9 sm:min-w-[116px] sm:px-5",
"text-[12px] font-medium text-[#FAFAFA] sm:text-[14px]",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
"cursor-pointer transition-opacity hover:opacity-80",
)
function StatusDot({ connected }: { connected: boolean }) {
function ScopeChip({
label,
connected,
}: {
label: string
connected: boolean
}) {
return (
<span
className={cn(
@ -83,19 +87,22 @@ function StatusDot({ connected }: { connected: boolean }) {
connected ? "bg-[#00AC3F]" : "bg-[#3A4150]",
)}
/>
{connected ? "Connected" : "Not connected"}
{label}
</span>
)
}
const menuItemClass =
"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"
function AppCard({
name,
subtitle,
icon,
connected,
canConnect,
canDisconnect,
lockedHint,
userConnected,
orgConnected,
isAdmin,
personalOnly,
busy,
onConnect,
onDisconnect,
@ -103,16 +110,20 @@ function AppCard({
name: string
subtitle: string
icon: React.ReactNode
connected: boolean
canConnect: boolean
canDisconnect: boolean
lockedHint?: string
userConnected: boolean
orgConnected: boolean
isAdmin: boolean
personalOnly?: boolean
busy: boolean
onConnect: () => void
onDisconnect: () => void
onConnect: (shared: boolean) => void
onDisconnect: (shared: boolean) => void
}) {
const anyConnected = userConnected || orgConnected
const showOrgChip = !personalOnly && (orgConnected || isAdmin)
const adminMenu = isAdmin && !personalOnly
return (
<div className="flex min-h-[152px] min-w-0 flex-col justify-between gap-4 rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="flex min-w-0 flex-col justify-between gap-3 rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] bg-[#080B0F] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)]">
{icon}
@ -137,67 +148,154 @@ function AppCard({
</div>
</div>
<div className="flex min-h-9 items-center justify-between gap-3 border-[#1E293B]/50 border-t pt-3">
<StatusDot connected={connected} />
{connected && canDisconnect ? (
<PillButton onClick={onDisconnect} disabled={busy}>
<div className="flex min-w-0 items-center gap-3">
{personalOnly || !anyConnected ? (
<ScopeChip
label={userConnected ? "Connected" : "Not connected"}
connected={userConnected}
/>
) : (
<>
<ScopeChip label="You" connected={userConnected} />
{showOrgChip ? (
<ScopeChip label="Workspace" connected={orgConnected} />
) : null}
</>
)}
</div>
{adminMenu ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
disabled={busy}
className={cn(
dmSans125ClassName(),
"relative flex h-8 min-w-[94px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 sm:h-9 sm:min-w-[116px] sm:px-5",
"text-[12px] font-medium text-[#FAFAFA] sm:text-[14px]",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
"cursor-pointer transition-opacity hover:opacity-80",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
>
{busy ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<>
{anyConnected ? "Manage" : "Connect"}
<ChevronDown className="size-3.5 text-[#737373]" />
</>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className={cn(
dmSans125ClassName(),
"min-w-[220px] rounded-xl border border-white/[0.08] p-1.5 shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
)}
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
<DropdownMenuItem
className={menuItemClass}
onClick={() =>
userConnected ? onDisconnect(false) : onConnect(false)
}
>
{userConnected ? "Disconnect my account" : "Connect my account"}
</DropdownMenuItem>
<DropdownMenuItem
className={menuItemClass}
onClick={() =>
orgConnected ? onDisconnect(true) : onConnect(true)
}
>
{orgConnected
? "Disconnect workspace"
: "Connect for workspace"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : userConnected ? (
<PillButton onClick={() => onDisconnect(false)} disabled={busy}>
{busy && <Loader2 className="size-3.5 animate-spin" />}
Disconnect
</PillButton>
) : (
!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)
) : personalOnly ? null : (
<PillButton onClick={() => onConnect(false)} disabled={busy}>
{busy && <Loader2 className="size-3.5 animate-spin" />}
Connect
</PillButton>
)}
</div>
</div>
)
}
function ScopeToggle({
scope,
onChange,
function SlackCard({
status,
isAdmin,
installHref,
}: {
scope: Scope
onChange: (s: Scope) => void
status: SlackStatus | null
isAdmin: boolean
installHref: string
}) {
const items: { id: Scope; label: string }[] = [
{ id: "org", label: "Organization" },
{ id: "user", label: "Personal" },
]
const connected = status?.connected ?? false
return (
<div className="inline-flex rounded-full border border-[#1E293B] bg-[#0D121A] p-1">
{items.map((it) => (
<button
key={it.id}
type="button"
onClick={() => onChange(it.id)}
className={cn(
dmSans125ClassName(),
"rounded-full px-4 h-8 text-[13px] font-medium transition-colors",
scope === it.id
? "bg-[#1E293B] text-[#FAFAFA]"
: "text-[#737373] hover:text-[#FAFAFA]",
)}
>
{it.label}
</button>
))}
<div className="flex min-w-0 flex-col justify-between gap-3 rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] bg-[#080B0F] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)]">
<SlackMark className="size-5" />
</div>
<div className="min-w-0 pt-0.5">
<p
className={cn(
dmSans125ClassName(),
"truncate font-semibold text-[14px] tracking-[-0.15px] text-[#FAFAFA]",
)}
>
Slack
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-1 line-clamp-2 break-words text-[12px] font-medium leading-5 text-[#737373]",
)}
>
Messaging
</p>
</div>
</div>
<div className="flex min-h-9 items-center justify-between gap-3 border-[#1E293B]/50 border-t pt-3">
<ScopeChip
label={
connected
? status?.teamName
? `Workspace · ${status.teamName}`
: "Workspace"
: "Not connected"
}
connected={connected}
/>
{isAdmin ? (
<a
href={installHref}
className={cn(dmSans125ClassName(), pillLinkClass)}
>
{connected ? "Reconnect" : "Connect"}
</a>
) : null}
</div>
</div>
)
}
function RowSkeleton() {
return (
<div className="min-h-[152px] rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="flex items-center gap-3">
<div className="size-10 animate-pulse rounded-[10px] bg-[#1c1f24]" />
<div className="space-y-2">
@ -205,7 +303,7 @@ function RowSkeleton() {
<div className="h-2.5 w-32 animate-pulse rounded bg-[#1c1f24]" />
</div>
</div>
<div className="mt-8 h-8 w-28 animate-pulse rounded-full bg-[#1c1f24] ml-auto" />
<div className="mt-5 h-8 w-28 animate-pulse rounded-full bg-[#1c1f24] ml-auto" />
</div>
)
}
@ -218,7 +316,7 @@ export default function CompanyBrainConnections() {
const [rows, setRows] = useState<ConnRow[]>([])
const [slackStatus, setSlackStatus] = useState<SlackStatus | null>(null)
const [busy, setBusy] = useState<string | null>(null)
const [scope, setScope] = useState<Scope>("user")
const [customOpen, setCustomOpen] = useState(false)
const [customName, setCustomName] = useState("")
const [customServerUrl, setCustomServerUrl] = useState("")
@ -379,8 +477,12 @@ export default function CompanyBrainConnections() {
}
if (data.authUrl) {
window.open(data.authUrl, "_blank", "noopener")
setCustomOpen(false)
setCustomName("")
setCustomServerUrl("")
} else if (data.ok) {
toast.success(`${slug} connected.`)
setCustomOpen(false)
setCustomName("")
setCustomServerUrl("")
await load()
@ -452,43 +554,9 @@ export default function CompanyBrainConnections() {
!catalogSlugs.has(row.serverSlug),
)
: []
const shared = scope === "org"
const description = shared
? "Connected by admins. Used for reads when you haven't connected your own."
: "Your personal accounts, used for your actions and your reads."
return (
<div className="space-y-5">
<div className="flex items-center justify-between gap-3">
<ScopeToggle scope={scope} onChange={setScope} />
{slackStatus?.connected && slackStatus.teamName ? (
<p
className={cn(
dmSans125ClassName(),
"ml-auto text-[13px] font-medium text-[#737373]",
)}
>
Slack · {slackStatus.teamName}
</p>
) : null}
{isAdmin ? (
<SecondaryButton href={`${BACKEND}/brain/slack/oauth/install`}>
<SlackMark className="size-4" />
Reconnect Slack
</SecondaryButton>
) : null}
</div>
<p
className={cn(
dmSans125ClassName(),
"px-1 text-[13px] font-medium text-[#737373]",
)}
>
{description}
</p>
<div className="grid gap-3 md:grid-cols-2">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{loading ? (
<>
<RowSkeleton />
@ -497,100 +565,128 @@ export default function CompanyBrainConnections() {
</>
) : (
<>
{!shared && isStaff ? (
<form
onSubmit={connectCustom}
className="flex min-h-[152px] min-w-0 flex-col justify-between gap-3 rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
>
<div>
<p
className={cn(
dmSans125ClassName(),
"text-[14px] font-semibold text-[#FAFAFA]",
)}
>
Custom MCP server
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-1 line-clamp-2 text-[12px] font-medium text-[#737373]",
)}
>
Add a personal OAuth MCP server by URL.
</p>
</div>
<div className="space-y-2">
<input
value={customName}
onChange={(event) => setCustomName(event.target.value)}
placeholder="Name"
className="h-8 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3 text-[12px] font-medium text-[#FAFAFA] outline-none placeholder:text-[#5F6673] focus:border-[#334155]"
/>
<input
value={customServerUrl}
onChange={(event) => setCustomServerUrl(event.target.value)}
placeholder="https://example.com/mcp"
className="h-8 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3 text-[12px] font-medium text-[#FAFAFA] outline-none placeholder:text-[#5F6673] focus:border-[#334155]"
/>
<div className="flex justify-end border-[#1E293B]/50 border-t pt-3">
<PillButton
type="submit"
disabled={busy?.startsWith("custom:") ?? false}
>
{busy?.startsWith("custom:") && (
<Loader2 className="size-3.5 animate-spin" />
)}
Connect
</PillButton>
</div>
</div>
</form>
) : null}
<SlackCard
status={slackStatus}
isAdmin={isAdmin}
installHref={`${BACKEND}/brain/slack/oauth/install`}
/>
{apps.map((entry) => (
<AppCard
key={`${scope}-${entry.slug}`}
key={entry.slug}
name={entry.name}
subtitle={titleCase(entry.category)}
icon={brainConnectorIcon(entry.slug, entry.name)}
connected={isConnected(entry.slug, shared)}
canConnect={shared ? isAdmin : true}
canDisconnect={shared ? isAdmin : true}
lockedHint={shared ? "Admin only" : undefined}
busy={busy === `${entry.slug}:${scope}`}
onConnect={() => connect(entry, shared)}
onDisconnect={() => disconnect(entry, shared)}
userConnected={isConnected(entry.slug, false)}
orgConnected={isConnected(entry.slug, true)}
isAdmin={isAdmin}
busy={busy?.startsWith(`${entry.slug}:`) ?? false}
onConnect={(shared) => connect(entry, shared)}
onDisconnect={(shared) => disconnect(entry, shared)}
/>
))}
{!shared &&
customRows.map((row) => (
<AppCard
key={`custom-${row.serverSlug}`}
name={titleCase(row.serverSlug.replace(/-/g, " "))}
subtitle={row.serverUrl ?? "Custom OAuth MCP"}
icon={brainConnectorIcon(row.serverSlug, row.serverSlug)}
connected
canConnect={false}
canDisconnect
busy={busy === `${row.serverSlug}:user`}
onConnect={() => {}}
onDisconnect={() =>
disconnect(
{
slug: row.serverSlug,
name: titleCase(row.serverSlug.replace(/-/g, " ")),
category: "Custom OAuth MCP",
authType: "oauth",
},
false,
)
}
/>
))}
{customRows.map((row) => (
<AppCard
key={`custom-${row.serverSlug}`}
name={titleCase(row.serverSlug.replace(/-/g, " "))}
subtitle={row.serverUrl ?? "Custom OAuth MCP"}
icon={brainConnectorIcon(row.serverSlug, row.serverSlug)}
userConnected
orgConnected={false}
isAdmin={false}
personalOnly
busy={busy === `${row.serverSlug}:user`}
onConnect={() => {}}
onDisconnect={() =>
disconnect(
{
slug: row.serverSlug,
name: titleCase(row.serverSlug.replace(/-/g, " ")),
category: "Custom OAuth MCP",
authType: "oauth",
},
false,
)
}
/>
))}
{isStaff ? (
<button
type="button"
onClick={() => setCustomOpen(true)}
className={cn(
dmSans125ClassName(),
"flex min-h-[104px] cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#2A313C] border-dashed",
"text-[13px] font-medium text-[#737B87] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]",
)}
>
<Plus className="size-4" />
Add custom MCP
</button>
) : null}
</>
)}
</div>
<Dialog open={customOpen} onOpenChange={setCustomOpen}>
<DialogContent
className={cn(
"w-[90%]! max-w-[440px]! flex flex-col gap-4 rounded-[22px] border-none bg-[#1B1F24] p-4",
dmSans125ClassName(),
)}
style={{
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",
}}
showCloseButton={false}
>
<div className="flex items-start justify-between gap-4">
<DialogHeader className="flex-1 space-y-1 pl-1">
<DialogTitle className="font-semibold text-[#FAFAFA]">
Custom MCP server
</DialogTitle>
<p className="text-[13px] font-medium leading-[1.35] text-[#737373]">
Add a personal OAuth MCP server by URL.
</p>
</DialogHeader>
<DialogPrimitive.Close
className="flex size-7 shrink-0 items-center justify-center rounded-full border border-[rgba(115,115,115,0.2)] bg-[#0D121A] transition-opacity hover:opacity-100 focus:outline-hidden"
style={{
boxShadow:
"0 0.711px 2.842px 0 rgba(0, 0, 0, 0.25), 0.178px 0.178px 0.178px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
<XIcon className="size-4 text-[#737373]" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
<form onSubmit={connectCustom} className="flex flex-col gap-2">
<input
value={customName}
onChange={(event) => setCustomName(event.target.value)}
placeholder="Name"
className="h-9 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3.5 text-[13px] font-medium text-[#FAFAFA] outline-none placeholder:text-[#5F6673] focus:border-[#334155]"
/>
<input
value={customServerUrl}
onChange={(event) => setCustomServerUrl(event.target.value)}
placeholder="https://example.com/mcp"
className="h-9 w-full rounded-full border border-[#1E293B] bg-[#0D121A] px-3.5 text-[13px] font-medium text-[#FAFAFA] outline-none placeholder:text-[#5F6673] focus:border-[#334155]"
/>
<div className="flex justify-end pt-2">
<PillButton
type="submit"
disabled={busy?.startsWith("custom:") ?? false}
>
{busy?.startsWith("custom:") && (
<Loader2 className="size-3.5 animate-spin" />
)}
Connect
</PillButton>
</div>
</form>
</DialogContent>
</Dialog>
</div>
)
}

View file

@ -75,7 +75,11 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
)
}
export default function CompanyBrainModels() {
export default function CompanyBrainModels({
showHeading = true,
}: {
showHeading?: boolean
}) {
const isCompanyBrain = useHasCompanyBrain()
const { isAdmin } = useOrgMemberRole(isCompanyBrain)
@ -106,15 +110,17 @@ export default function CompanyBrainModels() {
return (
<section className="flex flex-col gap-4 px-1">
<div className="flex flex-col gap-0.5">
<SectionTitle>Models</SectionTitle>
<span
className={cn(dmSans125ClassName(), "text-[12px] text-[#9A9A9A]")}
>
Choose which models Company Brain uses. Applies to this organization
only.
</span>
</div>
{showHeading ? (
<div className="flex flex-col gap-0.5">
<SectionTitle>Models</SectionTitle>
<span
className={cn(dmSans125ClassName(), "text-[12px] text-[#9A9A9A]")}
>
Choose which models Company Brain uses. Applies to this organization
only.
</span>
</div>
) : null}
{modelsQuery.isLoading ? (
<div className="flex items-center gap-2 text-[13px] text-[#9A9A9A]">

View file

@ -10,10 +10,6 @@ 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 { ProactivenessIcon } from "@/components/settings/proactiveness-icon"
import Proactiveness from "@/components/settings/proactiveness"
import CompanyBrainModels from "@/components/settings/company-brain-models"
import Support from "@/components/settings/support"
import { ErrorBoundary } from "@/components/error-boundary"
import { useRouter } from "next/navigation"
@ -31,7 +27,6 @@ import {
Zap,
HelpCircle,
CreditCard,
Cpu,
ShieldAlert,
ChevronRight,
ArrowUpRight,
@ -56,9 +51,6 @@ export const TABS = [
"billing",
"integrations",
"connections",
"company-brain",
"company-brain-models",
"proactiveness",
"support",
] as const
export type SettingsTab = (typeof TABS)[number]
@ -95,24 +87,6 @@ const NAV_ITEMS: NavItem[] = [
description: "Drive, Notion, OneDrive, MCP",
icon: <Zap className="size-[18px]" />,
},
{
id: "company-brain",
label: "Company Brain",
description: "Connect apps to your brain — org and personal",
icon: <Building2 className="size-[18px]" />,
},
{
id: "company-brain-models",
label: "Models",
description: "Choose the models your brain uses",
icon: <Cpu className="size-[18px]" />,
},
{
id: "proactiveness",
label: "Proactiveness",
description: "Scheduled digests and unprompted actions",
icon: <ProactivenessIcon className="size-[18px]" />,
},
{
id: "support",
label: "Support & Help",
@ -175,12 +149,12 @@ export function SettingsContent({
const { user, org, organizations, setActiveOrg, clearActiveOrg } = useAuth()
const isCompanyBrain = useHasCompanyBrain()
// Company Brain orgs manage tools inside Company Brain; hide the generic tabs.
// Company Brain orgs manage tools in the Configure view; hide the generic tabs.
const navItems = isCompanyBrain
? NAV_ITEMS.filter(
(item) => item.id !== "integrations" && item.id !== "connections",
)
: NAV_ITEMS.filter((item) => item.id !== "company-brain-models")
: NAV_ITEMS
const router = useRouter()
const isMobile = useIsMobile()
const localStorageUsername = useLocalStorageUsername()
@ -507,9 +481,6 @@ export function SettingsContent({
{activeTab === "billing" && <Billing />}
{activeTab === "integrations" && <Integrations />}
{activeTab === "connections" && <ConnectionsMCP />}
{activeTab === "company-brain" && <CompanyBrainConnections />}
{activeTab === "company-brain-models" && <CompanyBrainModels />}
{activeTab === "proactiveness" && <Proactiveness />}
{activeTab === "support" && <Support />}
</ErrorBoundary>
</section>

View file

@ -15,10 +15,10 @@ import { useRouter } from "next/navigation"
import {
LogOut,
Settings,
Settings2,
RotateCcw,
HelpCircle,
LifeBuoy,
Building2,
Sun,
} from "lucide-react"
import { cn } from "@lib/utils"
@ -26,7 +26,6 @@ import { dmSansClassName } from "@/lib/fonts"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { useTokenUsage } from "@/hooks/use-token-usage"
import { useSettingsModal } from "@/components/settings/settings-modal"
import { ProactivenessIcon } from "@/components/settings/proactiveness-icon"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import { useViewMode } from "@/lib/view-mode-context"
@ -165,20 +164,13 @@ export function UserProfileMenu({
<Settings className="size-4 text-[#737373]" />
Settings
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => openSettings("company-brain")}
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"
>
<Building2 className="size-4 text-[#737373]" />
Company Brain
</DropdownMenuItem>
{isCompanyBrain ? (
<DropdownMenuItem
onClick={() => openSettings("proactiveness")}
onClick={() => void setViewMode("configure")}
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"
>
<ProactivenessIcon className="size-4 text-[#737373]" />
Proactiveness
<Settings2 className="size-4 text-[#737373]" />
Configure
</DropdownMenuItem>
) : null}
{isCompanyBrain ? (

View file

@ -222,15 +222,7 @@ export const analytics = {
// settings / spaces / docs analytics
settingsTabChanged: (props: {
tab:
| "account"
| "billing"
| "integrations"
| "connections"
| "company-brain"
| "company-brain-models"
| "proactiveness"
| "support"
tab: "account" | "billing" | "integrations" | "connections" | "support"
}) => safeCapture("settings_tab_changed", props),
spaceCreated: () => safeCapture("space_created"),

View file

@ -28,6 +28,7 @@ const viewLiterals = [
"graph",
"list",
"integrations",
"configure",
"chat",
"digests",
// Integration sub-views — each card is its own view

View file

@ -320,7 +320,7 @@
},
"packages/tools": {
"name": "@supermemory/tools",
"version": "2.0.0",
"version": "2.1.1",
"dependencies": {
"@ai-sdk/anthropic": "^2.0.25",
"@ai-sdk/openai": "^2.0.23",