mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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:
parent
7c848a5da3
commit
20e585cd00
13 changed files with 639 additions and 496 deletions
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
129
apps/web/components/configure-view.tsx
Normal file
129
apps/web/components/configure-view.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const viewLiterals = [
|
|||
"graph",
|
||||
"list",
|
||||
"integrations",
|
||||
"configure",
|
||||
"chat",
|
||||
"digests",
|
||||
// Integration sub-views — each card is its own view
|
||||
|
|
|
|||
2
bun.lock
2
bun.lock
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue