+}[] = [
+ {
+ label: "Mon",
+ values: {
+ cursor: 8,
+ claudeCode: 4,
+ claudeDesktop: 3,
+ chatgpt: 2,
+ other: 1,
+ },
+ },
+ {
+ label: "Tue",
+ values: {
+ cursor: 10,
+ claudeCode: 6,
+ claudeDesktop: 4,
+ chatgpt: 3,
+ other: 1,
+ },
+ },
+ {
+ label: "Wed",
+ values: {
+ cursor: 12,
+ claudeCode: 9,
+ claudeDesktop: 5,
+ chatgpt: 4,
+ other: 1,
+ },
+ },
+ {
+ label: "Thu",
+ values: {
+ cursor: 9,
+ claudeCode: 5,
+ claudeDesktop: 4,
+ chatgpt: 3,
+ other: 1,
+ },
+ },
+ {
+ label: "Fri",
+ values: {
+ cursor: 14,
+ claudeCode: 10,
+ claudeDesktop: 6,
+ chatgpt: 4,
+ other: 2,
+ },
+ },
+ {
+ label: "Sat",
+ values: {
+ cursor: 5,
+ claudeCode: 3,
+ claudeDesktop: 2,
+ chatgpt: 1,
+ other: 1,
+ },
+ },
+ {
+ label: "Sun",
+ values: {
+ cursor: 17,
+ claudeCode: 12,
+ claudeDesktop: 7,
+ chatgpt: 4,
+ other: 1,
+ },
+ },
+]
+
+export const RECENT_ANSWERS = [
+ {
+ question: "What did we decide about pricing?",
+ answer: "Pro stays at $20/mo. No overages, top-up only.",
+ who: "You",
+ when: "2h ago",
+ },
+ {
+ question: "What's blocking the Acme deal?",
+ answer: "MSA review — Jane drafting a net-30 counter.",
+ who: "You",
+ when: "4h ago",
+ },
+ {
+ question: "Who owns the onboarding rewrite?",
+ answer: "Design team. Ship target Friday.",
+ who: "Sarah Kim",
+ when: "Yesterday",
+ },
+]
+
+export const WEEKLY_DIGEST = {
+ range: "Mon – Fri",
+ headline:
+ "Your brain captured 642 items, surfaced 12 decisions, and answered 184 questions across the team.",
+ bullets: [
+ { label: "5 deadlines coming up next week", tint: "text-[#4BA0FA]" },
+ {
+ label: "2 conflicts surfaced between Notion and Slack",
+ tint: "text-[#FF8A47]",
+ },
+ {
+ label: "Sarah Kim led contributions with 14 docs",
+ tint: "text-[#A1A1AA]",
+ },
+ ],
+}
+
+export const TEAM_THIS_WEEK = [
+ {
+ name: "Sarah Kim",
+ email: "sarah@acme.com",
+ contributions: 14,
+ summary: "14 docs · 3 spaces",
+ color: "#FF8A47",
+ },
+ {
+ name: "Jane Doe",
+ email: "jane@acme.com",
+ contributions: 11,
+ summary: "11 docs · 2 spaces",
+ color: "#4BA0FA",
+ },
+ {
+ name: "Mahesh S.",
+ email: "mahesh@acme.com",
+ contributions: 9,
+ summary: "9 docs · 1 space",
+ color: "#A1A1AA",
+ },
+ {
+ name: "Alex Chen",
+ email: "alex@acme.com",
+ contributions: 7,
+ summary: "7 docs · 2 spaces",
+ color: "#10A37F",
+ },
+]
+
+export const TEAM_RECENT = [
+ { who: "Sarah Kim", what: "added a memo to acme-deal-q2", when: "14m" },
+ { who: "Jane Doe", what: "updated the Acme MSA draft", when: "1h" },
+ { who: "You", what: 'asked "What did we decide about pricing?"', when: "2h" },
+ {
+ who: "Sarah Kim",
+ what: "connected Notion to Sales space",
+ when: "Yesterday",
+ },
+]
+
+export type ActivityEvent = {
+ when: string
+ text: string
+ source: "drive" | "notion" | "slack" | "system" | "answer" | "decision"
+}
+
+export const ACTIVITY: ActivityEvent[] = [
+ { when: "2m", text: "23 new docs ingested from Drive", source: "drive" },
+ { when: "14m", text: "Jane added a memo to acme-deal-q2", source: "notion" },
+ {
+ when: "1h",
+ text: "Decision surfaced: pricing locked at $20 Pro",
+ source: "slack",
+ },
+ {
+ when: "3h",
+ text: "14 Notion pages updated by Sarah Kim",
+ source: "notion",
+ },
+ { when: "5h", text: "New entity formed: pricing-v3", source: "system" },
+ {
+ when: "Yesterday",
+ text: "MSA contract uploaded · auto-tagged contracts/MSA",
+ source: "drive",
+ },
+]
+
+export type TimelineKind =
+ | "answer"
+ | "decision"
+ | "ingest"
+ | "mention"
+ | "system"
+
+export type TimelineEvent = {
+ kind: TimelineKind
+ when: string
+ title: string
+ detail?: string
+ source?: string
+ who?: string
+}
+
+export const TIMELINE_EVENTS: TimelineEvent[] = [
+ {
+ kind: "answer",
+ when: "2m ago",
+ title: "What did we decide about pricing?",
+ detail: "Pro stays at $20/mo. No overages, top-up only.",
+ source: "You asked",
+ },
+ {
+ kind: "ingest",
+ when: "14m ago",
+ title: "23 new docs ingested from Drive",
+ detail: "Q2 roadmap, Acme MSA draft v3, customer feedback Q1, +20 more.",
+ source: "Drive",
+ },
+ {
+ kind: "decision",
+ when: "1h ago",
+ title: "Decision: pricing locked at $20 Pro",
+ detail:
+ "Decided in #leadership thread to keep Pro at $20/mo. No overages — top-up only.",
+ source: "Slack · #leadership",
+ },
+ {
+ kind: "answer",
+ when: "4h ago",
+ title: "What's blocking the Acme deal?",
+ detail: "MSA review — Jane drafting a net-30 counter.",
+ source: "You asked",
+ },
+ {
+ kind: "mention",
+ when: "Today",
+ title: "onboarding-rewrite mentioned in 3 docs",
+ detail: "Owner: design team · Ship target: Friday.",
+ source: "Drive",
+ who: "Mahesh",
+ },
+ {
+ kind: "system",
+ when: "5h ago",
+ title: "New entity formed: pricing-v3",
+ detail: "Linked across 4 sources · auto-tagged by the brain.",
+ },
+ {
+ kind: "ingest",
+ when: "Yesterday",
+ title: "MSA contract uploaded · auto-tagged contracts/MSA",
+ source: "Drive",
+ },
+ {
+ kind: "answer",
+ when: "Yesterday",
+ title: "Who owns the onboarding rewrite?",
+ detail: "Design team. Ship target Friday.",
+ source: "Sarah Kim asked",
+ },
+ {
+ kind: "ingest",
+ when: "Yesterday",
+ title: "14 Notion pages updated by Sarah Kim",
+ source: "Notion",
+ who: "Sarah Kim",
+ },
+]
+
+export type SourceStatus = "healthy" | "syncing" | "warning" | "error"
+
+export type ConnectedSource = {
+ id: string
+ name: string
+ iconKey: "drive" | "notion" | "gmail" | "onedrive" | "github"
+ status: SourceStatus
+ delta: string
+ when: string
+ statusMessage?: string
+}
+
+export const CONNECTED_SOURCES: ConnectedSource[] = [
+ {
+ id: "drive",
+ name: "Google Drive",
+ iconKey: "drive",
+ status: "healthy",
+ delta: "+24 items",
+ when: "12m ago",
+ },
+ {
+ id: "notion",
+ name: "Company Notion",
+ iconKey: "notion",
+ status: "warning",
+ delta: "Sync stalled",
+ when: "4h ago",
+ statusMessage: "Re-auth · token expired",
+ },
+ {
+ id: "gmail",
+ name: "Gmail",
+ iconKey: "gmail",
+ status: "syncing",
+ delta: "Ingesting…",
+ when: "Just now",
+ },
+]
+
+export type RecommendedSource = {
+ id: string
+ name: string
+ iconKey: "slack" | "linear" | "granola" | "github" | "calendar"
+ reason: string
+}
+
+export const RECOMMENDED_SOURCES: RecommendedSource[] = [
+ {
+ id: "slack",
+ name: "Slack",
+ iconKey: "slack",
+ reason: "Decisions and threads — most teams connect this.",
+ },
+ {
+ id: "github",
+ name: "GitHub",
+ iconKey: "github",
+ reason: "PRs, issues, READMEs from your repos.",
+ },
+ {
+ id: "linear",
+ name: "Linear",
+ iconKey: "linear",
+ reason: "Track project decisions and blockers.",
+ },
+]
+
+export type LiveSession = {
+ name: string
+ avatarColor: string
+ status: "live" | "idle" | "away"
+ tool: string
+ toolColor: string
+ doing?: string
+ elapsed?: string
+}
+
+export const LIVE_SESSIONS: LiveSession[] = [
+ {
+ name: "Sarah Kim",
+ avatarColor: "#FF8A47",
+ status: "live",
+ tool: "Cursor",
+ toolColor: "#4BA0FA",
+ doing: "Refactoring auth flow with brain context",
+ elapsed: "3m elapsed",
+ },
+ {
+ name: "Jane Doe",
+ avatarColor: "#4BA0FA",
+ status: "live",
+ tool: "Claude Code",
+ toolColor: "#FF8A47",
+ doing: "MSA review · pulling Acme history",
+ elapsed: "18m elapsed",
+ },
+ {
+ name: "You",
+ avatarColor: "#A1A1AA",
+ status: "idle",
+ tool: "Cursor",
+ toolColor: "#4BA0FA",
+ elapsed: "Idle · 12m",
+ },
+]
+
+export const TODAY_SESSIONS_SUMMARY: {
+ name: string
+ avatarColor: string
+ tool: string
+ count: number
+ lastAt: string
+}[] = [
+ {
+ name: "Alex Chen",
+ avatarColor: "#10A37F",
+ tool: "ChatGPT",
+ count: 4,
+ lastAt: "2h ago",
+ },
+ {
+ name: "Mahesh S.",
+ avatarColor: "#A1A1AA",
+ tool: "Claude Desktop",
+ count: 7,
+ lastAt: "3h ago",
+ },
+ {
+ name: "Sarah Kim",
+ avatarColor: "#FF8A47",
+ tool: "Cursor",
+ count: 11,
+ lastAt: "Now",
+ },
+]
+
+export type ChatMessage = { role: "user" | "brain"; text: string }
+
+export const SAMPLE_CHAT: ChatMessage[] = [
+ {
+ role: "user",
+ text: "What did we decide about pricing this quarter?",
+ },
+ {
+ role: "brain",
+ text: "Pro is locked at $20/mo. No overages — top-up only. Decided in the #leadership Slack thread on May 28.",
+ },
+ {
+ role: "user",
+ text: "Who's working on the Acme MSA?",
+ },
+ {
+ role: "brain",
+ text: "Jane Doe is drafting a net-30 counter to Acme's net-60 ask. Latest draft is in Drive (Acme · MSA draft v3.pdf).",
+ },
+]
diff --git a/apps/web/components/brain-home/widgets.tsx b/apps/web/components/brain-home/widgets.tsx
new file mode 100644
index 000000000..69ff83f34
--- /dev/null
+++ b/apps/web/components/brain-home/widgets.tsx
@@ -0,0 +1,671 @@
+"use client"
+
+import { cn } from "@lib/utils"
+import { dmSans125ClassName } from "@/lib/fonts"
+import { GoogleDrive, Notion } from "@ui/assets/icons"
+import {
+ AlertCircle,
+ ArrowRight,
+ Calendar,
+ CheckCircle2,
+ FileText,
+ Github,
+ Loader2,
+ MessageSquare,
+ Plus,
+ Quote,
+ Sparkles,
+ Target,
+} from "lucide-react"
+import {
+ ACTIVITY,
+ AGENT_SERIES,
+ CONNECTED_SOURCES,
+ RECENT_ANSWERS,
+ RECOMMENDED_SOURCES,
+ SESSIONS_BY_DAY,
+ STATS,
+ TEAM_RECENT,
+ TEAM_THIS_WEEK,
+ WEEKLY_DIGEST,
+ type ActivityEvent,
+ type ConnectedSource,
+ type RecommendedSource,
+ type Stat,
+} from "./data"
+
+function GmailIcon({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export const modalCardStyle = {
+ 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",
+}
+
+export const inputBevelStyle = {
+ boxShadow:
+ "0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)",
+}
+
+export function StatsStrip({ stats = STATS }: { stats?: Stat[] }) {
+ return (
+
+ {stats.map((s) => (
+
+
+ {s.label}
+
+
+ {s.value}
+
+ {s.delta && (
+
+ {s.delta}
+
+ )}
+
+ ))}
+
+ )
+}
+
+export function SectionHeader({ title, cta }: { title: string; cta?: string }) {
+ return (
+
+
+ {title}
+
+ {cta && (
+
+ {cta}
+
+ )}
+
+ )
+}
+
+export function RecentAnswersCard() {
+ return (
+
+
+
+ Recent answers
+
+
+ See all →
+
+
+
+ {RECENT_ANSWERS.map((a) => (
+
+
+ {a.question}
+
+
+ → {a.answer}
+
+
+ {a.who} · {a.when}
+
+
+ ))}
+
+
+ )
+}
+
+export function WeeklyDigestCard() {
+ return (
+
+
+
+
+ This week
+
+
+ {WEEKLY_DIGEST.range}
+
+
+
+ {WEEKLY_DIGEST.headline.split(/(\d+)/).map((piece, i) =>
+ /^\d+$/.test(piece) ? (
+
+ {piece}
+
+ ) : (
+ {piece}
+ ),
+ )}
+
+
+ {WEEKLY_DIGEST.bullets.map((b) => (
+
+
+
+ {b.label}
+
+
+ ))}
+
+
+ Open weekly digest
+
+
+ )
+}
+
+export function BrainPulseBlock() {
+ return (
+
+ )
+}
+
+export function BrainActivityChart() {
+ const totalsByAgent = AGENT_SERIES.map((s) => ({
+ ...s,
+ total: SESSIONS_BY_DAY.reduce((sum, d) => sum + (d.values[s.key] ?? 0), 0),
+ }))
+ const grandTotal = totalsByAgent.reduce((sum, s) => sum + s.total, 0)
+ const dayTotals = SESSIONS_BY_DAY.map((d) =>
+ AGENT_SERIES.reduce((sum, s) => sum + (d.values[s.key] ?? 0), 0),
+ )
+ const max = Math.max(...dayTotals)
+ return (
+
+
+
+
+
+ Sessions by agent
+
+
+ {grandTotal} agent sessions, last 7 days — which tool the team
+ reached for.
+
+
+
+ {totalsByAgent.map((s) => (
+
+
+ {s.label}{" "}
+ {s.total}
+
+ ))}
+
+
+
+ {SESSIONS_BY_DAY.map((d, dayIdx) => {
+ const BAR_AREA_PX = 140
+ const totalForDay = dayTotals[dayIdx] ?? 0
+ const barHeightPx = Math.max(
+ 6,
+ Math.round((totalForDay / max) * BAR_AREA_PX),
+ )
+ return (
+
+
+
+ {AGENT_SERIES.map((series, sIdx) => {
+ const val = d.values[series.key] ?? 0
+ if (val === 0) return null
+ const segHeight =
+ totalForDay > 0 ? (val / totalForDay) * 100 : 0
+ return (
+
+ )
+ })}
+
+
+
+ {d.label}
+
+
+ )
+ })}
+
+
+ )
+}
+
+function activitySourceIcon(s: ActivityEvent["source"]) {
+ if (s === "drive") return
+ if (s === "notion") return
+ if (s === "slack")
+ return
+ if (s === "answer") return
+ if (s === "decision")
+ return
+ return
+}
+
+export function ActivityFeedBlock() {
+ return (
+
+
+
+ {ACTIVITY.map((a, i) => (
+
+
+ {activitySourceIcon(a.source)}
+
+
+ {a.text}
+
+
+ {a.when}
+
+
+ ))}
+
+
+ )
+}
+
+export function TeamActivityRail() {
+ return (
+
+
+
+ Team activity
+
+
+ This week
+
+
+
+ What your team's been adding to the brain.
+
+
+ {TEAM_THIS_WEEK.map((member) => (
+
+
+ {member.name[0]}
+
+
+
+ {member.name}
+
+
+ {member.summary}
+
+
+
+ {member.contributions}
+
+
+ ))}
+
+
+
+ Recent
+
+
+ {TEAM_RECENT.map((event, i) => (
+
+ {event.who} {event.what}
+ · {event.when}
+
+ ))}
+
+
+
+
+ Invite teammates
+
+
+ )
+}
+
+export function BrainHomeBackground() {
+ return (
+
+ )
+}
+
+export function Quotation({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+function connectedSourceIcon(key: ConnectedSource["iconKey"]) {
+ if (key === "drive") return
+ if (key === "notion") return
+ if (key === "gmail") return
+ if (key === "github") return
+ return
+}
+
+function sourceStatusMeta(status: ConnectedSource["status"]) {
+ if (status === "healthy")
+ return {
+ dot: "bg-[#4BA0FA]",
+ tint: "text-[#4BA0FA]",
+ icon:
,
+ }
+ if (status === "syncing")
+ return {
+ dot: "bg-[#A1A1AA]",
+ tint: "text-[#A1A1AA]",
+ icon:
,
+ }
+ if (status === "warning")
+ return {
+ dot: "bg-[#FF8A47]",
+ tint: "text-[#FF8A47]",
+ icon:
,
+ }
+ return {
+ dot: "bg-[#FF5C5C]",
+ tint: "text-[#FF5C5C]",
+ icon:
,
+ }
+}
+
+export function SourcesHealthRail({ empty }: { empty?: boolean }) {
+ if (empty) {
+ return (
+
+
+ Sources health
+
+
+ Nothing connected yet — Drive, Notion, Gmail and more are ready when
+ you are.
+
+
+
+ Connect a source
+
+
+ )
+ }
+ const issueCount = CONNECTED_SOURCES.filter(
+ (s) => s.status === "warning" || s.status === "error",
+ ).length
+ return (
+
+
+
+ Sources health
+
+ {issueCount > 0 ? (
+
+ {issueCount} need attention
+
+ ) : (
+
+ All healthy
+
+ )}
+
+
+ Is the brain getting fed?
+
+
+
+
+ )
+}
+
+function recommendedIcon(key: RecommendedSource["iconKey"]) {
+ if (key === "slack")
+ return
+ if (key === "github") return
+ if (key === "linear") return
+ if (key === "granola") return
+ if (key === "calendar") return
+ return
+}
+
+export function RecommendedRail() {
+ return (
+
+
+ Add more to the brain
+
+
+ Teams like yours connect these next.
+
+
+
+
+ )
+}
diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index 97466f356..566f0a36a 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -3,7 +3,7 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { useCustomer } from "autumn-js/react"
import { cn } from "@lib/utils"
-import { dmSans125ClassName } from "@/lib/fonts"
+import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
import { hasActivePlan } from "@lib/queries"
import { $fetch } from "@lib/api"
import { authClient } from "@lib/auth"
@@ -23,6 +23,7 @@ import {
ArrowRight,
BookOpen,
Check,
+ Info,
Loader,
Plus,
Search,
@@ -338,7 +339,7 @@ const SECTIONS: Array<{
className="size-6 rounded object-contain"
/>
),
- docsUrl: "https://docs.supermemory.ai/supermemory-mcp/introduction",
+ docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction",
})),
},
{
@@ -381,6 +382,7 @@ const SECTIONS: Array<{
simpleTitle: "Your Docs, Sheets and Slides, searchable",
icon:
,
pro: true,
+ docsUrl: "https://supermemory.ai/docs/connectors/google-drive",
},
{
kind: "connector",
@@ -391,6 +393,7 @@ const SECTIONS: Array<{
simpleTitle: "All your Notion pages, in supermemory",
icon:
,
pro: true,
+ docsUrl: "https://supermemory.ai/docs/connectors/notion",
},
{
kind: "connector",
@@ -401,6 +404,7 @@ const SECTIONS: Array<{
simpleTitle: "Your OneDrive files, ready to recall",
icon:
,
pro: true,
+ docsUrl: "https://supermemory.ai/docs/connectors/onedrive",
},
],
},
@@ -531,22 +535,369 @@ function IconBox({ children }: { children: ReactNode }) {
)
}
-function DocsLink({ href }: { href: string }) {
+type InfoUseCase = {
+ title: string
+ description: string
+}
+
+type InfoModalCloseReason = Parameters<
+ typeof analytics.integrationInfoModalClosed
+>[0]["close_reason"]
+
+const MCP_INFO_USE_CASES: InfoUseCase[] = [
+ {
+ title: "Persistent assistant memory",
+ description:
+ "Store useful context during conversations and recall it later from this MCP client.",
+ },
+ {
+ title: "Shared context across tools",
+ description:
+ "Use the same Supermemory account across MCP-compatible clients so memory follows the user between sessions.",
+ },
+ {
+ title: "Profiles and project context",
+ description:
+ "Bring user profiles and project-scoped memories into supported AI clients when they need context.",
+ },
+]
+
+const ITEM_INFO_USE_CASES: Record
= {
+ "plugin-claude_code": [
+ {
+ title: "Session context injection",
+ description:
+ "Fetch relevant project memories, user preferences, and past interactions when Claude Code starts a session.",
+ },
+ {
+ title: "Automatic coding capture",
+ description:
+ "Save useful tool activity like edits, new files, shell commands, and spawned tasks for future sessions.",
+ },
+ ],
+ "plugin-codex": [
+ {
+ title: "Recall before each prompt",
+ description:
+ "Inject relevant memories and profile context into Codex before each prompt.",
+ },
+ {
+ title: "Capture after sessions",
+ description:
+ "Store conversation transcripts after a session, scoped to the current project and user.",
+ },
+ {
+ title: "Explicit memory skills",
+ description:
+ "Use supermemory-search, supermemory-save, and supermemory-forget when memory needs direct control.",
+ },
+ ],
+ "plugin-opencode": [
+ {
+ title: "Project memory in OpenCode",
+ description:
+ "Inject preferences, project knowledge, and past interactions at the start of OpenCode sessions.",
+ },
+ {
+ title: "Smart session capture",
+ description:
+ "Save memories from explicit phrases like remember or save this, and summarize long sessions during compaction.",
+ },
+ ],
+ "plugin-openclaw": [
+ {
+ title: "Memory across messaging channels",
+ description:
+ "Give OpenClaw memory across WhatsApp, Telegram, Discord, Slack, iMessage, and other channels.",
+ },
+ {
+ title: "Auto-recall and auto-capture",
+ description:
+ "Inject relevant memories before AI turns and store conversation exchanges after turns.",
+ },
+ {
+ title: "Direct memory tools",
+ description:
+ "Let the AI store, search, forget, and inspect profile memories during conversations.",
+ },
+ ],
+ "plugin-hermes": [
+ {
+ title: "Semantic memory for Hermes",
+ description:
+ "Add long-term memory, profile recall, search, and session-aware ingest to Hermes.",
+ },
+ {
+ title: "Turn and session memory",
+ description:
+ "Prefetch relevant context before turns, capture completed turns, and ingest full sessions for richer graph updates.",
+ },
+ {
+ title: "Organized containers",
+ description:
+ "Use profile-scoped memory and optional multi-container tags for work, personal, or project-specific context.",
+ },
+ ],
+ "google-drive": [
+ {
+ title: "Scoped Drive sync",
+ description:
+ "Sync selected Google Docs, Sheets, Slides, and PDFs after OAuth and the hosted file picker.",
+ },
+ {
+ title: "Fresh knowledge base",
+ description:
+ "Keep selected Drive files updated in Supermemory, with scheduled and manual import support.",
+ },
+ ],
+ notion: [
+ {
+ title: "Workspace knowledge sync",
+ description:
+ "Sync Notion pages, databases, and blocks into Supermemory from connected workspaces.",
+ },
+ {
+ title: "Rich Notion context",
+ description:
+ "Preserve rich formatting and database properties so Notion content remains useful for retrieval.",
+ },
+ ],
+ onedrive: [
+ {
+ title: "Microsoft 365 documents",
+ description:
+ "Sync Word documents, Excel spreadsheets, and PowerPoint presentations from OneDrive.",
+ },
+ {
+ title: "Personal and business accounts",
+ description:
+ "Connect personal or business OneDrive accounts and keep Office files updated through sync.",
+ },
+ ],
+ chrome: [
+ {
+ title: "Save from the browser",
+ description:
+ "Capture webpages into Supermemory while browsing instead of manually copying content.",
+ },
+ {
+ title: "Bring bookmarks into memory",
+ description:
+ "Import saved browser context so it can be searched and reused later.",
+ },
+ ],
+ shortcuts: [
+ {
+ title: "Quick mobile capture",
+ description:
+ "Add memories from iPhone, iPad, or Mac through Apple Shortcuts.",
+ },
+ {
+ title: "Save without opening the app",
+ description:
+ "Send useful snippets and links into Supermemory from native Apple workflows.",
+ },
+ ],
+ raycast: [
+ {
+ title: "Fast desktop capture",
+ description:
+ "Add memories from Raycast on Mac without leaving the launcher.",
+ },
+ {
+ title: "Search from Raycast",
+ description:
+ "Look up Supermemory content directly from your desktop command bar.",
+ },
+ ],
+ "x-bookmarks": [
+ {
+ title: "Import saved X posts",
+ description:
+ "Turn X/Twitter bookmarks into searchable Supermemory memories.",
+ },
+ {
+ title: "Reuse social research",
+ description:
+ "Bring bookmarked threads, references, and ideas into the same memory layer as your other tools.",
+ },
+ ],
+}
+
+function getInfoUseCases(id: string): InfoUseCase[] {
+ return ITEM_INFO_USE_CASES[id] ?? MCP_INFO_USE_CASES
+}
+
+function ItemInfoButton({
+ name,
+ onClick,
+}: {
+ name: string
+ onClick: () => void
+}) {
return (
- e.stopPropagation()}
+ {
+ e.stopPropagation()
+ onClick()
+ }}
className={cn(
- dmSans125ClassName(),
- "flex size-8 shrink-0 items-center justify-center rounded-full text-[12px] text-[#A1A1AA] transition-colors hover:text-white sm:h-auto sm:w-auto sm:gap-1.5 sm:rounded-none",
+ "absolute top-4 right-4 flex size-7 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] opacity-0 transition-all hover:text-[#FAFAFA] focus-visible:opacity-100 focus-visible:outline-none group-hover:opacity-100 sm:size-8",
+ "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
)}
>
-
- Docs
-
+
+
+ )
+}
+
+function ItemInfoDialog({
+ actionSlot,
+ docsUrl,
+ icon,
+ id,
+ kind,
+ name,
+ onOpenChange,
+ open,
+}: {
+ actionSlot: ReactNode
+ docsUrl?: string
+ icon: ReactNode
+ id: string
+ kind: ItemKind
+ name: string
+ onOpenChange: (open: boolean) => void
+ open: boolean
+}) {
+ const useCases = getInfoUseCases(id)
+ const closeWithReason = (closeReason: InfoModalCloseReason) => {
+ analytics.integrationInfoModalClosed({
+ kind,
+ id,
+ name,
+ close_reason: closeReason,
+ })
+ onOpenChange(false)
+ }
+ return (
+ {
+ if (nextOpen) {
+ onOpenChange(true)
+ return
+ }
+ closeWithReason("dismiss")
+ }}
+ >
+ e.stopPropagation()}
+ 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",
+ }}
+ className={cn(
+ dmSans125ClassName(),
+ "flex max-h-[88dvh] flex-col gap-3 overflow-hidden border border-white/[0.12] bg-[#1B1F24] p-0 px-3 pt-3 pb-4 text-[#FAFAFA] rounded-2xl md:px-4 sm:max-w-[560px] sm:rounded-[22px]",
+ )}
+ >
+ {name} use cases and docs
+
+
{icon}
+
+
+ {name}
+
+
+ Use cases that apply to this Supermemory connection.
+
+
+
+ {docsUrl && (
+
+ Docs
+
+ )}
+
closeWithReason("close_button")}
+ className={cn(
+ "flex size-7 items-center justify-center rounded-full bg-[#0D121A] transition-opacity hover:opacity-80 focus:outline-none",
+ INSET,
+ )}
+ >
+
+
+
+
+
+
+
+ {useCases.map((useCase, index) => (
+
+
+
+ {index + 1}
+
+ {index < useCases.length - 1 && (
+
+ )}
+
+
+
+ {useCase.title}
+
+
+ {useCase.description}
+
+
+
+ ))}
+
+
+
+
+
closeWithReason("im_good")}
+ className={cn(
+ "px-3 py-2 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]",
+ dmSansClassName(),
+ )}
+ >
+ I'm good
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: closes the info dialog after the nested action button runs. */}
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handling stays on the nested real button. */}
+
closeWithReason("action")}>{actionSlot}
+
+
+
)
}
@@ -629,43 +980,61 @@ function ConnectionsCountPill({ count }: { count: number }) {
}
function ItemCard({
+ actionSlot,
icon,
+ id,
+ kind,
name,
tagline,
pro,
isNew,
docsUrl,
leftIndicator,
- rightSlot,
+ statusSlot,
}: {
+ actionSlot: ReactNode
icon: ReactNode
+ id: string
+ kind: ItemKind
name: string
tagline: string
pro?: boolean
isNew?: boolean
docsUrl?: string
leftIndicator?: ReactNode
- rightSlot: ReactNode
+ statusSlot?: ReactNode
}) {
+ const [infoOpen, setInfoOpen] = useState(false)
return (
+ // biome-ignore lint/a11y/useSemanticElements: the card contains nested action buttons, so it cannot be a native button.
setInfoOpen(true)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault()
+ setInfoOpen(true)
+ }
+ }}
className={cn(
- "flex h-full flex-col gap-4 rounded-[12px] bg-[#14161A] p-4 transition-colors hover:bg-[#16181D]",
+ "group relative flex h-full cursor-pointer flex-col gap-4 rounded-[12px] bg-[#14161A] p-4 transition-colors hover:bg-[#16181D] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA]/45",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
)}
>
+
setInfoOpen(true)} />
+
{icon}
- {docsUrl && (
- // biome-ignore lint/a11y/noStaticElementInteractions: wrapper to stop event propagation
-
e.stopPropagation()}
- onKeyDown={(e) => e.stopPropagation()}
- >
-
-
- )}
@@ -691,7 +1060,24 @@ function ItemCard({
{tagline}
-
{rightSlot}
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the status action. */}
+
e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+ {statusSlot}
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the primary action. */}
+
e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+ {actionSlot}
+
+
)
@@ -1413,7 +1799,7 @@ export function IntegrationsView() {
className="object-contain"
/>
),
- docsUrl: "https://docs.supermemory.ai/supermemory-mcp/introduction",
+ docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction",
ctaLabel: "Connect",
onCta: () => {
void setMcpClient(null)
@@ -1444,7 +1830,7 @@ export function IntegrationsView() {
className="object-contain"
/>
),
- docsUrl: "https://docs.supermemory.ai/integrations/claude-code",
+ docsUrl: "https://supermemory.ai/docs/integrations/claude-code",
ctaLabel: claudeCodeConnected
? "Active"
: claudeCodeNeedsPro
@@ -1502,46 +1888,35 @@ export function IntegrationsView() {
switch (item.kind) {
case "plugin": {
const activeKey = activePluginById.get(item.pluginId)
- const activeCount = activeCountByPlugin.get(item.pluginId) ?? 0
const needsProUpgrade =
!isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId)
if (activeKey) {
const busy = connectingPlugin === item.pluginId
return (
-
-
{
- trackCard(item)
- setConnectedPluginId(item.pluginId)
- }}
- />
- {
- if (needsProUpgrade) {
- handleUpgrade()
- return
- }
- trackCard(item)
- createPluginKeyMutation.mutate(item.pluginId)
- }}
- disabled={!!connectingPlugin}
- className={cn(
- "flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] disabled:opacity-50 sm:size-9",
- "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
- )}
- >
- {busy ? (
-
- ) : (
-
- )}
-
-
+ {
+ if (needsProUpgrade) {
+ handleUpgrade()
+ return
+ }
+ trackCard(item)
+ createPluginKeyMutation.mutate(item.pluginId)
+ }}
+ disabled={!!connectingPlugin}
+ className={cn(
+ "flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] disabled:opacity-50 sm:size-9",
+ "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
+ )}
+ >
+ {busy ? (
+
+ ) : (
+
+ )}
+
)
}
if (setupPluginIds.has(item.pluginId)) {
@@ -1585,24 +1960,21 @@ export function IntegrationsView() {
const needsProUpgrade = !isAutumnLoading && !hasProProduct
if (count > 0) {
return (
-
-
-
{
- trackCard(item)
- void setAddDoc("connect")
- }}
- className={cn(
- "flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] sm:size-9",
- "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
- )}
- >
-
-
-
+ {
+ trackCard(item)
+ void setAddDoc("connect")
+ }}
+ className={cn(
+ "flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] sm:size-9",
+ "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
+ )}
+ >
+
+
)
}
if (needsProUpgrade) {
@@ -1692,17 +2064,46 @@ export function IntegrationsView() {
}
}
+ const renderStatus = (item: Item): ReactNode => {
+ switch (item.kind) {
+ case "plugin": {
+ const activeKey = activePluginById.get(item.pluginId)
+ if (!activeKey) return null
+ return (
+ {
+ trackCard(item)
+ setConnectedPluginId(item.pluginId)
+ }}
+ />
+ )
+ }
+ case "connector": {
+ const count = connectionsByProvider[item.provider].length
+ if (count <= 0) return null
+ return
+ }
+ default:
+ return null
+ }
+ }
+
const renderItemCard = (item: Item) => (
)
@@ -2194,7 +2595,7 @@ export function IntegrationsView() {