mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Update integrations info modal (#1065)
## Summary - Replace percard Docs links with hover only info buttons and card click info modals - Add doc backed use cases inside setupstyle info modals, with Docs access and the same card action in the footer - Track all info modal close behavior through one analytics event with a close reason - Keep Active/Connected status in the card footer, align the info icon to the card action margin, and fix the Settings modal Integrations linkout <h3>Normal no hovering</h3> <img width="1029" height="529" alt="Screenshot 2026-06-09 at 1 21 38 PM" src="https://github.com/user-attachments/assets/ea817984-34a2-437e-9590-addea0f8d4a1" /> <h3>On hovering</h3> <img width="466" height="340" alt="Screenshot 2026-06-09 at 1 21 23 PM" src="https://github.com/user-attachments/assets/765c6132-9f91-4a9a-8141-6d61e24b688e" /> <img width="846" height="549" alt="Screenshot 2026-06-09 at 1 34 43 PM" src="https://github.com/user-attachments/assets/aaa10604-98e0-4139-b32f-b19e230210a2" /> <h3>Info modal</h3> <img width="1303" height="788" alt="Screenshot 2026-06-09 at 1 22 29 PM" src="https://github.com/user-attachments/assets/18b6228f-3a3b-481b-8459-392a39eccb1a" /> <img width="724" height="415" alt="Screenshot 2026-06-09 at 1 22 46 PM" src="https://github.com/user-attachments/assets/5e25e68b-53a9-4fa1-a5cf-1980ede7661a" />
This commit is contained in:
parent
3a3324cee3
commit
7c2bd19cd8
5 changed files with 498 additions and 95 deletions
|
|
@ -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: <GoogleDrive className="size-6" />,
|
||||
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: <Notion className="size-6" />,
|
||||
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: <OneDrive className="size-6" />,
|
||||
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<string, InfoUseCase[]> = {
|
||||
"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 (
|
||||
<a
|
||||
aria-label="Open docs"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`View ${name} use cases and docs`}
|
||||
title="Use cases and docs"
|
||||
onClick={(e) => {
|
||||
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)]",
|
||||
)}
|
||||
>
|
||||
<BookOpen className="size-3.5" />
|
||||
<span className="hidden sm:inline">Docs</span>
|
||||
</a>
|
||||
<Info className="size-3.5" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen) {
|
||||
onOpenChange(true)
|
||||
return
|
||||
}
|
||||
closeWithReason("dismiss")
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
onClick={(e) => 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]",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="sr-only">{name} use cases and docs</DialogTitle>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<IconBox>{icon}</IconBox>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[16px] font-semibold leading-tight text-[#FAFAFA]">
|
||||
{name}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[12px] text-[#A1A1AA]">
|
||||
Use cases that apply to this Supermemory connection.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{docsUrl && (
|
||||
<a
|
||||
href={docsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex h-7 items-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] text-[#A1A1AA] transition-colors hover:text-white",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<BookOpen className="size-3.5" /> Docs
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
onClick={() => 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,
|
||||
)}
|
||||
>
|
||||
<X className="size-4 text-[#737373]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 rounded-[14px] bg-[#14161A] p-4 sm:p-5",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
{useCases.map((useCase, index) => (
|
||||
<div key={useCase.title} className="flex gap-3">
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-[22px] shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[11px] font-semibold text-[#4BA0FA]",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
{index < useCases.length - 1 && (
|
||||
<span className="w-px flex-1 bg-white/[0.14]" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[15px] font-semibold leading-tight text-[#FAFAFA]">
|
||||
{useCase.title}
|
||||
</p>
|
||||
<p className="mt-1 text-[13px] leading-relaxed text-[#A1A1AA]">
|
||||
{useCase.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => closeWithReason("im_good")}
|
||||
className={cn(
|
||||
"px-3 py-2 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
I'm good
|
||||
</button>
|
||||
{/* 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. */}
|
||||
<div onClick={() => closeWithReason("action")}>{actionSlot}</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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.
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => 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)]",
|
||||
)}
|
||||
>
|
||||
<ItemInfoButton name={name} onClick={() => setInfoOpen(true)} />
|
||||
<ItemInfoDialog
|
||||
actionSlot={actionSlot}
|
||||
docsUrl={docsUrl}
|
||||
icon={icon}
|
||||
id={id}
|
||||
kind={kind}
|
||||
name={name}
|
||||
open={infoOpen}
|
||||
onOpenChange={setInfoOpen}
|
||||
/>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<IconBox>{icon}</IconBox>
|
||||
{docsUrl && (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: wrapper to stop event propagation
|
||||
<div
|
||||
role="presentation"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DocsLink href={docsUrl} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-end gap-3">
|
||||
<div className="min-w-0">
|
||||
|
|
@ -691,7 +1060,24 @@ function ItemCard({
|
|||
{tagline}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end">{rightSlot}</div>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the status action. */}
|
||||
<div
|
||||
className="flex min-w-0 flex-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{statusSlot}
|
||||
</div>
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the primary action. */}
|
||||
<div
|
||||
className="flex shrink-0 justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{actionSlot}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -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 (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<ActiveButton
|
||||
count={activeCount}
|
||||
lastActive={activeKey.lastRequest}
|
||||
onClick={() => {
|
||||
trackCard(item)
|
||||
setConnectedPluginId(item.pluginId)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Connect ${item.name} to another agent`}
|
||||
title="Connect another agent"
|
||||
onClick={() => {
|
||||
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 ? (
|
||||
<Loader className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Plus className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Connect ${item.name} to another agent`}
|
||||
title="Connect another agent"
|
||||
onClick={() => {
|
||||
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 ? (
|
||||
<Loader className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Plus className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
if (setupPluginIds.has(item.pluginId)) {
|
||||
|
|
@ -1585,24 +1960,21 @@ export function IntegrationsView() {
|
|||
const needsProUpgrade = !isAutumnLoading && !hasProProduct
|
||||
if (count > 0) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<ConnectionsCountPill count={count} />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Add another knowledge source"
|
||||
title="Add another knowledge source"
|
||||
onClick={() => {
|
||||
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)]",
|
||||
)}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Add another knowledge source"
|
||||
title="Add another knowledge source"
|
||||
onClick={() => {
|
||||
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)]",
|
||||
)}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<ActiveButton
|
||||
count={activeCountByPlugin.get(item.pluginId) ?? 0}
|
||||
lastActive={activeKey.lastRequest}
|
||||
onClick={() => {
|
||||
trackCard(item)
|
||||
setConnectedPluginId(item.pluginId)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "connector": {
|
||||
const count = connectionsByProvider[item.provider].length
|
||||
if (count <= 0) return null
|
||||
return <ConnectionsCountPill count={count} />
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const renderItemCard = (item: Item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
actionSlot={renderRight(item)}
|
||||
icon={item.icon}
|
||||
id={item.id}
|
||||
kind={item.kind}
|
||||
name={item.name}
|
||||
tagline={item.tagline}
|
||||
pro={item.pro}
|
||||
isNew={item.isNew}
|
||||
docsUrl={item.docsUrl}
|
||||
leftIndicator={renderLeftIndicator(item)}
|
||||
rightSlot={renderRight(item)}
|
||||
statusSlot={renderStatus(item)}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
@ -2194,7 +2595,7 @@ export function IntegrationsView() {
|
|||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<a
|
||||
href="https://docs.supermemory.ai/supermemory-mcp/introduction"
|
||||
href="https://supermemory.ai/docs/supermemory-mcp/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -123,13 +123,11 @@ export function SettingsContent({
|
|||
onTabChange,
|
||||
className,
|
||||
showIdentity = true,
|
||||
onClose,
|
||||
}: {
|
||||
activeTab: SettingsTab
|
||||
onTabChange: (tab: SettingsTab) => void
|
||||
className?: string
|
||||
showIdentity?: boolean
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const { user, org } = useAuth()
|
||||
const router = useRouter()
|
||||
|
|
@ -159,8 +157,7 @@ export function SettingsContent({
|
|||
}
|
||||
|
||||
const handleIntegrations = () => {
|
||||
router.push("/?view=integrations")
|
||||
onClose?.()
|
||||
void router.push("/?view=integrations")
|
||||
}
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,6 @@ export function SettingsModalProvider({ children }: { children: ReactNode }) {
|
|||
activeTab={tab}
|
||||
onTabChange={handleTabChange}
|
||||
showIdentity={false}
|
||||
onClose={() => setParam(null)}
|
||||
className="flex-1 min-h-0 w-full overflow-y-auto md:overflow-hidden px-5 md:px-4 pt-4 pb-6"
|
||||
/>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ export const analytics = {
|
|||
// integrations surface (main Nova page)
|
||||
integrationCardClicked: (props: { kind: string; id: string; name: string }) =>
|
||||
safeCapture("integration_card_clicked", props),
|
||||
integrationInfoModalClosed: (props: {
|
||||
kind: string
|
||||
id: string
|
||||
name: string
|
||||
close_reason: "dismiss" | "close_button" | "im_good" | "action"
|
||||
}) => safeCapture("integration_info_modal_closed", props),
|
||||
|
||||
nextAppResearchCtaDismissed: () =>
|
||||
safeCapture("next_app_research_cta_dismissed"),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
|
|||
name: "Claude Code",
|
||||
tagline: "Remembers your conventions, decisions, and project context",
|
||||
icon: "/images/plugins/claude-code.svg",
|
||||
docsUrl: "https://docs.supermemory.ai/integrations/claude-code",
|
||||
docsUrl: "https://supermemory.ai/docs/integrations/claude-code",
|
||||
installSteps: [
|
||||
{
|
||||
title: "Save your API key",
|
||||
|
|
@ -56,7 +56,7 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
|
|||
name: "Codex",
|
||||
tagline: "Persistent memory for the Codex CLI — free on every plan",
|
||||
icon: "/images/plugins/codex.png",
|
||||
docsUrl: "https://docs.supermemory.ai/integrations/codex",
|
||||
docsUrl: "https://supermemory.ai/docs/integrations/codex",
|
||||
githubUrl: "https://github.com/supermemoryai/codex-supermemory",
|
||||
installSteps: [
|
||||
{
|
||||
|
|
@ -79,7 +79,7 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
|
|||
name: "OpenCode",
|
||||
tagline: "Long-term memory for your OpenCode sessions",
|
||||
icon: "/images/plugins/opencode.svg",
|
||||
docsUrl: "https://docs.supermemory.ai/integrations/opencode",
|
||||
docsUrl: "https://supermemory.ai/docs/integrations/opencode",
|
||||
githubUrl: "https://github.com/supermemoryai/opencode-supermemory",
|
||||
usesOAuth: true,
|
||||
installSteps: [
|
||||
|
|
@ -108,7 +108,7 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
|
|||
name: "OpenClaw",
|
||||
tagline: "Cross-platform memory across Telegram, Discord, Slack",
|
||||
icon: "/images/plugins/openclaw.svg",
|
||||
docsUrl: "https://docs.supermemory.ai/integrations/openclaw",
|
||||
docsUrl: "https://supermemory.ai/docs/integrations/openclaw",
|
||||
installSteps: [
|
||||
{
|
||||
title: "Install the plugin",
|
||||
|
|
@ -128,7 +128,7 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
|
|||
name: "Hermes",
|
||||
tagline: "Persistent memory for the Hermes agent — free on every plan",
|
||||
icon: "/images/plugins/hermes.svg",
|
||||
docsUrl: "https://docs.supermemory.ai/integrations/hermes",
|
||||
docsUrl: "https://supermemory.ai/docs/integrations/hermes",
|
||||
installSteps: [
|
||||
{
|
||||
title: "Run Hermes memory setup",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue