diff --git a/apps/web/components/settings/company-brain-connections.tsx b/apps/web/components/settings/company-brain-connections.tsx new file mode 100644 index 00000000..734dabfc --- /dev/null +++ b/apps/web/components/settings/company-brain-connections.tsx @@ -0,0 +1,291 @@ +"use client" + +import { authClient } from "@lib/auth" +import { cn } from "@lib/utils" +import { useQuery } from "@tanstack/react-query" +import { Check, Loader2, Lock } from "lucide-react" +import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" +import { dmSans125ClassName } from "@/lib/fonts" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" +import { PillButton } from "../integrations/install-steps" + +const BACKEND = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +type ConnRow = { toolkit: string; org: boolean; user: boolean } + +function GithubMark({ className }: { className?: string }) { + return ( + + GitHub + + + ) +} + +function LinearMark({ className }: { className?: string }) { + return ( + + Linear + + + ) +} + +const TOOLKITS: Record< + string, + { label: string; subtitle: string; icon: React.ReactNode } +> = { + github: { + label: "GitHub", + subtitle: "Repos, pull requests and issues", + icon: , + }, + linear: { + label: "Linear", + subtitle: "Issues, projects and cycles", + icon: , + }, +} + +function StatusDot({ connected }: { connected: boolean }) { + return ( + + + {connected ? "Connected" : "Not connected"} + + ) +} + +function AppCard({ + toolkit, + connected, + canConnect, + lockedHint, + busy, + onConnect, +}: { + toolkit: string + connected: boolean + canConnect: boolean + lockedHint?: string + busy: boolean + onConnect: () => void +}) { + const meta = TOOLKITS[toolkit] ?? { + label: toolkit, + subtitle: "", + icon: null, + } + return ( +
+
+
+ {meta.icon} +
+
+

+ {meta.label} +

+

+ {meta.subtitle} +

+
+
+
+ + {!connected && + (canConnect ? ( + + {busy && } + Connect + + ) : lockedHint ? ( + + + {lockedHint} + + ) : null)} +
+
+ ) +} + +function Section({ + title, + description, + children, +}: { + title: string + description: string + children: React.ReactNode +}) { + return ( +
+
+

+ {title} +

+

+ {description} +

+
+ {children} +
+ ) +} + +function CardSkeleton() { + return ( +
+
+
+
+
+
+
+ ) +} + +export default function CompanyBrainConnections() { + const isCompanyBrain = useHasCompanyBrain() + const [rows, setRows] = useState(null) + const [busy, setBusy] = useState(null) + + const roleQuery = useQuery({ + queryKey: ["company-brain-connections", "role"], + queryFn: async () => + (await authClient.organization.getActiveMember()).data?.role ?? null, + staleTime: 60_000, + enabled: isCompanyBrain, + }) + const role = (roleQuery.data ?? "").toLowerCase() + const isAdmin = role === "owner" || role === "admin" + + const load = useCallback(async () => { + const res = await fetch(`${BACKEND}/brain/connections`, { + credentials: "include", + }) + if (res.ok) + setRows(((await res.json()) as { toolkits: ConnRow[] }).toolkits) + }, []) + + useEffect(() => { + if (!isCompanyBrain) return + void load() + const onFocus = () => void load() + window.addEventListener("focus", onFocus) + return () => window.removeEventListener("focus", onFocus) + }, [isCompanyBrain, load]) + + const connect = async (toolkit: string, scope: "user" | "org") => { + setBusy(`${toolkit}:${scope}`) + try { + const res = await fetch( + `${BACKEND}/brain/connections/${toolkit}/link?scope=${scope}`, + { method: "POST", credentials: "include" }, + ) + if (res.status === 403) { + toast.error("Only admins can connect the shared org account.") + return + } + const data = (await res.json()) as { url?: string; error?: string } + if (data.url) window.open(data.url, "_blank", "noopener") + else toast.error(data.error ?? "Couldn't start the connection.") + } finally { + setBusy(null) + } + } + + if (!isCompanyBrain) { + return ( +

+ Company Brain isn't enabled for this organization. +

+ ) + } + + const loading = rows === null + + return ( +
+
+ {loading ? ( + + ) : ( + rows.map((row) => ( + connect(row.toolkit, "org")} + /> + )) + )} +
+ +
+ {loading ? ( + + ) : ( + rows.map((row) => ( + connect(row.toolkit, "user")} + /> + )) + )} +
+
+ ) +} diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx index c0f2a167..2d8e2aaa 100644 --- a/apps/web/components/settings/settings-content.tsx +++ b/apps/web/components/settings/settings-content.tsx @@ -10,6 +10,7 @@ import Account from "@/components/settings/account" import Billing from "@/components/settings/billing" import Integrations from "@/components/settings/integrations" import ConnectionsMCP from "@/components/settings/connections-mcp" +import CompanyBrainConnections from "@/components/settings/company-brain-connections" import Support from "@/components/settings/support" import { ErrorBoundary } from "@/components/error-boundary" import { useRouter } from "next/navigation" @@ -44,6 +45,7 @@ export const TABS = [ "billing", "integrations", "connections", + "company-brain", "support", ] as const export type SettingsTab = (typeof TABS)[number] @@ -80,6 +82,12 @@ const NAV_ITEMS: NavItem[] = [ description: "Drive, Notion, OneDrive, MCP", icon: , }, + { + id: "company-brain", + label: "Company Brain", + description: "GitHub & Linear — org and personal", + icon: , + }, { id: "support", label: "Support & Help", @@ -416,6 +424,7 @@ export function SettingsContent({ {activeTab === "billing" && } {activeTab === "integrations" && } {activeTab === "connections" && } + {activeTab === "company-brain" && } {activeTab === "support" && }