diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index fef83c39..0bd70af1 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -64,6 +64,9 @@ export default function BrainOnboardingPage() { // `?new=1` forces creating an additional org even when the user already has one. const forceCreate = params?.get("new") === "1" + // ensureOrg strips `new` once the org exists, so latch it for `finish`'s reload. + const forcedCreateRef = useRef(forceCreate) + if (forceCreate) forcedCreateRef.current = true const nameParam = params?.get("name")?.trim() || "" const stepFromUrl = (params?.get("step") as BrainStep | null) ?? "about" @@ -73,9 +76,12 @@ export default function BrainOnboardingPage() { const [step, setStep] = useState(initialStep) + // `?mode=team` wins over email detection so a personal-domain user arriving + // from a "set up a Company Brain" CTA doesn't land in personal onboarding. + const modeParam = params?.get("mode") === "team" ? "team" : null const detectedMode = useMemo( - () => detectModeFromEmail(user?.email), - [user?.email], + () => modeParam ?? detectModeFromEmail(user?.email), + [modeParam, user?.email], ) const suggestedWorkspaceName = useMemo( () => workspaceNameFromEmail(user?.email), @@ -113,12 +119,12 @@ export default function BrainOnboardingPage() { sources?: SourcesValues team?: TeamValues } - if (cached.mode) setMode(cached.mode) + if (cached.mode && !modeParam) setMode(cached.mode) if (cached.about) setAbout((a) => ({ ...a, ...cached.about })) if (cached.sources) setSources((s) => ({ ...s, ...cached.sources })) if (cached.team) setTeam((t) => ({ ...t, ...cached.team })) } catch {} - }, [forceCreate]) + }, [forceCreate, modeParam]) useEffect(() => { try { @@ -209,12 +215,12 @@ export default function BrainOnboardingPage() { localStorage.removeItem(STORAGE_KEY) } catch {} // Extra org from settings: hard-reload so org-scoped caches don't show the previous org's data. - if (forceCreate) { + if (forcedCreateRef.current) { window.location.href = "/?onboarded=1" return } router.push("/?onboarded=1") - }, [router, mode, sources, team, forceCreate]) + }, [router, mode, sources, team]) const goNext = useCallback(() => { const idx = steps.indexOf(step) diff --git a/apps/web/components/app-experience.tsx b/apps/web/components/app-experience.tsx index 28b93b18..42fedec9 100644 --- a/apps/web/components/app-experience.tsx +++ b/apps/web/components/app-experience.tsx @@ -16,6 +16,7 @@ import { ChatSidebar, HomeChatComposer } from "@/components/chat" import type { ChatAttachmentDraft } from "@/components/chat/attachments" import { DashboardView } from "@/components/dashboard-view" import { BrainHomeView } from "@/components/brain-home/brain-home-view" +import { CompanyBrainPromo } from "@/components/company-brain-promo" import { useHasCompanyBrain } from "@/hooks/use-company-brain" import { MemoriesGrid } from "@/components/memories-grid" import { GraphLayoutView } from "@/components/graph-layout-view" @@ -802,7 +803,7 @@ export function AppExperience() { ) : ( } highlights={highlightsData?.highlights ?? []} isLoadingHighlights={isLoadingHighlights} onAddMemory={handleAddMemory} diff --git a/apps/web/components/company-brain-promo.tsx b/apps/web/components/company-brain-promo.tsx new file mode 100644 index 00000000..730eeb08 --- /dev/null +++ b/apps/web/components/company-brain-promo.tsx @@ -0,0 +1,89 @@ +"use client" + +import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import { ArrowRight, XIcon } from "lucide-react" +import { Logo } from "@ui/assets/Logo" +import { Button } from "@repo/ui/components/button" +import { cn } from "@lib/utils" +import { analytics } from "@/lib/analytics" +import { dmSansClassName } from "@/lib/fonts" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" + +const DISMISS_KEY = "supermemory-company-brain-promo-dismissed-v1" + +export function CompanyBrainPromo() { + const router = useRouter() + const hasCompanyBrain = useHasCompanyBrain() + const [dismissed, setDismissed] = useState(true) + + useEffect(() => { + if (hasCompanyBrain) return + try { + setDismissed(localStorage.getItem(DISMISS_KEY) === "1") + } catch { + setDismissed(false) + } + }, [hasCompanyBrain]) + + const visible = !hasCompanyBrain && !dismissed + + useEffect(() => { + if (visible) analytics.companyBrainPromoSeen() + }, [visible]) + + if (!visible) return null + + const dismiss = () => { + setDismissed(true) + try { + localStorage.setItem(DISMISS_KEY, "1") + } catch {} + analytics.companyBrainPromoDismissed() + } + + return ( +
+
+ +
+
+

+ Give your team a Company Brain +

+

+ Lives in your Slack. Answers from your team's tools, and brings things + up before you ask. +

+
+ + +
+ ) +} diff --git a/apps/web/components/user-profile-menu.tsx b/apps/web/components/user-profile-menu.tsx index b69701f2..ea034d73 100644 --- a/apps/web/components/user-profile-menu.tsx +++ b/apps/web/components/user-profile-menu.tsx @@ -13,6 +13,7 @@ import { import { authClient } from "@lib/auth" import { useRouter } from "next/navigation" import { + Brain, LogOut, Settings, Settings2, @@ -22,6 +23,7 @@ import { Sun, } from "lucide-react" import { cn } from "@lib/utils" +import { analytics } from "@/lib/analytics" import { dmSansClassName } from "@/lib/fonts" import { useOrgOnboarding } from "@hooks/use-org-onboarding" import { useTokenUsage } from "@/hooks/use-token-usage" @@ -164,6 +166,18 @@ export function UserProfileMenu({ Settings + {isCompanyBrain ? null : ( + { + analytics.companyBrainPromoClicked({ source: "profile_menu" }) + router.push("/onboarding?new=1&mode=team") + }} + 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" + > + + Set up Company Brain + + )} {isCompanyBrain ? ( void setViewMode("configure")} diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index eebd1697..f160e612 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -257,4 +257,12 @@ export const analytics = { rating: "up" | "down" | null message: string }) => safeCapture("digest_feedback_detail", props), + + // company brain promo + companyBrainPromoSeen: () => safeCapture("company_brain_promo_seen"), + companyBrainPromoClicked: (props: { + source: "dashboard_card" | "profile_menu" + }) => safeCapture("company_brain_promo_clicked", props), + companyBrainPromoDismissed: () => + safeCapture("company_brain_promo_dismissed"), }