diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 331f950cc..700e93a07 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -2,12 +2,13 @@ import { EnsureWorkspace } from "@/components/ensure-workspace" import { PWAInstallPrompt } from "@/components/pwa-install-prompt" +import { SettingsModalProvider } from "@/components/settings/settings-modal" export default function AppLayout({ children }: { children: React.ReactNode }) { return ( - <> + {children} - + ) } diff --git a/apps/web/app/(app)/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx index 51745b80a..32b741262 100644 --- a/apps/web/app/(app)/settings/page.tsx +++ b/apps/web/app/(app)/settings/page.tsx @@ -1,756 +1,23 @@ "use client" -import { Logo } from "@ui/assets/Logo" -import { UserProfileMenu } from "@/components/user-profile-menu" -import { useAuth } from "@lib/auth-context" -import NovaOrb from "@/components/nova/nova-orb" -import { useState, useEffect, useRef, useMemo } from "react" -import { cn } from "@lib/utils" -import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" -import Account from "@/components/settings/account" -import Billing from "@/components/settings/billing" -import Integrations from "@/components/settings/integrations" -import ConnectionsMCP from "@/components/settings/connections-mcp" -import Support from "@/components/settings/support" -import { ErrorBoundary } from "@/components/error-boundary" -import { useRouter } from "next/navigation" -import { useIsMobile } from "@hooks/use-mobile" -import { useLocalStorageUsername } from "@hooks/use-local-storage-username" -import { analytics } from "@/lib/analytics" -import { - LogOut, - RotateCcw, - Trash2, - Sun, - LoaderIcon, - User as UserIcon, - Zap, - HelpCircle, - CreditCard, - ShieldAlert, - ChevronRight, - ChevronsUpDown, - Check, - Building2, -} from "lucide-react" -import { authClient } from "@lib/auth" -import { Dialog, DialogContent, DialogClose } from "@ui/components/dialog" -import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" -import { useResetOrganization } from "@/hooks/use-reset-organization" -import { useDeleteUserAccount } from "@/hooks/use-account-settings" -import { useCustomer } from "autumn-js/react" -import { useOrgSummaries } from "@/hooks/use-org-summaries" -import { - PLAN_DISPLAY_NAMES, - useTokenUsage, - type PlanType, -} from "@/hooks/use-token-usage" - -const TABS = [ - "account", - "billing", - "integrations", - "connections", - "support", -] as const -type SettingsTab = (typeof TABS)[number] - -type NavItem = { - id: SettingsTab - label: string - description: string - icon: React.ReactNode -} - -const NAV_ITEMS: NavItem[] = [ - { - id: "account", - label: "Account", - description: "Your profile and organization", - icon: , - }, - { - id: "billing", - label: "Billing", - description: "Plan, usage and payments", - icon: , - }, - { - id: "integrations", - label: "Integrations", - description: "Save, sync and search across tools", - icon: , - }, - { - id: "connections", - label: "Connections & MCP", - description: "Drive, Notion, OneDrive, MCP", - icon: , - }, - { - id: "support", - label: "Support & Help", - description: "Get help or share feedback", - icon: , - }, -] - -function parseHashToTab(hash: string): SettingsTab { - const cleaned = hash.replace("#", "").toLowerCase() - return TABS.includes(cleaned as SettingsTab) - ? (cleaned as SettingsTab) - : "account" -} - -const ORG_PLAN_BADGE_STYLES: Record = { - free: "bg-[#2E353D] font-mono font-medium tracking-[0.12em] text-[#A3A3A3]", - pro: "bg-[#4BA0FA] font-bold tracking-[0.36px] text-[#00171A]", - max: "bg-[#1E7FE0] font-bold tracking-[0.36px] text-[#00171A]", - scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]", - enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]", -} - -function OrgPlanBadge({ plan }: { plan: PlanType }) { - return ( - - {PLAN_DISPLAY_NAMES[plan]} - - ) -} -function resolveOrgPlan( - orgId: string, - isCurrent: boolean, - currentPlan: PlanType, - planByOrgId: Map, -): PlanType { - const fromSummary = planByOrgId.get(orgId) - if (fromSummary) return fromSummary - if (isCurrent) return currentPlan - return "free" -} - -function SectionLabel({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ) -} - -function IdentityCard({ displayName }: { displayName: string }) { - const firstName = displayName?.split(" ")[0] || "" - - return ( -
- -
- -
-

- {firstName ? `${firstName}'s` : "Your"} -

-

- supermemory -

-
-
-
- ) -} +import { useEffect } from "react" +import { useRouter } from "next/navigation" +import { parseHashToTab } from "@/components/settings/settings-content" -export default function SettingsPage() { - const { user, org, organizations, setActiveOrg } = useAuth() - const [activeTab, setActiveTab] = useState("account") - const hasInitialized = useRef(false) +/** + * Legacy redirect: /settings (and /settings#billing etc.) now open the + * settings modal via the ?settings= URL state on the home page. + */ +export default function SettingsRedirect() { const router = useRouter() - const isMobile = useIsMobile() - const localStorageUsername = useLocalStorageUsername() - - const [isResetDialogOpen, setIsResetDialogOpen] = useState(false) - const [resetConfirmation, setResetConfirmation] = useState("") - const resetOrganization = useResetOrganization() - - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) - const [deleteEmailConfirm, setDeleteEmailConfirm] = useState("") - const deleteUserAccount = useDeleteUserAccount() - - const [dangerMenuOpen, setDangerMenuOpen] = useState(false) - const [orgSwitcherOpen, setOrgSwitcherOpen] = useState(false) - const [switchingOrgId, setSwitchingOrgId] = useState(null) - const canSwitchOrg = (organizations?.length ?? 0) > 1 - - const autumn = useCustomer() - const { currentPlan } = useTokenUsage(autumn) - const { data: orgSummaries } = useOrgSummaries() - const planByOrgId = useMemo(() => { - const map = new Map() - for (const summary of orgSummaries ?? []) { - map.set(summary.orgId, summary.plan) - } - return map - }, [orgSummaries]) - const activeOrgPlan = org?.id - ? resolveOrgPlan(org.id, true, currentPlan, planByOrgId) - : currentPlan - - const handleOrgSwitch = async (orgSlug: string, orgId: string) => { - if (orgId === org?.id) { - setOrgSwitcherOpen(false) - return - } - setSwitchingOrgId(orgId) - try { - await setActiveOrg(orgSlug) - window.location.reload() - } catch (error) { - console.error("Failed to switch organization:", error) - setSwitchingOrgId(null) - } - } - - const handleLogout = async () => { - await authClient.signOut() - router.push("/login") - } - - const handleDeleteAccount = async () => { - if (deleteEmailConfirm !== user?.email) return - deleteUserAccount.mutate( - { confirmation: deleteEmailConfirm }, - { - onSuccess: () => { - setIsDeleteDialogOpen(false) - setDeleteEmailConfirm("") - router.push("/login") - }, - }, - ) - } useEffect(() => { - if (hasInitialized.current) return - hasInitialized.current = true - - const hash = window.location.hash + const hash = typeof window !== "undefined" ? window.location.hash : "" const tab = parseHashToTab(hash) - setActiveTab(tab) - analytics.settingsTabChanged({ tab }) - - if (!hash || !TABS.includes(hash.replace("#", "") as SettingsTab)) { - window.history.pushState(null, "", "#account") - } - }, []) - - useEffect(() => { - const handleHashChange = () => { - const tab = parseHashToTab(window.location.hash) - setActiveTab(tab) - analytics.settingsTabChanged({ tab }) - } - - window.addEventListener("hashchange", handleHashChange) - return () => window.removeEventListener("hashchange", handleHashChange) - }, []) - - const headerDisplayName = - user?.displayUsername || - localStorageUsername || - user?.name || - user?.email?.split("@")[0] || - "" - - return ( -
-
- -
- {!isMobile && - (canSwitchOrg ? ( - - - - - - {[...(organizations ?? [])] - .sort((a, b) => a.name.localeCompare(b.name)) - .map((organization) => { - const isCurrent = organization.id === org?.id - const isSwitching = switchingOrgId === organization.id - const plan = resolveOrgPlan( - organization.id, - isCurrent, - currentPlan, - planByOrgId, - ) - return ( - - ) - })} - - - ) : ( -
- - - - - {org?.name ?? "Personal"} - - -
- ))} - -
-
- -
-
- {/* Left rail */} - - - {/* Content */} -
- - Something went wrong loading this section.{" "} - -

- } - > - {activeTab === "account" && } - {activeTab === "billing" && } - {activeTab === "integrations" && } - {activeTab === "connections" && } - {activeTab === "support" && } -
-
-
-
- - {/* Reset data dialog */} - {(() => { - const confirmText = org?.name || user?.name || "" - return ( - { - setIsResetDialogOpen(open) - if (!open) setResetConfirmation("") - }} - > - -
-
-

- Reset all data? -

-

- This permanently removes: -

-
    -
  • All documents and memories
  • -
  • All connections (Google Drive, Notion, etc.)
  • -
  • All custom spaces (default space stays)
  • -
  • Organization settings and filters
  • -
-

- Your account and billing plan stay intact.{" "} - - This cannot be undone. - -

-
-
-

- Type{" "} - - {confirmText || "your name"} - {" "} - to confirm: -

- setResetConfirmation(e.target.value)} - placeholder={confirmText || "Your name"} - autoComplete="off" - className="w-full rounded-xl border border-[#2A2D35] bg-[#0D0F14] px-4 py-2.5 text-sm text-white placeholder:text-[#525D6E] focus:outline-none focus:border-[#C7991B]/50 transition-colors" - /> -
-
- - - - -
-
-
-
- ) - })()} + router.replace( + tab === "integrations" ? "/?view=integrations" : `/?settings=${tab}`, + ) + }, [router]) - {/* Delete account dialog */} - { - setIsDeleteDialogOpen(open) - if (!open) setDeleteEmailConfirm("") - }} - > - -
-
-

- Delete your account? -

-

- Permanently deletes all your data and cancels any active - subscriptions.{" "} - - This cannot be undone. - -

-
-
-

- Type your email{" "} - {user?.email} to - confirm: -

- setDeleteEmailConfirm(e.target.value)} - placeholder={user?.email ?? "your@email.com"} - className="w-full rounded-xl border border-[#2A2D35] bg-[#0D0F14] px-4 py-2.5 text-sm text-white placeholder:text-[#525D6E] focus:outline-none focus:border-[#C73B1B]/50 transition-colors" - /> -
-
- - - - -
-
-
-
-
- ) + return null } diff --git a/apps/web/components/bottom-nav.tsx b/apps/web/components/bottom-nav.tsx index cf0a39716..af72c5a67 100644 --- a/apps/web/components/bottom-nav.tsx +++ b/apps/web/components/bottom-nav.tsx @@ -10,7 +10,6 @@ import { LifeBuoy, Settings, } from "lucide-react" -import { useRouter } from "next/navigation" import { useQueryState } from "nuqs" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" @@ -25,6 +24,7 @@ import { import { useViewMode, type ViewMode } from "@/lib/view-mode-context" import { feedbackParam } from "@/lib/search-params" import NovaOrb from "@/components/nova/nova-orb" +import { useSettingsModal } from "@/components/settings/settings-modal" const INTEGRATION_VIEWS: ViewMode[] = [ "integrations", @@ -43,7 +43,7 @@ interface BottomNavProps { } export function MobileBottomNav({ onAddMemory, onOpenSearch }: BottomNavProps) { - const router = useRouter() + const { openSettings } = useSettingsModal() const { viewMode, setViewMode } = useViewMode() const [, setFeedbackOpen] = useQueryState("feedback", feedbackParam) @@ -127,7 +127,7 @@ export function MobileBottomNav({ onAddMemory, onOpenSearch }: BottomNavProps) { router.push("/settings")} + onClick={() => openSettings()} /> diff --git a/apps/web/components/documents-command-palette.tsx b/apps/web/components/documents-command-palette.tsx index 11b9151e8..6e711b291 100644 --- a/apps/web/components/documents-command-palette.tsx +++ b/apps/web/components/documents-command-palette.tsx @@ -14,6 +14,7 @@ import { useIsMobile } from "@hooks/use-mobile" import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog" import { SearchIcon, Settings, Home, Plus, Code2, Loader2 } from "lucide-react" import { DocumentIcon } from "@/components/document-icon" +import { useSettingsModal } from "@/components/settings/settings-modal" import { $fetch } from "@lib/api" type DocumentsResponse = z.infer @@ -52,6 +53,7 @@ export function DocumentsCommandPalette({ }: DocumentsCommandPaletteProps) { const isMobile = useIsMobile() const router = useRouter() + const { openSettings } = useSettingsModal() const queryClient = useQueryClient() const [search, setSearch] = useState("") const [selectedIndex, setSelectedIndex] = useState(0) @@ -86,7 +88,7 @@ export function DocumentsCommandPalette({ id: "settings", label: "Go to Settings", icon: , - action: () => close(() => router.push("/settings")), + action: () => close(() => openSettings()), }, ...(onAddMemory ? [ diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index c70c62d07..cc3afb71e 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -30,7 +30,6 @@ import { } from "@ui/components/dropdown-menu" import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip" import { useProject } from "@/stores" -import { useRouter } from "next/navigation" import Link from "next/link" import { SpaceSelector } from "./space-selector" import { useIsMobile } from "@hooks/use-mobile" @@ -44,6 +43,7 @@ import { useCustomer } from "autumn-js/react" import { useTokenUsage } from "@/hooks/use-token-usage" import { useOrgSummaries } from "@/hooks/use-org-summaries" import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge" +import { useSettingsModal } from "@/components/settings/settings-modal" interface HeaderProps { onAddMemory?: () => void @@ -74,7 +74,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) { (orgSummaries ?? []).map((s) => [s.orgId, s.plan] as const), ) const { selectedProjects, setSelectedProjects } = useProject() - const router = useRouter() + const { openSettings } = useSettingsModal() const isMobile = useIsMobile() const [feedbackOpen, setFeedbackOpen] = useQueryState( "feedback", @@ -421,7 +421,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) { Feedback router.push("/settings")} + onClick={() => openSettings()} 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" > diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 8b5f042b9..97466f356 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -732,15 +732,23 @@ function FeaturedHero({ picks }: { picks: FeaturedPick[] }) { if (!pick) return null return ( - + ) } diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index fa3d7dc9d..347f58bec 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -40,13 +40,18 @@ import { toast } from "sonner" import { useContainerTags } from "@/hooks/use-container-tags" import { PopoverAnchor } from "@ui/components/popover" import { OrgContext } from "@/components/settings/org-context" +import { OrgPlanBadge } from "@/components/org-plan-badge" +import { useTokenUsage } from "@/hooks/use-token-usage" +import { useOrgSummaries } from "@/hooks/use-org-summaries" +import { useCustomer } from "autumn-js/react" +import { FileText, Layers, Plug, Search } from "lucide-react" function SectionTitle({ children }: { children: React.ReactNode }) { return (

{children} @@ -127,6 +132,10 @@ function isPendingInvitation(invitation: { export default function Account() { const { user, org, refetchActiveOrg, refetchOrganizations } = useAuth() + const autumn = useCustomer() + const { currentPlan, searchesUsed } = useTokenUsage(autumn) + const { data: orgSummaries } = useOrgSummaries() + const orgSummary = orgSummaries?.find((s) => s.orgId === org?.id) const [inviteDialogOpen, setInviteDialogOpen] = useState(false) const [inviteEmail, setInviteEmail] = useState("") const [inviteRole, setInviteRole] = useState("member") @@ -202,6 +211,11 @@ export default function Account() { [org?.invitations], ) + const showTeamCard = + !canManageTeam || + pendingInvitations.length > 0 || + (org?.members?.length ?? 0) > 1 + const resetInviteForm = () => { setInviteEmail("") setInviteRole("member") @@ -372,30 +386,29 @@ export default function Account() { : "—" return ( -

-
- Profile Details +
+
-
- {/* Avatar + Name/Email */} -
-
+
+ {/* Identity */} +
+
- + {user?.name?.charAt(0) ?? "U"}
-
+

{user?.name ?? "—"} @@ -403,28 +416,32 @@ export default function Account() {

{user?.email ?? "—"}

+
+ +
-
-
-

+

+ Organization -

+
{isEditingOrgName ? (
-
- - -
+ +
) : ( -
+
{org?.name ?? "Personal"} @@ -491,49 +504,110 @@ export default function Account() { setOrgNameDraft(org?.name ?? "") setIsEditingOrgName(true) }} - className={cn( - "inline-flex size-7 shrink-0 items-center justify-center rounded-md text-[#FAFAFA] transition-colors hover:bg-white/5", - )} + className="inline-flex size-5 shrink-0 items-center justify-center rounded-md text-[#737373] transition-colors hover:bg-white/5 hover:text-[#FAFAFA]" > - + ) : null}
)}
-
-

+ Member since -

-

+ {memberSince} -

+
+
+ Overview +
+ {[ + { + label: "Memories", + value: orgSummary?.documentCount, + icon: FileText, + }, + { + label: "Spaces", + value: orgSummary?.containerTagCount, + icon: Layers, + }, + { + label: "Connections", + value: orgSummary?.activeConnectors, + icon: Plug, + }, + { label: "Searches", value: searchesUsed, icon: Search }, + ].map(({ label, value, icon: Icon }) => ( +
+ + + +
+ + {typeof value === "number" ? value.toLocaleString() : "—"} + + + {label} + +
+
+ ))} +
+
+ -
-
+
+
- Team members +
+ Team members + {(org?.members?.length ?? 0) > 0 && ( + + {org?.members?.length} + + )} +

Invite people into {org?.name ?? "your organization"} and manage @@ -541,17 +615,6 @@ export default function Account() {

- {(org?.members?.length ?? 0) > 0 && ( - - {org?.members?.length}{" "} - {org?.members?.length === 1 ? "member" : "members"} - - )} {canManageTeam && (
- -
- {!canManageTeam && ( -
-
- + {showTeamCard && ( +
+
+ {!canManageTeam && ( +
+
+ +
+

+ Only organization owners and admins can invite teammates or + change roles. +

-

- Only organization owners and admins can invite teammates or - change roles. -

-
- )} + )} - {pendingInvitations.length > 0 && ( -
-

- Pending invitations -

-
    - {pendingInvitations.map((invitation) => ( -
  • -
    - -
    -
    -

    - {invitation.email} -

    -

    - Invited as {formatRole(invitation.role)} -

    -
    - {canManageTeam && ( -
    - + {invitation.email} +

    +

    + Invited as {formatRole(invitation.role)} +

    - )} -
  • - ))} -
-
- )} - - {org?.members && org.members.length > 0 ? ( -
    - {[...org.members] - .sort((a, b) => { - const rolePriority = (r: string) => - r === "owner" ? 0 : r === "admin" ? 1 : 2 - const diff = - rolePriority(a.role.toLowerCase()) - - rolePriority(b.role.toLowerCase()) - if (diff !== 0) return diff - return (a.user?.name ?? "").localeCompare( - b.user?.name ?? "", - ) - }) - .map((m, idx) => { - const isYou = m.userId === user?.id - const memberRole = m.role.toLowerCase() - const name = m.user?.name ?? m.user?.email ?? "Unknown" - const canEditMember = - canManageTeam && !isYou && memberRole !== "owner" - return ( -
  • 0 && "border-t border-white/[0.04]", + {canManageTeam && ( +
    + +
    )} - > - - - - {(name.charAt(0) || "U").toUpperCase()} - - -
    -
    - + ))} +
+
+ )} + + {org?.members && org.members.length > 0 && ( +
    + {[...org.members] + .sort((a, b) => { + const rolePriority = (r: string) => + r === "owner" ? 0 : r === "admin" ? 1 : 2 + const diff = + rolePriority(a.role.toLowerCase()) - + rolePriority(b.role.toLowerCase()) + if (diff !== 0) return diff + return (a.user?.name ?? "").localeCompare( + b.user?.name ?? "", + ) + }) + .map((m) => { + const isYou = m.userId === user?.id + const memberRole = m.role.toLowerCase() + const name = m.user?.name ?? m.user?.email ?? "Unknown" + const canEditMember = + canManageTeam && !isYou && memberRole !== "owner" + return ( +
  • + + + + {(name.charAt(0) || "U").toUpperCase()} + + +
    +
    + + {name} + + {isYou && ( + + You + )} - > - {name} - - {isYou && ( +
    + {m.user?.email && ( - You + {m.user.email} )}
    - {m.user?.email && ( - { + if (value === memberRole) return + updateMemberRoleMutation.mutate({ + memberId: m.id, + role: value as InviteRole, + }) + }} > - {m.user.email} - + + + + + Member + Admin + + + ) : ( + )} -
- {canEditMember ? ( - - ) : ( - - )} - {canEditMember && ( - - - - - - - removeMemberMutation.mutate(m.id) - } - disabled={ - removeMemberMutation.isPending || !isOwner - } - > - - Remove member - - - - )} - - ) - })} - - ) : ( -
-
- -
-
- - Just you for now - - - Invite teammates to start collaborating. - -
-
- )} + {canEditMember && ( + + + + + + + removeMemberMutation.mutate(m.id) + } + disabled={ + removeMemberMutation.isPending || !isOwner + } + > + + Remove member + + + + )} + + ) + })} + + )} +
-
+ )}
setInviteDialogOpen(false)} className={cn( dmSans125ClassName(), - "h-9 rounded-full border border-[#161F2C] bg-[#0D121A] px-4 text-[13px] font-medium text-[#737373] transition-colors hover:bg-[#14161A] hover:text-white", + "h-9 rounded-full px-4 text-[13px] font-medium text-[#737373] transition-colors hover:bg-white/[0.04] hover:text-white", )} > Cancel diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 556874f87..2021f83b2 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -436,7 +436,7 @@ function getInvoiceProductLabel(productId: string | undefined): string { export default function Billing() { const queryClient = useQueryClient() const { user, org } = useAuth() - const autumn = useCustomer({ expand: ["payment_method"] }) + const autumn = useCustomer() const [isUpgrading, setIsUpgrading] = useState(false) const [isCancelling, setIsCancelling] = useState(false) const [isResuming, setIsResuming] = useState(false) diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx index 08e79fa32..43bbcee2e 100644 --- a/apps/web/components/settings/connections-mcp.tsx +++ b/apps/web/components/settings/connections-mcp.tsx @@ -23,8 +23,8 @@ import { useQueryState } from "nuqs" import type { ConnectionResponseSchema } from "@repo/validation/api" import type { z } from "zod" import { analytics } from "@/lib/analytics" -import { ConnectAIModal } from "@/components/connect-ai-modal" import { AddDocumentModal } from "@/components/add-document" +import { useRouter } from "next/navigation" import { RemoveConnectionDialog } from "@/components/remove-connection-dialog" import { addDocumentParam } from "@/lib/search-params" import { DEFAULT_PROJECT_ID } from "@lib/constants" @@ -421,7 +421,7 @@ export default function ConnectionsMCP() { const queryClient = useQueryClient() const autumn = useCustomer() const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) - const [mcpModalOpen, setMcpModalOpen] = useState(false) + const router = useRouter() const [removeDialog, setRemoveDialog] = useState<{ open: boolean connection: Connection | null @@ -709,14 +709,14 @@ export default function ConnectionsMCP() {

- - setMcpModalOpen(true)}> - - - Connect your AI to Supermemory - - - + router.push("/?view=integrations&cat=ai-clients")} + > + + + Connect your AI to Supermemory + +
diff --git a/apps/web/components/settings/org-context.tsx b/apps/web/components/settings/org-context.tsx index 8c17ed01d..0aeb7c9f9 100644 --- a/apps/web/components/settings/org-context.tsx +++ b/apps/web/components/settings/org-context.tsx @@ -1,70 +1,17 @@ "use client" -import { useEffect, useMemo, useState } from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { LoaderIcon, Settings, X } from "lucide-react" +import { useEffect, useState } from "react" +import { LoaderIcon } from "lucide-react" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { useOrgSettings, useUpdateOrgSettings } from "@/hooks/use-org-settings" import { cn } from "@lib/utils" -import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" - -type ContextTemplate = { - id: string - label: string - description: string - prompt: string -} - -const SURFACE_SHADOW = - "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" - -const CONTEXT_TEMPLATES: ContextTemplate[] = [ - { - id: "personal-general", - label: "General Personal Assistant", - description: - "Remember preferences, routines, relationships, plans, and life context.", - prompt: `Supermemory personal assistant. The user saves conversations, notes, and daily context. - -EXTRACT: -- Preferences: "prefers morning meetings", "allergic to peanuts" -- Routines: "works out every Tuesday and Thursday" -- Relationships: "Sarah is their manager", "lives with roommate Jake" -- Plans: "planning a trip to Japan in March" -- Life events: "moved to Austin last month", "started a new job" - -SKIP: -- Generic assistant suggestions the user did not confirm -- Pleasantries and small talk without factual content -- Repeated scheduling details already captured`, - }, - { - id: "personal-productivity", - label: "Productivity Assistant", - description: - "Focus on tasks, decisions, deadlines, workflows, and blockers.", - prompt: `Supermemory productivity tool. The user saves meeting notes, task updates, and project context. - -EXTRACT: -- Action items: "needs to send proposal to client by Friday" -- Decisions: "team decided to use Figma for design handoff" -- Deadlines: "Q3 review due September 15th" -- Workflows: "deploys happen every Wednesday via CI pipeline" -- Blockers: "waiting on legal approval before launch" - -SKIP: -- Meeting filler ("let's circle back", "good point") -- Status updates that repeat previously captured information -- Agenda items with no outcome or decision`, - }, -] function SectionTitle({ children }: { children: React.ReactNode }) { return (

{children} @@ -72,55 +19,9 @@ function SectionTitle({ children }: { children: React.ReactNode }) { ) } -function SettingsCard({ children }: { children: React.ReactNode }) { - return ( -

- {children} -
- ) -} - -function PillButton({ - children, - onClick, - disabled, - variant = "default", -}: { - children: React.ReactNode - onClick: () => void - disabled?: boolean - variant?: "default" | "ghost" | "primary" -}) { - return ( - - ) -} - export function OrgContext() { const { data: settings, isLoading, isError } = useOrgSettings() const updateSettings = useUpdateOrgSettings() - const [confirmDialog, setConfirmDialog] = useState< - "enable" | "disable" | null - >(null) - const [isManaging, setIsManaging] = useState(false) const [prompt, setPrompt] = useState("") const enabled = settings?.shouldLLMFilter ?? false @@ -132,255 +33,95 @@ export function OrgContext() { setPrompt(settings?.filterPrompt ?? "") }, [settings?.filterPrompt]) - const selectedTemplateId = useMemo(() => { - const normalized = prompt.trim() - return CONTEXT_TEMPLATES.find( - (template) => template.prompt.trim() === normalized, - )?.id - }, [prompt]) - - const handleConfirmToggle = () => { - const newEnabled = confirmDialog === "enable" - updateSettings.mutate( - newEnabled - ? { shouldLLMFilter: true } - : { shouldLLMFilter: false, filterPrompt: null }, - { - onSuccess: () => { - setConfirmDialog(null) - if (!newEnabled) { - setIsManaging(false) - setPrompt("") - } - }, - }, - ) + const handleToggle = (next: boolean) => { + updateSettings.mutate({ shouldLLMFilter: next }) } const handleSave = () => { - updateSettings.mutate( - { - shouldLLMFilter: true, - filterPrompt: prompt.trim() ? prompt.trim() : null, - }, - { - onSuccess: () => { - setIsManaging(false) - }, - }, - ) - } - - const handleCancel = () => { - setPrompt(savedPrompt) - setIsManaging(false) + updateSettings.mutate({ + shouldLLMFilter: true, + filterPrompt: prompt.trim() ? prompt.trim() : null, + }) } return ( -
-
- Organization Context -

- Guide how Nova processes and remembers your content. -

-
- -
-
-
-
-

- {enabled ? "Context is enabled" : "Context is disabled"} -

-

- {enabled - ? "New content will use your guidance when Nova creates memories." - : "Turn this on to tell Nova what matters most when it learns."} -

-
-
-
- setConfirmDialog(enabled ? "disable" : "enable")} - disabled={!settingsReady || updateSettings.isPending} - variant={enabled ? "ghost" : "primary"} - > - {enabled ? "DISABLE" : "ENABLE"} - - {enabled && ( - setIsManaging(true)} - disabled={updateSettings.isPending} - > - - MANAGE - - )} -
-
- - {enabled && !isManaging && savedPrompt && ( -
-

- {savedPrompt} -

-
- )} - - {enabled && !isManaging && !savedPrompt && ( -

- No organization context configured.{" "} - -

- )} - - {enabled && isManaging && ( -
-
-