From a787041ca7a1be48b3f7ba5b0a1ffc62e4159879 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sun, 2 Aug 2026 07:46:05 +0000 Subject: [PATCH] feat(web): company brain trial visibility + setup timeline (#1384) - Header pill with trial days left (Autumn-first, org metadata fallback) - Brain home: Your Company Brain timeline card (trial strip, milestones from /brain/overview) promoted to top-right - Trial copy in CB onboarding Slack step and docked header - Brain home now reads the new /brain/overview endpoint (drops the dead /brain/connections fetch) Fixes ENG-1142 --- .../components/brain-home/brain-home-view.tsx | 376 ++++++++++++++---- .../brain-home/connections-board.tsx | 164 ++++---- apps/web/components/brain-trial-pill.tsx | 45 +++ apps/web/components/company-brain-header.tsx | 2 + .../company-brain-onboarding.tsx | 21 +- .../onboarding-brain/research-action-rail.tsx | 25 +- apps/web/hooks/use-brain-trial.ts | 83 ++++ 7 files changed, 531 insertions(+), 185 deletions(-) create mode 100644 apps/web/components/brain-trial-pill.tsx create mode 100644 apps/web/hooks/use-brain-trial.ts diff --git a/apps/web/components/brain-home/brain-home-view.tsx b/apps/web/components/brain-home/brain-home-view.tsx index 495bdcf0..06a00ba6 100644 --- a/apps/web/components/brain-home/brain-home-view.tsx +++ b/apps/web/components/brain-home/brain-home-view.tsx @@ -5,11 +5,18 @@ import { useAuth } from "@lib/auth-context" import { cn } from "@lib/utils" import { useQuery } from "@tanstack/react-query" import { ArrowRight, Check, FileText, Loader2, UserPlus } from "lucide-react" -import Link from "next/link" import { useQueryState } from "nuqs" import { useSettingsModal } from "@/components/settings/settings-modal" +import { useBrainTrial } from "@/hooks/use-brain-trial" import { dmSans125ClassName } from "@/lib/fonts" -import { ConnectionsBoard } from "./connections-board" +import { useViewMode } from "@/lib/view-mode-context" +import { + AskInSlackCard, + CONNECT_TOOLS_CARD_ID, + ConnectToolsCard, + SlackBanner, + useConnectionsBoard, +} from "./connections-board" const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" @@ -26,6 +33,26 @@ type RecentDoc = { updatedAt?: string | Date | null } +type RolloutOverview = { + status: "running" | "done" | "failed" + discovered: number + joined: number + ready: number + introduced: number + failed: number +} + +type BrainOverview = { + research: { status: string | null } + slack: { + connected: boolean + teamName: string | null + rollout: RolloutOverview | null + } + connections: { apps: number } + members: { count: number } +} + function useBrainOverview() { const { user, org } = useAuth() const enabled = !!user && !!org?.id @@ -87,29 +114,6 @@ function useBrainOverview() { enabled, }) - const brain = useQuery({ - queryKey: ["brain-connections"], - queryFn: async () => { - const [c, s] = await Promise.all([ - fetch(`${BACKEND}/brain/connections`, { credentials: "include" }), - fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }), - ]) - const toolkits = c.ok - ? ((await c.json()) as { toolkits: { org: boolean; user: boolean }[] }) - .toolkits - : [] - const slack = s.ok - ? ((await s.json()) as { connected: boolean }).connected - : false - return { - activeCount: toolkits.filter((t) => t.org || t.user).length, - slack, - } - }, - staleTime: 30_000, - enabled, - }) - const mcp = useQuery({ queryKey: ["mcp-status"], queryFn: async () => { @@ -121,11 +125,24 @@ function useBrainOverview() { enabled, }) + const overview = useQuery({ + queryKey: ["brain-overview", org?.id], + queryFn: async (): Promise => { + const res = await fetch(`${BACKEND}/brain/overview`, { + credentials: "include", + }) + if (!res.ok) return null + return (await res.json()) as BrainOverview + }, + staleTime: 30_000, + enabled, + }) + const memoriesCount = docs.data?.pagination?.totalItems ?? 0 - const connectedCount = - (brain.data?.activeCount ?? 0) + - (brain.data?.slack ? 1 : 0) + - (connectors.data?.length ?? 0) + const slackConnected = overview.data?.slack.connected ?? false + const appsCount = + (overview.data?.connections.apps ?? 0) + (connectors.data?.length ?? 0) + const connectedCount = appsCount + (slackConnected ? 1 : 0) const currentRole = org?.members ?.find((m) => m.userId === user?.id) @@ -137,20 +154,35 @@ function useBrainOverview() { lastUpdatedAt: lastUpdated.data ?? null, memoriesCount, connectedCount, - membersCount: org?.members?.length ?? 0, + membersCount: overview.data?.members.count ?? org?.members?.length ?? 0, canInvite: currentRole === "owner" || currentRole === "admin", hasSource: connectedCount > 0, hasAgent: mcp.data ?? false, hasMemory: memoriesCount > 0, + hasApps: appsCount > 0, + slackConnected, + researchStatus: overview.data?.research.status ?? null, + rollout: overview.data?.slack.rollout ?? null, } } export function BrainHomeView() { const o = useBrainOverview() - const stepsDone = [o.hasSource, o.hasAgent, o.hasMemory].filter( - Boolean, - ).length - const showGettingStarted = !o.loading && stepsDone < 3 + const trial = useBrainTrial() + const board = useConnectionsBoard() + // Rows with no reported state (older orgs, pre-Slack) don't count or render. + const milestones = [ + ...(o.researchStatus != null ? [o.researchStatus === "done"] : []), + o.slackConnected, + ...(o.rollout != null ? [o.rollout.status === "done"] : []), + o.hasApps, + o.membersCount > 1, + o.hasMemory, + ] + const milestonesDone = milestones.filter(Boolean).length + const milestonesTotal = milestones.length + const showTimeline = + !o.loading && (trial.state !== "none" || milestonesDone < milestonesTotal) return (
@@ -159,24 +191,31 @@ export function BrainHomeView() { connected={o.connectedCount} members={o.membersCount} canInvite={o.canInvite} - setupDone={stepsDone} + setupDone={milestonesDone} + setupTotal={milestonesTotal} lastUpdatedAt={o.lastUpdatedAt} /> - -
- - {showGettingStarted && ( - - )} + {board.slack && !board.slack.connected && } +
+
+ {board.showBoard && } + +
+
+ {showTimeline && ( + 1} + hasMemory={o.hasMemory} + canInvite={o.canInvite} + toolsCardVisible={board.showBoard} + /> + )} + +
) @@ -188,6 +227,7 @@ function StatsRow({ members, canInvite, setupDone, + setupTotal, lastUpdatedAt, }: { memories: number @@ -195,6 +235,7 @@ function StatsRow({ members: number canInvite: boolean setupDone: number + setupTotal: number lastUpdatedAt: string | Date | null }) { const { openSettings } = useSettingsModal() @@ -226,8 +267,8 @@ function StatsRow({ ) : undefined, }, - setupDone < 3 - ? { label: "Setup", value: `${setupDone}/3` } + setupDone < setupTotal + ? { label: "Setup", value: `${setupDone}/${setupTotal}` } : { label: "Last updated", value: formatWhen(lastUpdatedAt) || "—", @@ -358,34 +399,182 @@ function RecentMemories({ ) } -function GettingStarted({ - hasSource, - hasAgent, +function TrialStrip() { + const trial = useBrainTrial() + const { openSettings } = useSettingsModal() + + if (trial.state === "ended") { + return ( +
+

+ Trial ended · your brain is paused +

+ +
+ ) + } + + if (trial.state !== "trialing" || !trial.startedAtMs || !trial.endsAtMs) { + return null + } + + const DAY_MS = 24 * 60 * 60 * 1000 + const totalDays = Math.max( + 1, + Math.round((trial.endsAtMs - trial.startedAtMs) / DAY_MS), + ) + const days = trial.daysRemaining ?? 0 + const dayNum = Math.min(totalDays, Math.max(1, totalDays - days + 1)) + const pct = Math.min(100, Math.max(4, (dayNum / totalDays) * 100)) + + return ( + + ) +} + +function BrainTimeline({ + researchStatus, + slackConnected, + rollout, + hasApps, + invited, hasMemory, + canInvite, + toolsCardVisible, }: { - hasSource: boolean - hasAgent: boolean + researchStatus: string | null + slackConnected: boolean + rollout: RolloutOverview | null + hasApps: boolean + invited: boolean hasMemory: boolean + canInvite: boolean + toolsCardVisible: boolean }) { - const steps = [ + const trial = useBrainTrial() + const { openSettings } = useSettingsModal() + const { setViewMode } = useViewMode() + const [, setInvite] = useQueryState("invite") + + const onInvite = () => { + setInvite("1") + openSettings("account") + } + + const onSetUpApps = () => { + const card = document.getElementById(CONNECT_TOOLS_CARD_ID) + if (toolsCardVisible && card) { + card.scrollIntoView({ behavior: "smooth", block: "center" }) + } else { + void setViewMode("configure") + } + } + + const researching = + researchStatus === "queued" || researchStatus === "running" + type Step = { + done: boolean + busy?: boolean + title: string + hint?: string + action?: { label: string; onClick?: () => void; href?: string } + } + // Rows with unreported state are omitted rather than shown as never-started. + const steps: Step[] = [ + ...(researchStatus != null + ? [ + { + done: researchStatus === "done", + busy: researching, + title: researching + ? "Researching your company…" + : "Company research", + }, + ] + : []), { - done: hasSource, - title: "Connect a source", - hint: "GitHub, Linear, Drive or Slack.", - href: "/settings/integrations", + done: slackConnected, + title: slackConnected ? "Slack connected" : "Connect Slack", + hint: slackConnected + ? undefined + : trial.state === "trialing" + ? "Ask your brain from any channel." + : "Starts your 14-day free trial. No credit card needed.", + action: slackConnected + ? undefined + : { label: "Add", href: `${BACKEND}/brain/slack/oauth/install` }, + }, + ...(rollout != null + ? [ + { + done: rollout.status === "done", + busy: rollout.status === "running", + title: + rollout.status === "running" + ? `Learning from channels… ${rollout.ready}/${rollout.discovered} ready` + : rollout.status === "done" + ? `Learning from channels · ${rollout.ready} ready` + : "Learning from channels", + }, + ] + : []), + { + done: hasApps, + title: "Connect apps", + hint: hasApps ? undefined : "Linear, Notion, GitHub and more.", + action: hasApps ? undefined : { label: "Set up", onClick: onSetUpApps }, }, { - done: hasAgent, - title: "Install a coding agent", - hint: "Claude Code, Codex or Cursor.", - href: "/settings/integrations", + done: invited, + title: "Invite teammates", + hint: invited ? undefined : "Multiply what the brain remembers.", + action: + invited || !canInvite + ? undefined + : { label: "Invite", onClick: onInvite }, }, { done: hasMemory, - title: "Add your first memory", - hint: "Save a doc, or ask your brain below.", + title: "First memories captured", + hint: hasMemory ? undefined : "Save a doc, or ask your brain below.", }, ] + return (
- Getting started + Your Company Brain

-

- A few steps to make your brain useful. +

+ How far you've come.

-
    + + +
      {steps.map((step) => (
    • - {step.done && } + {step.done ? ( + + ) : step.busy ? ( + + ) : null}

      {step.title}

      - {!step.done && step.href && ( - - Set up - - - )} + {!step.done && + step.action && + (step.action.href ? ( + + {step.action.label} + + + ) : ( + + ))}
      - {!step.done && ( + {!step.done && step.hint && (

      {step.hint}

      diff --git a/apps/web/components/brain-home/connections-board.tsx b/apps/web/components/brain-home/connections-board.tsx index 479c343d..04ab056c 100644 --- a/apps/web/components/brain-home/connections-board.tsx +++ b/apps/web/components/brain-home/connections-board.tsx @@ -55,7 +55,7 @@ function titleCase(s: string) { return s.replace(/\b\w/g, (c) => c.toUpperCase()) } -export function ConnectionsBoard() { +export function useConnectionsBoard() { const [catalog, setCatalog] = useState(null) const [rows, setRows] = useState([]) const [slack, setSlack] = useState<{ @@ -155,7 +155,6 @@ export function ConnectionsBoard() { } } - const { setViewMode } = useViewMode() const apps = catalog ?? [] const loading = catalog === null const unconnected = apps.filter((a) => !isConnected(a.slug)) @@ -172,92 +171,84 @@ export function ConnectionsBoard() { const connectedCount = apps.filter((a) => isConnected(a.slug)).length const showBoard = loading || unconnected.length > 0 + return { + slack, + loading, + busy, + featured, + overflow, + previewApps, + connectedCount, + showBoard, + isConnected, + connect, + } +} + +export type ConnectionsBoardState = ReturnType + +export const CONNECT_TOOLS_CARD_ID = "connect-tools" + +export function ConnectToolsCard({ board }: { board: ConnectionsBoardState }) { + const { setViewMode } = useViewMode() + const { loading, featured, overflow, busy, isConnected, connect } = board + return ( -
      - {slack && !slack.connected && } - -
      - {showBoard ? ( -
      -
      -

      - Connect your tools -

      -

      - Give your Slack agent live access to the apps your team already - uses. -

      -
      - -
      - {loading ? ( - Array.from({ length: 3 }).map((_, i) => ( - - )) - ) : ( - <> - {featured.map((entry, i) => ( - connect(entry)} - showDivider={ - i < featured.length - 1 || overflow.length > 0 - } - /> - ))} - {overflow.length > 0 && ( - a.name)} - onClick={() => void setViewMode("configure")} - /> - )} - - )} -
      -
      - ) : null} - - +
      +
      +

      + Connect your tools +

      +

      + Give your Slack agent live access to the apps your team already uses. +

      -
      + +
      + {loading ? ( + Array.from({ length: 3 }).map((_, i) => ( + + )) + ) : ( + <> + {featured.map((entry, i) => ( + connect(entry)} + showDivider={i < featured.length - 1 || overflow.length > 0} + /> + ))} + {overflow.length > 0 && ( + a.name)} + onClick={() => void setViewMode("configure")} + /> + )} + + )} +
      +
) } -function AgentPreview({ - apps, - isConnected, - connectedCount, - wide = false, -}: { - apps: CatalogEntry[] - isConnected: (slug: string) => boolean - connectedCount: number - wide?: boolean -}) { - const prompts = apps +export function AskInSlackCard({ board }: { board: ConnectionsBoardState }) { + const { previewApps, isConnected, connectedCount } = board + const prompts = previewApps .filter((a) => AGENT_PROMPTS[a.slug]) .slice(0, 6) .map((a) => ({ @@ -269,10 +260,7 @@ function AgentPreview({ return (
@@ -437,7 +425,7 @@ function TileSkeleton({ showDivider = false }: { showDivider?: boolean }) { ) } -function SlackBanner() { +export function SlackBanner() { return (
openSettings("billing")} + aria-label={ended ? "Trial ended — activate Scale" : label} + className={cn( + "inline-flex h-7 shrink-0 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 text-[12px] font-medium transition-colors", + ended + ? "border-[#E5735A]/30 bg-[#E5735A]/10 text-[#E5735A] hover:bg-[#E5735A]/15" + : closing + ? "border-[#E5A45A]/30 bg-[#E5A45A]/10 text-[#E5A45A] hover:bg-[#E5A45A]/15" + : "border-white/[0.08] bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white/90", + dmSansClassName(), + className, + )} + > + + {label} + + ) +} diff --git a/apps/web/components/company-brain-header.tsx b/apps/web/components/company-brain-header.tsx index 12662a12..99cd1116 100644 --- a/apps/web/components/company-brain-header.tsx +++ b/apps/web/components/company-brain-header.tsx @@ -35,6 +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 { BrainTrialPill } from "@/components/brain-trial-pill" import { GraphIcon } from "@/components/integration-icons" import { SpaceSelector } from "@/components/space-selector" import { UserProfileMenu } from "@/components/user-profile-menu" @@ -496,6 +497,7 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) { ) : ( <> + {canInvite && ( diff --git a/apps/web/components/onboarding-brain/company-brain-onboarding.tsx b/apps/web/components/onboarding-brain/company-brain-onboarding.tsx index d510d462..e9a0673a 100644 --- a/apps/web/components/onboarding-brain/company-brain-onboarding.tsx +++ b/apps/web/components/onboarding-brain/company-brain-onboarding.tsx @@ -12,7 +12,7 @@ import { Globe, Loader2, } from "lucide-react" -import { useQueryClient } from "@tanstack/react-query" +import { useQuery, useQueryClient } from "@tanstack/react-query" import { AnimatePresence, motion } from "motion/react" import { type ReactNode, useEffect, useRef, useState } from "react" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" @@ -464,6 +464,19 @@ function DockedHeader({ exhausted: boolean onContinue: () => void }) { + // Same key as the rail/header queries so a connect in another tab is picked up on refocus. + const { data: slack } = useQuery({ + queryKey: ["brain-slack-status"], + queryFn: async (): Promise<{ connected: boolean }> => { + const res = await fetch(`${BACKEND}/brain/slack/status`, { + credentials: "include", + }) + if (!res.ok) return { connected: false } + return (await res.json()) as { connected: boolean } + }, + staleTime: 30_000, + }) + const slackConnected = slack?.connected ?? false const brandName = workspaceNameFromDomain(domain) || domain const showSpinner = !done && !exhausted const statusLabel = done @@ -504,6 +517,12 @@ function DockedHeader({ )} {statusLabel} + {slackConnected && ( + + + Slack connected · free trial started + + )}
{/* Never gated on research; the admin can move on while it keeps working. */}