diff --git a/README.md b/README.md
index a134afad..7760c85b 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
- State-of-the-art memory and context engine for AI.
+ State-of-the-art memory and context engine for AI. And yes - you can use it as a company/personal brain.
diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx
index 498a75a2..ac646ff7 100644
--- a/apps/browser-extension/entrypoints/popup/App.tsx
+++ b/apps/browser-extension/entrypoints/popup/App.tsx
@@ -175,10 +175,21 @@ function App() {
setShowProjectSelector(true)
}
+ // Reconcile stored default against live list: reset if deleted, refresh if renamed.
useEffect(() => {
- if (!defaultProject && projects.length > 0) {
- const firstProject = projects[0]
- setDefaultProjectMutation.mutate(firstProject)
+ if (projects.length === 0) return
+ if (!defaultProject) {
+ setDefaultProjectMutation.mutate(projects[0])
+ return
+ }
+ const live = projects.find((p) => p.id === defaultProject.id)
+ if (!live) {
+ setDefaultProjectMutation.mutate(projects[0])
+ } else if (
+ live.name !== defaultProject.name ||
+ live.containerTag !== defaultProject.containerTag
+ ) {
+ setDefaultProjectMutation.mutate(live)
}
}, [defaultProject, projects, setDefaultProjectMutation])
diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts
index c810e73f..b7b6147f 100644
--- a/apps/browser-extension/wxt.config.ts
+++ b/apps/browser-extension/wxt.config.ts
@@ -29,7 +29,7 @@ export default defineConfig({
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
- version: "6.1.3",
+ version: "6.1.4",
permissions: ["storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx
index 2c5eaf07..3d67b3ba 100644
--- a/apps/web/app/(app)/onboarding/page.tsx
+++ b/apps/web/app/(app)/onboarding/page.tsx
@@ -41,7 +41,7 @@ import {
CheckCircle2,
Loader2,
} from "lucide-react"
-import { analytics } from "@/lib/analytics"
+import { analytics, type OnboardingStep } from "@/lib/analytics"
import { consumePendingConnectUrl } from "@/lib/constants"
type DetectedSource = "x" | "linkedin" | "resume" | null
@@ -378,6 +378,13 @@ function isAccountSource(source: DetectedSource): source is "x" | "linkedin" {
return source === "x" || source === "linkedin"
}
+const STATUS_TO_STEP: Record = {
+ idle: "profile_input",
+ processing: "processing",
+ done: "done",
+ error: "error",
+}
+
function useSpotlightAutoRotation(
status: Status,
pauseSpotlight: boolean,
@@ -555,6 +562,7 @@ export default function OnboardingPage() {
const fileRef = useRef(null)
const pollingRef = useRef | null>(null)
const skippingRef = useRef(false)
+ const completedTrackedRef = useRef(false)
const [isSkipping, setIsSkipping] = useState(false)
const [spotlightCategory, setSpotlightCategory] =
useState("productivity")
@@ -591,6 +599,21 @@ export default function OnboardingPage() {
usePollingCleanup(pollingRef)
useDoneAnimation(status, setStampLanded, setVisibleSnippets)
+ // biome-ignore lint/correctness/useExhaustiveDependencies: fire per status transition only
+ useEffect(() => {
+ analytics.onboardingStepViewed({
+ step: STATUS_TO_STEP[status],
+ trigger: "auto",
+ })
+ if (status === "done" && !completedTrackedRef.current) {
+ completedTrackedRef.current = true
+ analytics.onboardingCompleted({
+ source: isAccountSource(detected) ? detected : undefined,
+ memories_count: memoriesCount,
+ })
+ }
+ }, [status])
+
const handleChange = (v: string) => {
setValue(v)
setDetected(detectSource(v))
@@ -619,6 +642,7 @@ export default function OnboardingPage() {
if (skippingRef.current) return
skippingRef.current = true
setIsSkipping(true)
+ analytics.onboardingSkipped({ from_step: STATUS_TO_STEP[status] })
try {
await ensureOrg()
const pendingPath = consumePendingConnectUrl()
@@ -628,7 +652,7 @@ export default function OnboardingPage() {
skippingRef.current = false
setIsSkipping(false)
}
- }, [ensureOrg, router])
+ }, [ensureOrg, router, status])
const pollDocument = useCallback((docId: string) => {
const maxAttempts = 60
@@ -688,6 +712,7 @@ export default function OnboardingPage() {
const handleSubmit = useCallback(
async (source: "x" | "linkedin" | "resume", resumeFileOverride?: File) => {
+ analytics.onboardingProfileSubmitted({ source })
setStatus("processing")
setSpotlightCategory("productivity")
setPauseSpotlight(false)
diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx
index 6c70deb2..17433857 100644
--- a/apps/web/app/(app)/page.tsx
+++ b/apps/web/app/(app)/page.tsx
@@ -572,7 +572,7 @@ export default function NewPage() {
const isDashboardShell =
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
const isGraphMode = viewMode === "graph"
- const showBottomNav = isMobile && !isChatView && !!session
+ const showBottomNav = isMobile && !!session
return (
diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx
index 49fc56fc..dbdacdfc 100644
--- a/apps/web/app/(auth)/login/page.tsx
+++ b/apps/web/app/(auth)/login/page.tsx
@@ -140,14 +140,11 @@ export default function LoginPage() {
)
return
}
- router.replace("/")
- }, [
- sessionPending,
- sessionData?.session,
- oauthQueryForResume,
- params,
- router,
- ])
+ // Carry the flag so the dashboard posts the session token to the extension (else: sign-in loop).
+ const dest = new URL("/", window.location.origin)
+ dest.searchParams.set("extension-auth-success", "true")
+ window.location.assign(dest.toString())
+ }, [sessionPending, sessionData?.session, oauthQueryForResume, params])
// Get redirect URL from query params
const redirectUrl = params.get("redirect")
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 5218f989..f044e533 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -38,6 +38,7 @@ export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
+ interactiveWidget: "resizes-content",
}
export default function RootLayout({
diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx
index ac92197c..6dccfb7d 100644
--- a/apps/web/components/chat/index.tsx
+++ b/apps/web/components/chat/index.tsx
@@ -19,7 +19,6 @@ import {
} from "@ui/components/sheet"
import { ScrollArea } from "@ui/components/scroll-area"
import {
- ArrowLeft,
Check,
ChevronDownIcon,
HistoryIcon,
@@ -112,7 +111,7 @@ const CHAT_QUEUE_LIMIT = 5
export function ChatSidebar({
isChatOpen,
- setIsChatOpen,
+ setIsChatOpen: _setIsChatOpen,
queuedMessage,
queuedHighlightContent,
onConsumeQueuedMessage,
@@ -1147,25 +1146,13 @@ export function ChatSidebar({
"flex items-center justify-between px-0 z-10",
isPageDesktop
? "relative shrink-0 pt-2 pb-1"
- : "absolute top-0 right-0 left-0 pt-4 px-4",
+ : isMobile
+ ? "relative shrink-0 px-4 pt-4 pb-2"
+ : "absolute top-0 right-0 left-0 pt-4 px-4",
!isMobile && !isPageDesktop && "rounded-t-2xl",
)}
>
- {layout === "page" && isMobile && (
-
setIsChatOpen(false)}
- aria-label="Back to memories"
- >
-
-
- )}
{!isStackedInput && (
<>
0
? cn(
"flex flex-col space-y-3 min-h-full justify-end",
- isPageDesktop ? "pt-2" : "pt-14",
+ isPageDesktop || isMobile ? "pt-2" : "pt-14",
)
: ""
}
@@ -1406,7 +1393,9 @@ export function ChatSidebar({
className={cn(
"shrink-0",
isStackedInput &&
- "pb-[max(1.25rem,calc(env(safe-area-inset-bottom)+1rem))] md:pb-6",
+ (isMobile
+ ? "px-4 pb-2"
+ : "px-4 pb-[max(1.25rem,calc(env(safe-area-inset-bottom)+1rem))] md:pb-6"),
)}
>
diff --git a/apps/web/components/document-modal/graph-list-memories.tsx b/apps/web/components/document-modal/graph-list-memories.tsx
index c8f4ae8d..313424e7 100644
--- a/apps/web/components/document-modal/graph-list-memories.tsx
+++ b/apps/web/components/document-modal/graph-list-memories.tsx
@@ -169,9 +169,11 @@ function VersionStatus({
export function GraphListMemories({
memoryEntries,
documentId,
+ className,
}: {
memoryEntries: MemoryEntry[]
documentId?: string
+ className?: string
}) {
const { effectiveContainerTags } = useProject()
const [expandedMemories, setExpandedMemories] = useState>(
@@ -193,7 +195,10 @@ export function GraphListMemories({
return (
!open && onClose()}>
-
+ const hasPluginInsights =
+ pluginDocument &&
+ pluginDocument.kind !== "claude-code-doc" &&
+ pluginDocument.kind !== "openclaw-session"
+ const hasDocumentInsights = Boolean(
+ hasPluginInsights ||
+ _document?.summary ||
+ pluginDocument?.summary ||
+ (_document?.memoryEntries && _document.memoryEntries.length > 0),
+ )
+
+ const documentPreview = (
+
+
+
+ )
+
+ const documentInsights = (
+
+ {hasPluginInsights &&
}
+ {_document && (_document.summary || pluginDocument?.summary) && (
+
+ )}
+ {_document?.memoryEntries && _document.memoryEntries.length > 0 && (
+
+ )}
+
+ )
+
+ const modalContent = (
+ <>
+ {isMobile ? (
+
+ {_document?.title} - Document
+
+ ) : (
{_document?.title} - Document
-
-
-
-
-
- {pluginDocument?.kind === "claude-code-doc" &&
- _document?.customId && (
-
- )}
-
- {_document?.url && (
-
- {!isMobile && (
- Visit source
- )}
-
-
+ )}
+
+
+
+
+
+ {pluginDocument?.kind === "claude-code-doc" &&
+ _document?.customId && (
+
)}
+
+ {_document?.url && (
+
+ {!isMobile && Visit source }
+
+
+ )}
+ {isMobile ? (
+
+
+ Close
+
+ ) : (
Close
-
+ )}
-
-
+ {isMobile && hasDocumentInsights ? (
+
+
+
+ Content
+
+
+ Insights
+
+
+
+ {documentPreview}
+
+
-
-
-
- {pluginDocument &&
- pluginDocument.kind !== "claude-code-doc" &&
- pluginDocument.kind !== "openclaw-session" && (
-
- )}
- {_document && (_document.summary || pluginDocument?.summary) && (
-
- )}
- {_document?.memoryEntries && _document.memoryEntries.length > 0 && (
-
- )}
-
+ {documentInsights}
+
+
+ ) : isMobile ? (
+
{documentPreview}
+ ) : (
+
+ {documentPreview}
+ {documentInsights}
+ )}
+ >
+ )
+
+ if (isMobile) {
+ return (
+
!open && onClose()}
+ shouldScaleBackground
+ >
+ div:first-child]:bg-[#3A4252] [&>div:first-child]:h-1 [&>div:first-child]:w-9 [&>div:first-child]:mt-2.5 [&>div:first-child]:mb-1",
+ dmSansClassName(),
+ )}
+ style={{
+ boxShadow:
+ "0 -12px 40px rgba(0, 0, 0, 0.45), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
+ }}
+ >
+
+ {modalContent}
+
+
+
+ )
+ }
+
+ return (
+
!open && onClose()}>
+
+ {modalContent}
)
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
index fd81bf52..c70c62d0 100644
--- a/apps/web/components/header.tsx
+++ b/apps/web/components/header.tsx
@@ -19,6 +19,7 @@ import {
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
+import { getBillingSettingsUrl } from "@/lib/url-helpers"
import { GraphIcon, IntegrationsIcon } from "@/components/integration-icons"
import {
DropdownMenu,
@@ -80,6 +81,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
feedbackParam,
)
const { viewMode, setViewMode } = useViewMode()
+ const billingSettingsUrl = getBillingSettingsUrl()
const handleFeedback = () => setFeedbackOpen(true)
@@ -352,6 +354,15 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
+
+
+
+ Upgrade
+
+
) : (
<>
+
+
+
+
+
+ Upgrade
+
+
+
+
+ Upgrade
+
+
{
markOrgOnboarded()
- analytics.onboardingCompleted()
const pendingPath = consumePendingConnectUrl()
router.push(pendingPath ?? "/")
}
diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index e602f070..5a5a7161 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -1135,7 +1135,10 @@ export function IntegrationsView() {
}
throw new Error(response.error?.message || "Failed to connect")
},
- onMutate: (provider) => setConnectingProvider(provider),
+ onMutate: (provider) => {
+ setConnectingProvider(provider)
+ analytics.connectionAuthStarted({ provider })
+ },
onError: (err) => {
setConnectingProvider(null)
toast.error("Failed to connect", {
@@ -1349,6 +1352,13 @@ export function IntegrationsView() {
return true
})
+ const trackCard = (item: Item) =>
+ analytics.integrationCardClicked({
+ kind: item.kind,
+ id: item.id,
+ name: item.name,
+ })
+
const renderRight = (item: Item): ReactNode => {
switch (item.kind) {
case "plugin": {
@@ -1370,7 +1380,10 @@ export function IntegrationsView() {
const busy = connectingPlugin === item.pluginId
return (
createPluginKeyMutation.mutate(item.pluginId)}
+ onClick={() => {
+ trackCard(item)
+ createPluginKeyMutation.mutate(item.pluginId)
+ }}
disabled={!!connectingPlugin}
>
{busy ? (
@@ -1397,7 +1410,10 @@ export function IntegrationsView() {
const busy = connectingProvider === item.provider
return (
addConnectionMutation.mutate(item.provider)}
+ onClick={() => {
+ trackCard(item)
+ addConnectionMutation.mutate(item.provider)
+ }}
disabled={!!connectingProvider}
>
{busy ? (
@@ -1415,6 +1431,7 @@ export function IntegrationsView() {
return (
{
+ trackCard(item)
window.open(
(item.action as { type: "external"; href: string }).href,
"_blank",
@@ -1431,12 +1448,13 @@ export function IntegrationsView() {
}
return (
+ onClick={() => {
+ trackCard(item)
setViewMode(
(item.action as { type: "view"; viewMode: ViewParamValue })
.viewMode,
)
- }
+ }}
>
Connect
@@ -1444,13 +1462,23 @@ export function IntegrationsView() {
}
case "mcp-client":
return (
- openMcpClient(item.clientKey)}>
+ {
+ trackCard(item)
+ openMcpClient(item.clientKey)
+ }}
+ >
Connect
)
case "import":
return (
- setViewMode(item.viewMode)}>
+ {
+ trackCard(item)
+ setViewMode(item.viewMode)
+ }}
+ >
Connect
)
diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx
index 155659b5..fa3d7dc9 100644
--- a/apps/web/components/settings/account.tsx
+++ b/apps/web/components/settings/account.tsx
@@ -4,15 +4,8 @@ import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
import { authClient } from "@lib/auth"
-import { useOrgSummaries } from "@/hooks/use-org-summaries"
-import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
-import {
- PLAN_RANK,
- useTokenUsage,
- type PlanType,
-} from "@/hooks/use-token-usage"
-import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
+import { Popover, PopoverContent } from "@ui/components/popover"
import {
Select,
SelectContent,
@@ -28,23 +21,21 @@ import {
} from "@ui/components/dropdown-menu"
import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery } from "@tanstack/react-query"
import {
- Check,
LoaderIcon,
ChevronDown,
- Building2,
Users,
UserPlus,
Mail,
MoreHorizontal,
UserMinus,
X,
+ Pencil,
Tag,
Plus,
} from "lucide-react"
-import { useMemo, useRef, useState } from "react"
+import { useEffect, useMemo, useRef, useState } from "react"
import { toast } from "sonner"
import { useContainerTags } from "@/hooks/use-container-tags"
import { PopoverAnchor } from "@ui/components/popover"
@@ -135,16 +126,7 @@ function isPendingInvitation(invitation: {
}
export default function Account() {
- const {
- user,
- org,
- organizations: allOrgs,
- setActiveOrg,
- refetchActiveOrg,
- } = useAuth()
- const autumn = useCustomer()
- const [switchingOrgId, setSwitchingOrgId] = useState(null)
- const [orgMenuOpen, setOrgMenuOpen] = useState(false)
+ const { user, org, refetchActiveOrg, refetchOrganizations } = useAuth()
const [inviteDialogOpen, setInviteDialogOpen] = useState(false)
const [inviteEmail, setInviteEmail] = useState("")
const [inviteRole, setInviteRole] = useState("member")
@@ -156,6 +138,8 @@ export default function Account() {
>([])
const [tagQuery, setTagQuery] = useState("")
const [tagDropdownOpen, setTagDropdownOpen] = useState(false)
+ const [isEditingOrgName, setIsEditingOrgName] = useState(false)
+ const [orgNameDraft, setOrgNameDraft] = useState("")
const tagInputRef = useRef(null)
const tagAnchorRef = useRef(null)
const { allProjects: allContainerTags } = useContainerTags()
@@ -174,28 +158,17 @@ export default function Account() {
const showAccessType = inviteRole === "member"
const showTagPicker =
inviteRole === "member" && inviteAccessType === "restricted"
- const canSwitchOrg = (allOrgs?.length ?? 0) > 1
- const { data: orgSummaries } = useOrgSummaries()
- const handleOrgSwitch = async (orgSlug: string, orgId: string) => {
- if (orgId === org?.id) return
- setSwitchingOrgId(orgId)
- try {
- await setActiveOrg(orgSlug)
- window.location.reload()
- } catch (error) {
- console.error("Failed to switch organization:", error)
- setSwitchingOrgId(null)
- }
- }
-
- const { currentPlan } = useTokenUsage(autumn)
+ useEffect(() => {
+ setOrgNameDraft(org?.name ?? "")
+ setIsEditingOrgName(false)
+ }, [org?.name])
const activeMemberRoleQuery = useQuery({
queryKey: ["organization", org?.id, "active-member-role"],
queryFn: async () => {
if (!org?.id) return null
- const result = await authClient.organization.getActiveMemberRole({
+ const result = await authClient.organization.getActiveMember({
query: { organizationId: org.id },
})
if (result.error) {
@@ -346,40 +319,50 @@ export default function Account() {
},
})
+ const updateOrgNameMutation = useMutation({
+ mutationFn: async (name: string) => {
+ if (!org?.id) throw new Error("No active organization")
+ const trimmed = name.trim()
+ if (!trimmed) throw new Error("Enter an organization name")
+ const result = await authClient.organization.update({
+ organizationId: org.id,
+ data: { name: trimmed },
+ })
+ if (result.error) {
+ throw new Error(
+ result.error.message ?? "Failed to update organization name",
+ )
+ }
+ return trimmed
+ },
+ onSuccess: async (name) => {
+ setOrgNameDraft(name)
+ setIsEditingOrgName(false)
+ await Promise.all([refetchActiveOrg(), refetchOrganizations()])
+ toast.success("Organization name updated")
+ },
+ onError: (error) => {
+ toast.error(getErrorMessage(error, "Failed to update organization name"))
+ },
+ })
+
const handleInviteSubmit = (event: React.FormEvent) => {
event.preventDefault()
if (!canManageTeam || inviteMemberMutation.isPending) return
inviteMemberMutation.mutate()
}
- const planByOrgId = useMemo(() => {
- const map = new Map()
- for (const summary of orgSummaries ?? []) {
- map.set(summary.orgId, summary.plan)
+ const handleOrgNameSubmit = (event: React.FormEvent) => {
+ event.preventDefault()
+ if (!canManageTeam || updateOrgNameMutation.isPending) return
+ const trimmed = orgNameDraft.trim()
+ if (!trimmed || trimmed === org?.name) {
+ setOrgNameDraft(org?.name ?? "")
+ setIsEditingOrgName(false)
+ return
}
- return map
- }, [orgSummaries])
-
- const sortedOrgsForMenu = useMemo(() => {
- if (!allOrgs?.length) return []
- return [...allOrgs].sort((a, b) => {
- const planA = resolveOrgPlan(
- a.id,
- a.id === org?.id,
- currentPlan,
- planByOrgId,
- )
- const planB = resolveOrgPlan(
- b.id,
- b.id === org?.id,
- currentPlan,
- planByOrgId,
- )
- const rankDiff = PLAN_RANK[planB] - PLAN_RANK[planA]
- if (rankDiff !== 0) return rankDiff
- return a.name.localeCompare(b.name)
- })
- }, [allOrgs, org?.id, currentPlan, planByOrgId])
+ updateOrgNameMutation.mutate(trimmed)
+ }
const memberSince = user?.createdAt
? new Date(user.createdAt).toLocaleDateString("en-US", {
@@ -438,84 +421,84 @@ export default function Account() {
>
Organization
- {canSwitchOrg ? (
-
-
+ setOrgNameDraft(event.target.value)}
+ disabled={updateOrgNameMutation.isPending}
+ maxLength={80}
className={cn(
- "flex min-w-0 max-w-full items-center gap-2 transition-opacity",
- "cursor-pointer hover:opacity-90",
dmSans125ClassName(),
+ "h-9 min-w-0 flex-1 rounded-[9px] border border-white/10 bg-black/30 px-3 text-[14px] font-medium tracking-[-0.14px] text-[#FAFAFA] outline-none transition-colors placeholder:text-[#525252] focus:border-[#4BA0FA]/60",
)}
- >
-
+
+
- {org?.name ?? "Personal"}
-
-
-
-
- {sortedOrgsForMenu.map((organization) => {
- const isCurrent = organization.id === org?.id
- const isSwitching = switchingOrgId === organization.id
- const plan = resolveOrgPlan(
- organization.id,
- isCurrent,
- currentPlan,
- planByOrgId,
- )
- return (
-
- handleOrgSwitch(
- organization.slug,
- organization.id,
- )
- }
- className={cn(
- "w-full flex items-center gap-3 px-3 py-2.5 rounded-[8px] text-left transition-colors",
- isCurrent
- ? "bg-white/5"
- : "hover:bg-white/5 cursor-pointer",
- "disabled:opacity-60 disabled:cursor-default",
- dmSans125ClassName(),
- )}
- >
-
-
- {organization.name}
-
- {isSwitching ? (
-
- ) : isCurrent ? (
-
- ) : (
-
- )}
-
-
- )
- })}
-
-
+ {updateOrgNameMutation.isPending ? (
+
+ ) : null}
+ Save
+
+
{
+ setOrgNameDraft(org?.name ?? "")
+ setIsEditingOrgName(false)
+ }}
+ className={cn(
+ "inline-flex size-8 items-center justify-center rounded-full bg-[#0D121A] text-[#737373] shadow-inside-out transition-colors hover:text-[#FAFAFA] disabled:cursor-not-allowed disabled:opacity-50",
+ )}
+ >
+
+
+
+
) : (
-
- {org?.name ?? "Personal"}
-
+
+
+ {org?.name ?? "Personal"}
+
+ {canManageTeam ? (
+
{
+ 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",
+ )}
+ >
+
+
+ ) : null}
+
)}
diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx
index 0a650a15..6fe59994 100644
--- a/apps/web/components/settings/billing.tsx
+++ b/apps/web/components/settings/billing.tsx
@@ -13,6 +13,9 @@ import {
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { useCustomer } from "autumn-js/react"
import {
+ Check,
+ ChevronLeft,
+ ChevronRight,
Coins,
ExternalLink,
LoaderIcon,
@@ -30,6 +33,11 @@ const API_BASE =
const CREDIT_FEATURE_ID = "usd_credits"
const TOP_UP_PLAN_ID = "credits_topup"
const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const
+const PLAN_CARD_ACTION_CLASS =
+ "inline-flex h-10 w-full items-center justify-center gap-2 rounded-[10px] text-[14px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-60"
+
+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"
type BillingInvoice = {
planIds?: string[]
@@ -61,6 +69,91 @@ type AutoTopupsResponse =
}
| { ok: false; reason: string; message?: string }
+type PlanCardDefinition = {
+ id: "free" | "pro" | "scale" | "enterprise"
+ name: string
+ price: string
+ period: string
+ credits: string
+ productId: "api_free" | "api_pro" | "api_scale" | "api_enterprise"
+ description: string
+ includesFrom?: string
+ features: string[]
+ isContactSales?: boolean
+}
+
+const PLAN_CARDS: PlanCardDefinition[] = [
+ {
+ id: "free",
+ name: "Free",
+ price: "$0",
+ period: "",
+ credits: "$5",
+ productId: "api_free",
+ description: "Try supermemory with no commitment",
+ features: [
+ "Pay-as-you-go after $5 runs out",
+ "Full search and memory access",
+ "Email support",
+ ],
+ },
+ {
+ id: "pro",
+ name: "Pro",
+ price: "$19",
+ period: "/mo",
+ credits: "$20",
+ productId: "api_pro",
+ description: "For people building with AI memory",
+ features: [
+ "Auto top-up when balance runs low",
+ "All plugins (Claude Code, Cursor, Hermes...)",
+ "Priority support",
+ ],
+ },
+]
+
+const ADVANCED_PLAN_CARDS: PlanCardDefinition[] = [
+ {
+ id: "scale",
+ name: "Scale",
+ price: "$399",
+ period: "/mo",
+ credits: "$600",
+ productId: "api_scale",
+ description: "For teams and production workloads",
+ includesFrom: "Pro",
+ features: [
+ "Auto top-up & spend caps",
+ "Gmail, S3 & Web Crawler connectors",
+ "Dedicated support",
+ ],
+ },
+ {
+ id: "enterprise",
+ name: "Enterprise",
+ price: "Custom",
+ period: "",
+ credits: "Unlimited",
+ productId: "api_enterprise",
+ description: "Custom deployments with dedicated engineering",
+ includesFrom: "Scale",
+ features: [
+ "Custom metering & billing",
+ "Custom integrations & SSO",
+ "Forward-deployed engineer",
+ ],
+ isContactSales: true,
+ },
+]
+
+const PLAN_RANK: Record
= {
+ free: 0,
+ pro: 1,
+ scale: 2,
+ enterprise: 3,
+}
+
function SectionTitle({
children,
aside,
@@ -103,6 +196,95 @@ function SettingsCard({
)
}
+function PlanCard({
+ action,
+ plan,
+}: {
+ action: React.ReactNode
+ plan: PlanCardDefinition
+}) {
+ return (
+
+
+ {plan.name}
+
+
+
+
+ {plan.price}
+
+ {plan.period ? (
+ {plan.period}
+ ) : null}
+
+
+
+ {plan.description}
+
+
+ {plan.isContactSales ? null : (
+
+
+
+
+ {plan.credits}
+
+
+ of usage included
+
+
+
+ )}
+
+ {plan.includesFrom ? (
+
+
+
+ Everything in {plan.includesFrom}, plus
+
+
+
+ ) : null}
+
+
+ {plan.features.map((feature) => (
+
+
+ {feature}
+
+ ))}
+
+
+ {action}
+
+ )
+}
+
function Pill({
children,
tone = "muted",
@@ -236,6 +418,8 @@ export default function Billing() {
const [isCancelling, setIsCancelling] = useState(false)
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
const [isCreditsDialogOpen, setIsCreditsDialogOpen] = useState(false)
+ const [isPlanCarouselActive, setIsPlanCarouselActive] = useState(false)
+ const [planPage, setPlanPage] = useState<0 | 1>(0)
const [topUpAmount, setTopUpAmount] = useState(25)
const [customTopUpAmount, setCustomTopUpAmount] = useState("")
const [topUpPendingAmount, setTopUpPendingAmount] = useState(
@@ -333,11 +517,11 @@ export default function Billing() {
const planDisplayNames = PLAN_DISPLAY_NAMES
- const handleUpgrade = async () => {
+ const handleUpgrade = async (planId: "api_pro" | "api_scale") => {
setIsUpgrading(true)
try {
const result = await autumn.attach({
- planId: "api_pro",
+ planId,
successUrl: `${window.location.origin}/settings#billing`,
})
if ((result as { paymentUrl?: string })?.paymentUrl) {
@@ -490,6 +674,97 @@ export default function Billing() {
})
}
+ const getPlanCardAction = (plan: PlanCardDefinition) => {
+ const disabled = isUpgrading || isCheckingStatus || autumn.isLoading
+ const isCurrentPlan = plan.id === currentPlan
+ const isIncludedPlan = PLAN_RANK[currentPlan] > PLAN_RANK[plan.id]
+
+ if (plan.id === "free") {
+ return (
+
+ {hasPaidPlan ? "Included with current plan" : "Your current plan"}
+
+ )
+ }
+
+ if (isCurrentPlan) {
+ return (
+
+ Your current plan
+
+ )
+ }
+
+ if (isIncludedPlan) {
+ return (
+
+ Included with {planDisplayNames[currentPlan]}
+
+ )
+ }
+
+ if (plan.isContactSales) {
+ return (
+
+ Contact sales
+
+ )
+ }
+
+ const checkoutPlanId =
+ plan.productId === "api_pro" || plan.productId === "api_scale"
+ ? plan.productId
+ : null
+ if (!checkoutPlanId) return null
+
+ return (
+ handleUpgrade(checkoutPlanId)}
+ disabled={disabled}
+ className={cn(
+ dmSans125ClassName(),
+ PLAN_CARD_ACTION_CLASS,
+ "bg-[#0054AD] text-[#FAFAFA] hover:bg-[#0B65C9]",
+ )}
+ >
+ {disabled ? : null}
+ Upgrade to {plan.name}
+
+ )
+ }
+
return (
@@ -668,348 +943,322 @@ export default function Billing() {
: "Usage resets with your billing cycle"}
-
- {!hasPaidPlan ? (
-
- {isUpgrading || isCheckingStatus || autumn.isLoading ? (
-
- ) : null}
- Upgrade to Pro - $19/month
-
- ) : null}
-
- Credits
-
-
-
-
-
- Usage this period
-
-
-
- {planUsagePct < 1 && planUsagePct > 0
- ? "< 1"
- : Math.round(planUsagePct)}
- %
-
- of monthly usage
-
- {daysRemaining !== null
- ? `· resets in ${daysRemaining} day${daysRemaining !== 1 ? "s" : ""}`
- : "· resets with your billing cycle"}
-
-
-
-
- {planUsagePct > 0
- ? `${formatUsd(usdSpent)} used this period`
- : "No usage yet this period"}
-
-
-
-
-
-
-
-
- Buy credits
-
-
-
+
+ setPlanPage(0)}
+ disabled={planPage === 0}
+ className="flex size-8 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-[#A3A3A3] transition-colors hover:bg-white/[0.05] hover:text-[#FAFAFA] disabled:cursor-not-allowed disabled:opacity-35"
+ aria-label="Show Free and Pro plans"
>
-
-
-
- Buy Credits
-
-
- Add USD to your balance for metered usage.
-
-
-
-
-
-
-
-
-
-
-
-
- Choose an amount
-
-
{
- setTopUpAmount(value)
- setCustomTopUpAmount("")
- }}
- disabled={topUpPendingAmount !== null}
- />
-
-
- Custom amount (USD)
-
-
- setCustomTopUpAmount(event.target.value)
- }
- placeholder="e.g. 75"
- type="number"
- value={customTopUpAmount}
- className="h-11 rounded-[10px] border border-white/10 bg-[#080B10] px-3 text-[14px] text-[#FAFAFA] outline-none placeholder:text-[#737373] focus:border-[#0054AD]"
- />
-
-
-
-
-
-
-
-
- Auto reload
-
-
- {autoTopUpEnabled ? "on" : "off"}
-
-
-
-
-
- Auto reload is{" "}
- {autoTopUpEnabled ? "enabled" : "disabled"}
-
-
- handleAutoReloadToggle(!autoTopUpEnabled)
- }
- className={cn(
- dmSans125ClassName(),
- "inline-flex h-9 min-w-[96px] items-center justify-center rounded-[9px] border border-white/10 bg-[#0D121A] px-3 text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-45",
- )}
- >
- {autoTopUpEnabled ? "Disable" : "Enable"}
-
-
-
- {!hasPaymentMethod && !activeAutoTopUp?.enabled ? (
-
- Save a card in Manage Billing to enable automatic
- reloads.
-
- ) : null}
-
-
-
-
- Threshold (USD)
-
- {
- const value = Number.parseFloat(
- event.target.value,
- )
- setAutoTopUpThreshold(
- Number.isFinite(value) ? value : 0,
- )
- }}
- type="number"
- value={
- Number.isFinite(autoTopUpThreshold)
- ? autoTopUpThreshold
- : ""
- }
- className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60"
- />
-
-
-
- Reload amount (USD)
-
- {
- const value = Number.parseFloat(
- event.target.value,
- )
- setAutoTopUpAmount(
- Number.isFinite(value) ? value : 0,
- )
- }}
- type="number"
- value={
- Number.isFinite(autoTopUpAmount)
- ? autoTopUpAmount
- : ""
- }
- className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60"
- />
-
-
- void handleSaveAutoTopUp()}
- disabled={isSavingAutoTopUp || !isAdmin}
- className={cn(
- dmSans125ClassName(),
- "inline-flex h-9 w-full items-center justify-center gap-2 rounded-[8px] border border-white/10 bg-[#080B10] text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-60",
- )}
- >
- {isSavingAutoTopUp ? (
-
- ) : null}
- Save threshold & reload amount
-
-
-
-
-
-
void handleTopUp(selectedTopUpAmount)}
- disabled={
- topUpPendingAmount !== null ||
- !isAdmin ||
- selectedTopUpAmount <= 0
- }
- className={cn(
- dmSans125ClassName(),
- "inline-flex h-11 w-full items-center justify-center gap-2 rounded-[10px] bg-[#0054AD] text-[14px] font-bold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9] disabled:cursor-not-allowed disabled:opacity-60",
- )}
- >
- {topUpPendingAmount !== null ? (
-
- ) : null}
- Buy {formatUsd(selectedTopUpAmount)} in credits
-
-
- {!isAdmin ? (
-
- Only owners/admins can purchase credits.
-
- ) : null}
-
-
-
-
setIsCreditsDialogOpen(true)}
- disabled={!isAdmin}
- className="inline-flex h-8 items-center justify-center gap-2 rounded-[8px] text-[12px] font-medium text-[#A3A3A3] transition-colors hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-50"
- >
-
+
+
setPlanPage(1)}
+ disabled={planPage === 1}
+ className="flex size-8 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-[#A3A3A3] transition-colors hover:bg-white/[0.05] hover:text-[#FAFAFA] disabled:cursor-not-allowed disabled:opacity-35"
+ aria-label="Show Scale and Enterprise plans"
+ >
+
+
+
+ ) : undefined
+ }
+ >
+ Plans
+
+
+
+
+ {PLAN_CARDS.map((plan) => (
+
- Auto reload: {autoTopUpEnabled ? "on" : "off"}
-
+ ))}
+
+
+ {ADVANCED_PLAN_CARDS.map((plan) => (
+
+ ))}
-
+
+ {isPlanCarouselActive ? null : (
+
+ {
+ setIsPlanCarouselActive(true)
+ setPlanPage(1)
+ }}
+ className={cn(
+ dmSans125ClassName(),
+ "inline-flex items-center justify-center gap-2 text-[13px] font-semibold text-[#A3A3A3] transition-colors hover:text-[#FAFAFA]",
+ )}
+ >
+
+ Other plans
+
+
+ →
+
+
+
+ )}
+
- {hasPaidPlan ? (
+
+
+
+
+
+ Buy Credits
+
+
+ Add USD to your balance for metered usage.
+
+
+
+
+
+
+
+
+
+
+
+
+ Choose an amount
+
+
{
+ setTopUpAmount(value)
+ setCustomTopUpAmount("")
+ }}
+ disabled={topUpPendingAmount !== null}
+ />
+
+
+ Custom amount (USD)
+
+ setCustomTopUpAmount(event.target.value)}
+ placeholder="e.g. 75"
+ type="number"
+ value={customTopUpAmount}
+ className="h-11 rounded-[10px] border border-white/10 bg-[#080B10] px-3 text-[14px] text-[#FAFAFA] outline-none placeholder:text-[#737373] focus:border-[#0054AD]"
+ />
+
+
+
+
+
+
+
+
+ Auto reload
+
+
+ {autoTopUpEnabled ? "on" : "off"}
+
+
+
+
+
+ Auto reload is {autoTopUpEnabled ? "enabled" : "disabled"}
+
+
handleAutoReloadToggle(!autoTopUpEnabled)}
+ className={cn(
+ dmSans125ClassName(),
+ "inline-flex h-9 min-w-[96px] items-center justify-center rounded-[9px] border border-white/10 bg-[#0D121A] px-3 text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-45",
+ )}
+ >
+ {autoTopUpEnabled ? "Disable" : "Enable"}
+
+
+
+ {!hasPaymentMethod && !activeAutoTopUp?.enabled ? (
+
+ Save a card in Manage Billing to enable automatic reloads.
+
+ ) : null}
+
+
+
+
+ Threshold (USD)
+
+ {
+ const value = Number.parseFloat(event.target.value)
+ setAutoTopUpThreshold(Number.isFinite(value) ? value : 0)
+ }}
+ type="number"
+ value={
+ Number.isFinite(autoTopUpThreshold)
+ ? autoTopUpThreshold
+ : ""
+ }
+ className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60"
+ />
+
+
+
+ Reload amount (USD)
+
+ {
+ const value = Number.parseFloat(event.target.value)
+ setAutoTopUpAmount(Number.isFinite(value) ? value : 0)
+ }}
+ type="number"
+ value={
+ Number.isFinite(autoTopUpAmount) ? autoTopUpAmount : ""
+ }
+ className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60"
+ />
+
+
+ void handleSaveAutoTopUp()}
+ disabled={isSavingAutoTopUp || !isAdmin}
+ className={cn(
+ dmSans125ClassName(),
+ "inline-flex h-9 w-full items-center justify-center gap-2 rounded-[8px] border border-white/10 bg-[#080B10] text-[13px] font-medium text-[#FAFAFA] transition-colors hover:bg-[#121A24] disabled:cursor-not-allowed disabled:opacity-60",
+ )}
+ >
+ {isSavingAutoTopUp ? (
+
+ ) : null}
+ Save threshold & reload amount
+
+
+
+
+
+
void handleTopUp(selectedTopUpAmount)}
+ disabled={
+ topUpPendingAmount !== null ||
+ !isAdmin ||
+ selectedTopUpAmount <= 0
+ }
+ className={cn(
+ dmSans125ClassName(),
+ "inline-flex h-11 w-full items-center justify-center gap-2 rounded-[10px] bg-[#0054AD] text-[14px] font-bold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9] disabled:cursor-not-allowed disabled:opacity-60",
+ )}
+ >
+ {topUpPendingAmount !== null ? (
+
+ ) : null}
+ Buy {formatUsd(selectedTopUpAmount)} in credits
+
+
+ {!isAdmin ? (
+
+ Only owners/admins can purchase credits.
+
+ ) : null}
+
+
+
+
+ {hasPaidPlan ? (
+
+ Credits
- Top-up credits{" "}
-
- (optional)
-
+ Top-up credits
- {creditRemaining > 0 ? "Add more" : "Add credits"}
+ {creditRemaining > 0 ? "Add more" : "Buy credits"}
- ) : null}
-
+
+ ) : null}
Invoice history
diff --git a/apps/web/components/settings/integrations.tsx b/apps/web/components/settings/integrations.tsx
index 125cec90..355a51e1 100644
--- a/apps/web/components/settings/integrations.tsx
+++ b/apps/web/components/settings/integrations.tsx
@@ -84,7 +84,7 @@ function PillButton({
className={cn(
"relative flex items-center justify-center gap-2",
"bg-[#0D121A]",
- "rounded-full h-11 px-4 flex-1",
+ "rounded-full h-11 min-w-0 px-3 flex-1 sm:px-4",
"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",
@@ -244,6 +244,15 @@ export default function Integrations() {
analytics.extensionInstallClicked()
}
+ const addShortcutLabel =
+ createApiKeyMutation.isPending && selectedShortcutType === "add"
+ ? "Creating..."
+ : "Add shortcut"
+ const searchShortcutLabel =
+ createApiKeyMutation.isPending && selectedShortcutType === "search"
+ ? "Creating..."
+ : "Search shortcut"
+
const handleDialogClose = (open: boolean) => {
setShowApiKeyModal(open)
if (!open) {
@@ -331,7 +340,7 @@ export default function Integrations() {
-
+
handleShortcutClick("add")}
disabled={createApiKeyMutation.isPending}
@@ -342,11 +351,14 @@ export default function Integrations() {
) : (
)}
-
- {createApiKeyMutation.isPending &&
- selectedShortcutType === "add"
- ? "Creating..."
- : "Add memory shortcut"}
+
+ {addShortcutLabel}
+
+ {createApiKeyMutation.isPending &&
+ selectedShortcutType === "add"
+ ? "Creating..."
+ : "Add memory shortcut"}
+
)}
-
- {createApiKeyMutation.isPending &&
- selectedShortcutType === "search"
- ? "Creating..."
- : "Search memory shortcut"}
+
+ {searchShortcutLabel}
+
+ {createApiKeyMutation.isPending &&
+ selectedShortcutType === "search"
+ ? "Creating..."
+ : "Search memory shortcut"}
+
@@ -397,7 +412,7 @@ export default function Integrations() {
-
+
)}
-
+
{createRaycastApiKeyMutation.isPending
? "Generating..."
: "Get API key"}
@@ -415,7 +430,7 @@ export default function Integrations() {
-
+
Install extension
diff --git a/apps/web/components/settings/org-context.tsx b/apps/web/components/settings/org-context.tsx
index dac97f7e..8c17ed01 100644
--- a/apps/web/components/settings/org-context.tsx
+++ b/apps/web/components/settings/org-context.tsx
@@ -94,7 +94,7 @@ function PillButton({
children: React.ReactNode
onClick: () => void
disabled?: boolean
- variant?: "default" | "danger" | "primary"
+ variant?: "default" | "ghost" | "primary"
}) {
return (
{children}
@@ -212,7 +208,7 @@ export function OrgContext() {
setConfirmDialog(enabled ? "disable" : "enable")}
disabled={!settingsReady || updateSettings.isPending}
- variant={enabled ? "danger" : "primary"}
+ variant={enabled ? "ghost" : "primary"}
>
{enabled ? "DISABLE" : "ENABLE"}
@@ -300,7 +296,9 @@ export function OrgContext() {
-
setConfirmDialog(null)}>
+ setConfirmDialog(null)}
+ variant="ghost"
+ >
CANCEL
{updateSettings.isPending && (
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
index d762654e..132ebeb3 100644
--- a/apps/web/lib/analytics.ts
+++ b/apps/web/lib/analytics.ts
@@ -1,5 +1,8 @@
import posthog from "posthog-js"
+export type OnboardingStep = "profile_input" | "processing" | "done" | "error"
+export type OnboardingSource = "x" | "linkedin" | "resume"
+
// Helper function to safely capture events
const safeCapture = (
eventName: string,
@@ -40,12 +43,13 @@ export const analytics = {
upgradeCompleted: () => safeCapture("upgrade_completed"),
billingPortalOpened: () => safeCapture("billing_portal_opened"),
- connectionAdded: (provider: string) =>
- safeCapture("connection_added", { provider }),
connectionDeleted: () => safeCapture("connection_deleted"),
- connectionAuthStarted: () => safeCapture("connection_auth_started"),
- connectionAuthCompleted: () => safeCapture("connection_auth_completed"),
- connectionAuthFailed: () => safeCapture("connection_auth_failed"),
+ connectionAuthStarted: (props: { provider: string }) =>
+ safeCapture("connection_auth_started", props),
+
+ // integrations surface (main Nova page)
+ integrationCardClicked: (props: { kind: string; id: string; name: string }) =>
+ safeCapture("integration_card_clicked", props),
nextAppResearchCtaDismissed: () =>
safeCapture("next_app_research_cta_dismissed"),
@@ -72,21 +76,13 @@ export const analytics = {
addDocumentModalOpened: () => safeCapture("add_document_modal_opened"),
// onboarding analytics
- onboardingStepViewed: (props: { step: string; trigger: "user" | "auto" }) =>
- safeCapture("onboarding_step_viewed", props),
+ onboardingStepViewed: (props: {
+ step: OnboardingStep
+ trigger: "user" | "auto"
+ }) => safeCapture("onboarding_step_viewed", props),
- onboardingNameSubmitted: (props: { name_length: number }) =>
- safeCapture("onboarding_name_submitted", props),
-
- onboardingProfileSubmitted: (props: {
- has_twitter: boolean
- has_linkedin: boolean
- other_links_count: number
- description_length: number
- }) => safeCapture("onboarding_profile_submitted", props),
-
- onboardingRelatableSelected: (props: { options: string[] }) =>
- safeCapture("onboarding_relatable_selected", props),
+ onboardingProfileSubmitted: (props: { source: OnboardingSource }) =>
+ safeCapture("onboarding_profile_submitted", props),
onboardingIntegrationClicked: (props: { integration: string }) =>
safeCapture("onboarding_integration_clicked", props),
@@ -100,7 +96,13 @@ export const analytics = {
onboardingXBookmarksDetailOpened: () =>
safeCapture("onboarding_x_bookmarks_detail_opened"),
- onboardingCompleted: () => safeCapture("onboarding_completed"),
+ onboardingSkipped: (props: { from_step: OnboardingStep }) =>
+ safeCapture("onboarding_skipped", props),
+
+ onboardingCompleted: (props?: {
+ source?: OnboardingSource
+ memories_count?: number
+ }) => safeCapture("onboarding_completed", props),
// main app analytics
searchOpened: (props: {
diff --git a/apps/web/lib/url-helpers.ts b/apps/web/lib/url-helpers.ts
index cfd57209..8fc7d29e 100644
--- a/apps/web/lib/url-helpers.ts
+++ b/apps/web/lib/url-helpers.ts
@@ -1,4 +1,22 @@
const PROXY_LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"])
+const DEV_APP_ORIGIN = "https://app.dev.supermemory.ai"
+const PROD_APP_ORIGIN = "https://app.supermemory.ai"
+
+export function getAppOriginForCurrentEnvironment(hostname?: string): string {
+ const currentHostname =
+ hostname ?? (typeof window !== "undefined" ? window.location.hostname : "")
+ const normalized = currentHostname.toLowerCase()
+ const isLocalOrDev =
+ process.env.NODE_ENV !== "production" ||
+ PROXY_LOCAL_HOSTS.has(normalized) ||
+ normalized.includes("app.dev.supermemory")
+
+ return isLocalOrDev ? DEV_APP_ORIGIN : PROD_APP_ORIGIN
+}
+
+export function getBillingSettingsUrl(hostname?: string): string {
+ return `${getAppOriginForCurrentEnvironment(hostname)}/settings#billing`
+}
/** Reconstruct the browser-facing URL when running behind portless (or similar). */
export function getPublicRequestUrl(request: Request): URL {
diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx
index 724efdd7..bd6b2f3a 100644
--- a/packages/memory-graph/src/components/legend.tsx
+++ b/packages/memory-graph/src/components/legend.tsx
@@ -6,6 +6,8 @@ interface LegendProps {
edges?: GraphEdge[]
isLoading?: boolean
colors: GraphThemeColors
+ compact?: boolean
+ maxHeight?: number
}
function HexagonIcon({
@@ -191,6 +193,8 @@ export const Legend = memo(function Legend({
edges = [],
isLoading: _isLoading = false,
colors,
+ compact = false,
+ maxHeight,
}: LegendProps) {
const [isExpanded, setIsExpanded] = useState(false)
const [connectionsExpanded, setConnectionsExpanded] = useState(true)
@@ -201,7 +205,8 @@ export const Legend = memo(function Legend({
const outerStyle: React.CSSProperties = {
overflow: "hidden",
- width: 214,
+ width: compact ? "min(214px, calc(100vw - 32px))" : 214,
+ maxWidth: "100%",
}
const cardStyle: React.CSSProperties = {
@@ -209,6 +214,7 @@ export const Legend = memo(function Legend({
backgroundColor: colors.controlBg,
border: `1px solid ${colors.controlBorder}`,
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)",
+ maxHeight,
}
const headerBtnStyle: React.CSSProperties = {
@@ -217,6 +223,7 @@ export const Legend = memo(function Legend({
alignItems: "center",
gap: 6,
width: "100%",
+ justifyContent: "flex-start",
cursor: "pointer",
outline: "none",
background: "none",
@@ -263,6 +270,21 @@ export const Legend = memo(function Legend({
gap: 8,
}
+ const expandedContentStyle: React.CSSProperties = {
+ marginTop: 16,
+ display: "flex",
+ flexDirection: "column",
+ gap: 16,
+ ...(compact
+ ? {
+ maxHeight: maxHeight ? Math.max(maxHeight - 56, 112) : 220,
+ overflowY: "auto",
+ overscrollBehavior: "contain",
+ paddingRight: 2,
+ }
+ : {}),
+ }
+
return (
@@ -281,14 +303,7 @@ export const Legend = memo(function Legend({
{isExpanded && (
-
+
{/* Statistics section */}
Statistics
diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx
index ed5ec094..dd82f490 100644
--- a/packages/memory-graph/src/components/memory-graph.tsx
+++ b/packages/memory-graph/src/components/memory-graph.tsx
@@ -83,6 +83,9 @@ export function MemoryGraph({
const graphFitHeight = isCompactViewport
? Math.max(containerSize.height - 170, 240)
: containerSize.height
+ const compactLegendMaxHeight = isCompactViewport
+ ? Math.max(containerSize.height - 104, 160)
+ : undefined
// Rebuild version chain index during render (not in an effect) so that
// the chain data is up-to-date when getChain() is called in useMemo below.
@@ -644,13 +647,14 @@ export function MemoryGraph({
const bottomLeftStackStyle: React.CSSProperties = {
position: "absolute",
- bottom: 16,
- left: 16,
+ bottom: isCompactViewport ? 12 : 16,
+ left: isCompactViewport ? 12 : 16,
zIndex: 20,
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 8,
+ maxWidth: isCompactViewport ? "calc(100% - 24px)" : undefined,
}
return (
@@ -722,7 +726,9 @@ export function MemoryGraph({