-
- {/* Connector section - conditional layout based on hasConnections */}
- {hasConnections ? (
-
- {Object.entries(CONNECTORS).map(([provider, config]) => {
- const Icon = config.icon
- const isConnecting =
- connectingProvider === provider ||
- (addConnectionMutation.isPending &&
- addConnectionMutation.variables?.provider === provider)
-
- if (provider === "google-drive") {
- return (
-
-
handleConnect("google-drive")}
- disabled={
- !isProUser ||
- isConnecting ||
- addConnectionMutation.isPending
- }
- className="flex-1 py-3 flex items-center justify-center gap-2 hover:bg-[#1B1F24] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
- {config.title}
- {isConnecting && (
-
- )}
-
-
-
-
-
-
-
-
-
- {(
- Object.entries(GDRIVE_SCOPE_LABELS) as [
- GDriveSyncScope,
- string,
- ][]
- ).map(([scope, label]) => (
- {
- e.stopPropagation()
- setGdriveSyncScope(scope)
- }}
- className="flex items-center justify-between"
- >
- {label}
- {gdriveSyncScope === scope && (
-
- )}
-
- ))}
-
-
-
- )
- }
-
- return (
-
handleConnect(provider as ConnectorProvider)}
- disabled={
- !isProUser || isConnecting || addConnectionMutation.isPending
- }
- className="bg-[#14161A] border border-[rgba(82,89,102,0.2)] rounded-[12px] px-4 py-3 flex items-center justify-center gap-2 hover:bg-[#1B1F24] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
-
- {config.title}
-
- {isConnecting && (
-
- )}
-
- )
- })}
+ {/* Top header — only when empty; once connected, the Add CTA moves into the list header below */}
+ {!hasConnections && (
+
+
Add a connection
+
+ PRO
+
- ) : (
+ )}
+
+ {/* Provider rows — only on empty state. Each is a labelled, descriptive CTA. */}
+ {!hasConnections && (
{Object.entries(CONNECTORS).map(([provider, config]) => {
const Icon = config.icon
- const connection = connections.find(
- (conn) => conn.provider === provider,
- )
- const isConnected = !!connection
const isConnecting =
connectingProvider === provider ||
(addConnectionMutation.isPending &&
@@ -346,33 +411,14 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
-
-
{config.title}
- {isConnected && (
-
- {connection.metadata?.syncInProgress
- ? "Syncing..."
- : "Connected"}
-
- )}
-
+
{config.title}
{config.description}
- {isConnected ? (
-
handleDisconnect(connection)}
- disabled={deleteConnectionMutation.isPending}
- className="text-[#737373] hover:text-white hover:bg-[#1B1F24] h-8 w-8 p-0"
- >
-
-
- ) : provider === "google-drive" ? (
+ {provider === "google-drive" ? (
)}
- {/* Connected list panel - only when hasConnections */}
+ {/* Connected list - rich rows with status / project / last sync / doc count */}
{hasConnections && (
-
-
-
- Connected to Supermemory
-
- {connectionsLimit > 0 && (
-
- {connections.length}/{connectionsLimit} connections used
-
- )}
-
-
- {connections.map((connection) => {
- const config =
- CONNECTORS[connection.provider as ConnectorProvider]
- if (!config) return null
+
+
+
+
+
+ Connected to Supermemory
+
+
+ PRO
+
+
+ {connectionsLimit > 0 && (
+
+ {connections.length}/{connectionsLimit} connections used
+
+ )}
+
- const Icon = config.icon
- const subtext = getConnectionSubtext(connection)
-
- return (
-
+
+
-
-
-
-
- {config.title}
-
-
- {subtext}
-
-
+ {isAnyConnecting ? (
+
+ ) : (
+ <>
+
+ Add a connection
+
+ >
+ )}
+
+
+
+
+
+
+ Choose a service
+
+
+
+
{
+ setConnectingProvider("google-drive")
+ addConnectionMutation.mutate({
+ provider: "google-drive",
+ syncScope: "scoped",
+ })
+ }}
+ className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100"
+ >
+
+
+
+ Google Drive
+
+
+ Pick specific files & folders
+
+
+
+
{
+ setConnectingProvider("google-drive")
+ addConnectionMutation.mutate({
+ provider: "google-drive",
+ syncScope: "full",
+ })
+ }}
+ className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100"
+ >
+
+
+
+ Google Drive
+
+
+ Sync entire drive
+
+
+
+
{
+ setConnectingProvider("notion")
+ addConnectionMutation.mutate({ provider: "notion" })
+ }}
+ className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100"
+ >
+
+
+
+ Notion
+
+
+ Pages and databases
+
+
+
+
{
+ setConnectingProvider("onedrive")
+ addConnectionMutation.mutate({ provider: "onedrive" })
+ }}
+ className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100"
+ >
+
+
+
+ OneDrive
+
+
+ Office documents
+
+
+
-
handleDisconnect(connection)}
- disabled={deleteConnectionMutation.isPending}
- className="text-[#737373] hover:text-white hover:bg-[#1B1F24] h-8 w-8 p-0 shrink-0"
- >
-
-
- )
- })}
+
+
+
+
+ {connections.map((connection) => (
+ setRemoveDialog({ open: true, connection })}
+ isDeleting={deleteConnectionMutation.isPending}
+ />
+ ))}
)}
@@ -568,6 +708,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
)}
)}
+
{
diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx
index be611c3f..9652f5ea 100644
--- a/apps/web/components/dashboard-view.tsx
+++ b/apps/web/components/dashboard-view.tsx
@@ -389,10 +389,14 @@ function MemoryOfDayCard({ data }: { data: MemoryOfDay }) {
if (!memory) return null
+ const href = data.sourceDocumentId
+ ? `/?view=list&doc=${encodeURIComponent(data.sourceDocumentId)}`
+ : "/?view=list"
+
return (
router.push("/?view=list")}
+ onClick={() => router.push(href)}
className={cn(
"group w-full h-full text-left bg-[#0B1017] border border-[rgba(255,255,255,0.05)] rounded-[18px] p-3 flex flex-col justify-between hover:border-[rgba(255,255,255,0.10)] transition-colors cursor-pointer",
dmSansClassName(),
diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index 55e1215a..ab8b0b1b 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -23,7 +23,8 @@ import { analytics } from "@/lib/analytics"
import Image from "next/image"
import { IntegrationGridCard } from "@/components/integrations/integration-grid-card"
import { useViewMode } from "@/lib/view-mode-context"
-import type { ViewParamValue } from "@/lib/search-params"
+import { addDocumentParam, type ViewParamValue } from "@/lib/search-params"
+import { useQueryState } from "nuqs"
type Connection = z.infer
@@ -166,6 +167,7 @@ const CARD_GROUPS: Array<{ label: string; ids: CardId[] }> = [
export function IntegrationsView() {
const { setViewMode } = useViewMode()
+ const [, setAddDoc] = useQueryState("add", addDocumentParam)
const { org } = useAuth()
const autumn = useCustomer()
const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
@@ -290,6 +292,8 @@ export function IntegrationsView() {
analytics.onboardingChromeExtensionClicked({
source: "integrations",
})
+ } else if (card.id === "connections") {
+ void setAddDoc("connect")
} else {
void setViewMode(card.id as ViewParamValue)
}
diff --git a/apps/web/components/integrations/connections-detail.tsx b/apps/web/components/integrations/connections-detail.tsx
deleted file mode 100644
index 5ae87178..00000000
--- a/apps/web/components/integrations/connections-detail.tsx
+++ /dev/null
@@ -1,442 +0,0 @@
-"use client"
-
-import { dmSans125ClassName } from "@/lib/fonts"
-import { cn } from "@lib/utils"
-import { $fetch } from "@lib/api"
-import { hasActivePlan } from "@lib/queries"
-import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
-import { useCustomer } from "autumn-js/react"
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import { Check, Clock, FolderOpen, Plus, Trash2, Zap } from "lucide-react"
-import { useEffect, useState } from "react"
-import { toast } from "sonner"
-import { useQueryState } from "nuqs"
-import type { ConnectionResponseSchema } from "@repo/validation/api"
-import type { z } from "zod"
-import { analytics } from "@/lib/analytics"
-import { AddDocumentModal } from "@/components/add-document"
-import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
-import { addDocumentParam } from "@/lib/search-params"
-import { DEFAULT_PROJECT_ID } from "@lib/constants"
-import type { Project } from "@lib/types"
-
-type Connection = z.infer
-
-const CONNECTORS = {
- "google-drive": {
- title: "Google Drive",
- icon: GoogleDrive,
- documentLabel: "documents",
- },
- notion: { title: "Notion", icon: Notion, documentLabel: "pages" },
- onedrive: { title: "OneDrive", icon: OneDrive, documentLabel: "documents" },
-} as const
-
-type ConnectorProvider = keyof typeof CONNECTORS
-
-function ConnectionRow({
- connection,
- onDelete,
- isDeleting,
- disabled,
- projects,
-}: {
- connection: Connection
- onDelete: () => void
- isDeleting: boolean
- disabled?: boolean
- projects: Project[]
-}) {
- const config = CONNECTORS[connection.provider as ConnectorProvider]
- if (!config) return null
-
- const Icon = config.icon
- const isConnected =
- !connection.expiresAt || new Date(connection.expiresAt) > new Date()
-
- const formatRelativeTime = (date: string | null | undefined) => {
- if (!date) return "Never"
- const d = new Date(date)
- const diffMs = Date.now() - d.getTime()
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
- const diffDays = Math.floor(diffHours / 24)
- if (diffHours < 1) return "Just now"
- if (diffHours < 24) return `${diffHours}h ago`
- if (diffDays === 1) return "Yesterday"
- if (diffDays < 7) return `${diffDays} days ago`
- return d.toLocaleDateString()
- }
-
- const getProjectName = (tag: string): string => {
- if (tag === DEFAULT_PROJECT_ID) return "Default"
- return (
- projects.find((p) => p.containerTag === tag)?.name ??
- tag.replace(/^sm_project_/, "").replace(/_/g, " ")
- )
- }
-
- const documentCount = (connection.metadata?.documentCount as number) ?? 0
- const containerTags = (
- connection as Connection & { containerTags?: string[] }
- ).containerTags
- const projectName = containerTags?.[0]
- ? getProjectName(containerTags[0])
- : null
-
- return (
-
-
-
-
-
-
-
- {config.title}
-
-
-
-
- {isConnected ? "Connected" : "Disconnected"}
-
-
-
-
- {connection.email || "Unknown"}
-
-
-
-
-
-
-
-
- {projectName && (
-
-
-
- {projectName}
-
-
- )}
-
-
-
- {formatRelativeTime(connection.createdAt)}
-
-
-
-
-
- {documentCount}
-
-
- {config.documentLabel}
-
-
-
-
-
- )
-}
-
-export function ConnectionsDetail() {
- const queryClient = useQueryClient()
- const autumn = useCustomer()
- const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
- const [removeDialog, setRemoveDialog] = useState<{
- open: boolean
- connection: Connection | null
- }>({ open: false, connection: null })
- const [, setAddDoc] = useQueryState("add", addDocumentParam)
-
- const projects = (queryClient.getQueryData(["projects"]) ||
- []) as Project[]
-
- const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
-
- const connectionsFeature = autumn.customer?.features?.connections
- const connectionsUsed = connectionsFeature?.usage ?? 0
- const connectionsLimit = connectionsFeature?.included_usage ?? 10
- const canAddConnection = connectionsUsed < connectionsLimit
-
- const {
- data: connections = [],
- isLoading: isLoadingConnections,
- error: connectionsError,
- } = useQuery({
- queryKey: ["connections"],
- queryFn: async () => {
- const response = await $fetch("@post/connections/list", {
- body: { containerTags: [] },
- })
- if (response.error)
- throw new Error(response.error?.message || "Failed to load connections")
- return response.data as Connection[]
- },
- staleTime: 30 * 1000,
- refetchInterval: 60 * 1000,
- enabled: hasProProduct,
- })
-
- useEffect(() => {
- if (connectionsError) {
- toast.error("Failed to load connections", {
- description:
- connectionsError instanceof Error
- ? connectionsError.message
- : "Unknown error",
- })
- }
- }, [connectionsError])
-
- const deleteConnectionMutation = useMutation({
- mutationFn: async ({
- connectionId,
- deleteDocuments,
- }: {
- connectionId: string
- deleteDocuments: boolean
- }) => {
- await $fetch(`@delete/connections/${connectionId}`, {
- query: { deleteDocuments },
- })
- return { deleteDocuments }
- },
- onSuccess: (_data, variables) => {
- analytics.connectionDeleted()
- toast.success(
- variables.deleteDocuments
- ? "Connection removal has started. Documents will be permanently deleted in the next few minutes."
- : "Connection removed. Your memories have been kept.",
- )
- setRemoveDialog({ open: false, connection: null })
- queryClient.invalidateQueries({ queryKey: ["connections"] })
- },
- onError: (error) => {
- toast.error("Failed to remove connection", {
- description: error instanceof Error ? error.message : "Unknown error",
- })
- },
- })
-
- const handleUpgrade = async () => {
- try {
- await autumn.attach({
- productId: "api_pro",
- successUrl: "https://app.supermemory.ai/?view=integrations",
- })
- window.location.reload()
- } catch (error) {
- console.error(error)
- }
- }
-
- const isLoading = autumn.isLoading
-
- return (
-
- {!hasProProduct && !isLoading && (
- <>
-
-
-
-
-
- Connect Google Drive, Notion, and OneDrive to import your
- knowledge
-
-
- {[
- "Unlimited memories",
- "10 connections",
- "Advanced search",
- "Priority support",
- ].map((text) => (
-
-
-
- {text}
-
-
- ))}
-
-
- Upgrade to Pro
-
-
-
- >
- )}
-
-
-
-
- Connected to Supermemory
-
-
- {connections.length}/{connectionsLimit} connections used
-
-
-
- {isLoadingConnections ? (
-
- ) : connections.length > 0 ? (
- connections.map((connection) => (
-
setRemoveDialog({ open: true, connection })}
- isDeleting={deleteConnectionMutation.isPending}
- disabled={!hasProProduct}
- projects={projects}
- />
- ))
- ) : (
-
-
-
- No connections yet
-
-
- Connect a service below to import your knowledge
-
-
- )}
-
- setIsAddDocumentOpen(false)}
- />
-
- {
- if (!open) setRemoveDialog({ open: false, connection: null })
- }}
- provider={removeDialog.connection?.provider}
- documentCount={
- (removeDialog.connection?.metadata?.documentCount as number) ?? 0
- }
- onConfirm={(deleteDocuments) => {
- if (removeDialog.connection) {
- deleteConnectionMutation.mutate({
- connectionId: removeDialog.connection.id,
- deleteDocuments,
- })
- }
- }}
- isDeleting={deleteConnectionMutation.isPending}
- />
-
- setAddDoc("connect")}
- disabled={!hasProProduct || !canAddConnection}
- className={cn(
- "relative flex items-center justify-center gap-2",
- "bg-[#0D121A] rounded-full h-11 px-4 w-full",
- "cursor-pointer transition-opacity hover:opacity-80",
- "shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
- "disabled:opacity-50 disabled:cursor-not-allowed",
- dmSans125ClassName(),
- )}
- >
-
-
- Connect knowledge bases
-
-
-
-
- )
-}
diff --git a/apps/web/components/remove-connection-dialog.tsx b/apps/web/components/remove-connection-dialog.tsx
index 5a83c719..f003adfa 100644
--- a/apps/web/components/remove-connection-dialog.tsx
+++ b/apps/web/components/remove-connection-dialog.tsx
@@ -5,6 +5,7 @@ import { cn } from "@lib/utils"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { Loader2, XIcon } from "lucide-react"
import { Button } from "@ui/components/button"
+import { Checkbox } from "@ui/components/checkbox"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import {
Dialog,
@@ -42,12 +43,15 @@ export function RemoveConnectionDialog({
onConfirm,
isDeleting,
}: RemoveConnectionDialogProps) {
- const [action, setAction] = useState<"keep" | "delete">("keep")
+ const [alsoDelete, setAlsoDelete] = useState(false)
const displayName =
providerName || (provider ? PROVIDER_LABELS[provider] : "this connection")
+ const memoryNoun = documentCount === 1 ? "memory" : "memories"
+ const hasMemories = documentCount > 0
+
const handleConfirm = () => {
- onConfirm(action === "delete")
+ onConfirm(alsoDelete)
}
return (
@@ -56,13 +60,13 @@ export function RemoveConnectionDialog({
onOpenChange={(o) => {
if (!isDeleting) {
onOpenChange(o)
- if (!o) setAction("keep")
+ if (!o) setAlsoDelete(false)
}
}}
>
-
-
-
-
- Remove connection
-
-
- What would you like to do with the{" "}
- {documentCount > 0 ? (
- <>
-
- {documentCount}
- {" "}
- memories from{" "}
- >
- ) : (
- <>memories from >
- )}
-
- {displayName}
-
- ?
-
-
+
+
+
+ Disconnect {displayName}?
+
-
-
setAction("keep")}
- className={cn(
- "flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
- action === "keep"
- ? "bg-[#14161A] border border-[rgba(82,89,102,0.3)]"
- : "bg-[#14161A]/50 border border-transparent hover:border-[rgba(82,89,102,0.2)]",
- )}
- style={{
- boxShadow:
- action === "keep"
- ? "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)"
- : "none",
- }}
- >
-
- {action === "keep" && (
-
- )}
-
-
-
- Remove connection only
-
-
- Disconnect the integration but keep all imported memories
-
-
-
+
+ {hasMemories ? (
+ <>
+ Sync stops. Your{" "}
+
+ {documentCount} {memoryNoun}
+ {" "}
+ stay in Supermemory.
+ >
+ ) : (
+ <>Sync stops. No memories were imported from this connection.>
+ )}
+
-
setAction("delete")}
- className={cn(
- "flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
- action === "delete"
- ? "bg-[#14161A] border border-[rgba(220,38,38,0.3)]"
- : "bg-[#14161A]/50 border border-transparent hover:border-[rgba(82,89,102,0.2)]",
- )}
- style={{
- boxShadow:
- action === "delete"
- ? "0px 1px 2px 0px rgba(87,0,0,0.1), inset 0px 0px 0px 1px rgba(67,43,43,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08)"
- : "none",
- }}
+ {hasMemories && (
+
-
- {action === "delete" && (
-
- )}
-
-
-
- Remove connection and memories
-
-
- Permanently delete all memories imported from this connection
-
-
-
-
+
setAlsoDelete(checked === true)}
+ disabled={isDeleting}
+ />
+
+ Also delete the {documentCount} imported {memoryNoun}{" "}
+ (optional)
+
+
+ )}
{
onOpenChange(false)
- setAction("keep")
+ setAlsoDelete(false)
}}
className="text-[#737373] cursor-pointer rounded-full"
>
@@ -203,19 +146,18 @@ export function RemoveConnectionDialog({
disabled={isDeleting}
onClick={handleConfirm}
className={cn(
- action === "delete" &&
- "bg-red-600! hover:bg-red-700! text-white",
+ alsoDelete && "bg-red-600! hover:bg-red-700! text-white",
)}
>
{isDeleting ? (
<>
- Removing...
+ Disconnecting...
>
- ) : action === "delete" ? (
- "Remove & delete memories"
+ ) : alsoDelete ? (
+ `Disconnect and delete ${memoryNoun}`
) : (
- "Remove connection"
+ "Disconnect"
)}