From e739b09519458a19cac07da685f184a2a60466c1 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 29 May 2026 01:58:39 +0530 Subject: [PATCH 1/9] implement update org name in settings (#1014) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 --- apps/web/components/settings/account.tsx | 253 +++++++++++------------ 1 file changed, 118 insertions(+), 135 deletions(-) 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", )} - > - +
+ - ) - })} - - + {updateOrgNameMutation.isPending ? ( + + ) : null} + Save + + +
+ ) : ( - - {org?.name ?? "Personal"} - +
+ + {org?.name ?? "Personal"} + + {canManageTeam ? ( + + ) : null} +
)}
From 3b24bf2fb2cd2e06741b0b8ebf01f4a478fcfb27 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Fri, 29 May 2026 08:17:17 -0700 Subject: [PATCH 2/9] fix(web): make Nova chat mobile-native instead of full-screen takeover (#1021) --- apps/web/app/(app)/page.tsx | 2 +- apps/web/app/layout.tsx | 1 + apps/web/components/chat/index.tsx | 54 +++++++++--------------------- 3 files changed, 18 insertions(+), 39 deletions(-) diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index c177ec1e..120bd2b9 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -562,7 +562,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/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 c8ebb1e1..af21ddfa 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, @@ -97,7 +96,7 @@ export function ChatLaunchFab({ export function ChatSidebar({ isChatOpen, - setIsChatOpen, + setIsChatOpen: _setIsChatOpen, queuedMessage, queuedHighlightContent, onConsumeQueuedMessage, @@ -781,8 +780,7 @@ export function ChatSidebar({ const isStackedInput = layout === "page" const showHeaderRow = !isPageDesktop || isMobile || !isStackedInput const isResponding = status === "submitted" || status === "streaming" - const showInputStatusStrip = - !isStackedInput || isResponding || messages.length > 0 + const showInputStatusStrip = !isStackedInput const chatHistorySheet = (
- {layout === "page" && isMobile && ( - - )} {!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", ) : "" } @@ -1142,7 +1128,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"), )} > From 6fb42ad0e0997761ebf3fa8e9d16c21034e5457e Mon Sep 17 00:00:00 2001 From: Vedant Mahajan Date: Fri, 29 May 2026 23:46:27 +0530 Subject: [PATCH 3/9] Remove redundant credits section (#1019) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 --- apps/web/components/settings/billing.tsx | 526 ++++++++----------- apps/web/components/settings/org-context.tsx | 27 +- 2 files changed, 236 insertions(+), 317 deletions(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 0a650a15..b03a6659 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -31,6 +31,9 @@ const CREDIT_FEATURE_ID = "usd_credits" const TOP_UP_PLAN_ID = "credits_topup" const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const +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[] stripeId: string @@ -694,322 +697,237 @@ export default function Billing() { -
- Credits - - -
-
-

- Usage this period +

+ +
+
+

+ Buy Credits

-
- - {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"} +

+ Add USD to your balance for metered usage.

- -
- - - - - -
-
-

- Buy Credits -

-

- Add USD to your balance for metered usage. -

-
- - - -
- -
-
-

- Choose an amount -

- { - setTopUpAmount(value) - setCustomTopUpAmount("") - }} - disabled={topUpPendingAmount !== null} - /> -
- - - 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"} -

- -
- - {!hasPaymentMethod && !activeAutoTopUp?.enabled ? ( -

- Save a card in Manage Billing to enable automatic - reloads. -

- ) : null} - -
-
- - { - 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" - /> -
-
- - { - 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" - /> -
-
- -
-
-
- - - - {!isAdmin ? ( -

- Only owners/admins can purchase credits. -

- ) : null} -
- -
+ -
+
- - {hasPaidPlan ? ( +
+
+

+ Choose an amount +

+ { + setTopUpAmount(value) + setCustomTopUpAmount("") + }} + disabled={topUpPendingAmount !== null} + /> +
+ + 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"} +

+ +
+ + {!hasPaymentMethod && !activeAutoTopUp?.enabled ? ( +

+ Save a card in Manage Billing to enable automatic reloads. +

+ ) : null} + +
+
+ + { + 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" + /> +
+
+ + { + 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" + /> +
+
+ +
+
+
+ + + + {!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/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 (
- CANCEL + + CANCEL +
- setConfirmDialog(null)}> + setConfirmDialog(null)} + variant="ghost" + > CANCEL {updateSettings.isPending && ( From fc08bd891fe5d998e29560a3218a623584c94781 Mon Sep 17 00:00:00 2001 From: Vedant Mahajan Date: Fri, 29 May 2026 23:47:19 +0530 Subject: [PATCH 4/9] Add memory drawer (#1015) Co-authored-by: Ishaan Gupta Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 --- .../document-modal/graph-list-memories.tsx | 7 +- apps/web/components/document-modal/index.tsx | 279 ++++++++++++------ .../memory-graph/src/components/legend.tsx | 33 ++- .../src/components/memory-graph.tsx | 10 +- 4 files changed, 226 insertions(+), 103 deletions(-) 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 -
-
- - </div> - <div className="flex items-center gap-1.5 md:gap-2 shrink-0"> - {pluginDocument?.kind === "claude-code-doc" && - _document?.customId && ( - <CopySessionIdButton sessionId={_document.customId} /> - )} - <DeleteButton - documentId={_document?.id} - customId={_document?.customId} - deleteMutation={deleteMutation} - /> - {_document?.url && ( - <a - href={getDocumentSourceUrl(_document)} - target="_blank" - rel="noopener noreferrer" - className={cn( - "flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]", - isMobile ? "size-7 justify-center" : "px-3 py-2", - )} - > - {!isMobile && ( - <span className="line-clamp-1">Visit source</span> - )} - <ArrowUpRightIcon className="size-4 text-[#737373]" /> - </a> + )} + <div className="flex items-center justify-between h-fit gap-2 md:gap-4"> + <div className="flex-1 min-w-0"> + <Title + title={_document?.title} + documentType={_document?.type ?? "text"} + url={_document?.url} + pluginIconSrc={pluginDocument?.pluginIconSrc} + /> + </div> + <div className="flex items-center gap-1.5 md:gap-2 shrink-0"> + {pluginDocument?.kind === "claude-code-doc" && + _document?.customId && ( + <CopySessionIdButton sessionId={_document.customId} /> )} + <DeleteButton + documentId={_document?.id} + customId={_document?.customId} + deleteMutation={deleteMutation} + /> + {_document?.url && ( + <a + href={getDocumentSourceUrl(_document)} + target="_blank" + rel="noopener noreferrer" + className={cn( + "flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]", + isMobile ? "size-7 justify-center" : "px-3 py-2", + )} + > + {!isMobile && <span className="line-clamp-1">Visit source</span>} + <ArrowUpRightIcon className="size-4 text-[#737373]" /> + </a> + )} + {isMobile ? ( + <button + className="bg-[#0D121A] size-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus:outline-none disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]" + type="button" + tabIndex={-1} + onClick={onClose} + > + <XIcon stroke="#737373" /> + <span className="sr-only">Close</span> + </button> + ) : ( <DialogPrimitive.Close className="bg-[#0D121A] size-7 flex items-center justify-center rounded-full transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus:outline-none disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]" data-slot="dialog-close" @@ -331,50 +387,91 @@ export function DocumentModal({ <XIcon stroke="#737373" /> <span className="sr-only">Close</span> </DialogPrimitive.Close> - </div> + )} </div> - <div className="flex-1 grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-3 overflow-hidden min-h-0"> - <div - id="document-preview" - className={cn( - "bg-[#14161A] rounded-[14px] overflow-hidden flex flex-col shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)] relative", - )} + </div> + {isMobile && hasDocumentInsights ? ( + <Tabs + defaultValue="content" + className="flex min-h-0 flex-1 flex-col pt-1.5" + > + <TabsList className="grid h-11 w-full grid-cols-2 rounded-full border border-[#263142] bg-[#0A1019] p-1 shadow-[inset_0_1px_2px_rgba(255,255,255,0.04),0_1px_3px_rgba(0,0,0,0.35)]"> + <TabsTrigger + value="content" + className="rounded-full text-[15px] font-medium text-[#8E99AA] transition-colors data-[state=active]:bg-[#0B2B60]! data-[state=active]:text-[#F8FAFC] data-[state=active]:shadow-[inset_0_1px_1px_rgba(255,255,255,0.08),0_1px_4px_rgba(54,155,253,0.18)]" + > + Content + </TabsTrigger> + <TabsTrigger + value="insights" + className="rounded-full text-[15px] font-medium text-[#8E99AA] transition-colors data-[state=active]:bg-[#0B2B60]! data-[state=active]:text-[#F8FAFC] data-[state=active]:shadow-[inset_0_1px_1px_rgba(255,255,255,0.08),0_1px_4px_rgba(54,155,253,0.18)]" + > + Insights + </TabsTrigger> + </TabsList> + <TabsContent value="content" className="mt-4 flex min-h-0 flex-1"> + {documentPreview} + </TabsContent> + <TabsContent + value="insights" + className="mt-4 flex min-h-0 flex-1 flex-col overflow-y-auto pb-1 scrollbar-thin" > - <DocumentContent - document={_document} - textEditorProps={textEditorProps} - pluginDocument={pluginDocument} - /> - </div> - <div - id="document-memories-summary" - className={cn( - "gap-3 flex flex-col overflow-hidden", - dmSansClassName(), - )} - > - {pluginDocument && - pluginDocument.kind !== "claude-code-doc" && - pluginDocument.kind !== "openclaw-session" && ( - <PluginDetails parsed={pluginDocument} /> - )} - {_document && (_document.summary || pluginDocument?.summary) && ( - <DocumentSummary - memoryEntries={_document.memoryEntries} - summary={ - (pluginDocument?.summary ?? _document.summary) as string - } - createdAt={_document.createdAt} - /> - )} - {_document?.memoryEntries && _document.memoryEntries.length > 0 && ( - <GraphListMemories - memoryEntries={_document.memoryEntries as MemoryEntry[]} - documentId={_document.id} - /> - )} - </div> + {documentInsights} + </TabsContent> + </Tabs> + ) : isMobile ? ( + <div className="flex min-h-0 flex-1 pt-1.5">{documentPreview}</div> + ) : ( + <div className="flex-1 grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-3 min-h-0 overflow-hidden"> + {documentPreview} + {documentInsights} </div> + )} + </> + ) + + if (isMobile) { + return ( + <Drawer + open={isOpen} + onOpenChange={(open: boolean) => !open && onClose()} + shouldScaleBackground + > + <DrawerContent + className={cn( + "flex flex-col gap-0 border-none bg-[#1B1F24] p-0", + "h-[88svh] max-h-[88svh] overflow-hidden rounded-t-[22px]", + "[&>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", + }} + > + <div className="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden px-3 pt-2 pb-4"> + {modalContent} + </div> + </DrawerContent> + </Drawer> + ) + } + + return ( + <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> + <DialogContent + className={cn( + "p-0 border-none bg-[#1B1F24] flex flex-col px-3 md:px-4 pt-3 pb-4 gap-3", + "w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! rounded-[22px]", + dmSansClassName(), + )} + style={{ + boxShadow: + "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset", + }} + showCloseButton={false} + > + {modalContent} </DialogContent> </Dialog> ) 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 ( <div style={outerStyle}> <div style={cardStyle}> @@ -281,14 +303,7 @@ export const Legend = memo(function Legend({ </button> {isExpanded && ( - <div - style={{ - marginTop: 16, - display: "flex", - flexDirection: "column", - gap: 16, - }} - > + <div style={expandedContentStyle}> {/* Statistics section */} <div style={{ display: "flex", flexDirection: "column", gap: 8 }}> <span style={sectionLabelStyle}>Statistics</span> 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({ <Legend colors={colors} edges={edges} + compact={isCompactViewport} isLoading={isLoading} + maxHeight={compactLegendMaxHeight} nodes={nodes} /> </div> From ab068e2ba57867733702cd66086ce52f1563125b Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 29 May 2026 19:54:13 +0000 Subject: [PATCH 5/9] fix(web): restore Nova onboarding analytics events (#1023) The onboarding rework moved the flow to app/(app)/onboarding and dropped the funnel tracking, so onboarding_completed stopped firing on 2026-05-02. - Wire onboarding_step_viewed across idle/processing/done/error transitions - Fire onboarding_completed on real completion (status=done) with source + memories_count - onboarding_profile_submitted now carries { source } - Add onboarding_skipped { from_step } - Remove dead onboardingCompleted() from InitialHeader and unused name/relatable helpers --- apps/web/app/(app)/onboarding/page.tsx | 29 ++++++++++++++-- apps/web/components/initial-header.tsx | 2 -- apps/web/components/integrations-view.tsx | 42 +++++++++++++++++++---- apps/web/lib/analytics.ts | 42 ++++++++++++----------- 4 files changed, 84 insertions(+), 31 deletions(-) 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<Status, OnboardingStep> = { + 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<HTMLInputElement>(null) const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null) const skippingRef = useRef(false) + const completedTrackedRef = useRef(false) const [isSkipping, setIsSkipping] = useState(false) const [spotlightCategory, setSpotlightCategory] = useState<SpotlightCategoryId>("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/components/initial-header.tsx b/apps/web/components/initial-header.tsx index 831fb77c..95bc9628 100644 --- a/apps/web/components/initial-header.tsx +++ b/apps/web/components/initial-header.tsx @@ -4,7 +4,6 @@ import { Logo } from "@ui/assets/Logo" import { Button } from "@ui/components/button" import { useRouter } from "next/navigation" import { useOrgOnboarding } from "@hooks/use-org-onboarding" -import { analytics } from "@/lib/analytics" import { consumePendingConnectUrl } from "@/lib/constants" import { cn } from "@lib/utils" @@ -23,7 +22,6 @@ export function InitialHeader({ const handleSkip = () => { 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 ( <PillButton - onClick={() => 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 ( <PillButton - onClick={() => addConnectionMutation.mutate(item.provider)} + onClick={() => { + trackCard(item) + addConnectionMutation.mutate(item.provider) + }} disabled={!!connectingProvider} > {busy ? ( @@ -1415,6 +1431,7 @@ export function IntegrationsView() { return ( <PillButton onClick={() => { + trackCard(item) window.open( (item.action as { type: "external"; href: string }).href, "_blank", @@ -1431,12 +1448,13 @@ export function IntegrationsView() { } return ( <PillButton - onClick={() => + onClick={() => { + trackCard(item) setViewMode( (item.action as { type: "view"; viewMode: ViewParamValue }) .viewMode, ) - } + }} > Connect </PillButton> @@ -1444,13 +1462,23 @@ export function IntegrationsView() { } case "mcp-client": return ( - <PillButton onClick={() => openMcpClient(item.clientKey)}> + <PillButton + onClick={() => { + trackCard(item) + openMcpClient(item.clientKey) + }} + > Connect </PillButton> ) case "import": return ( - <PillButton onClick={() => setViewMode(item.viewMode)}> + <PillButton + onClick={() => { + trackCard(item) + setViewMode(item.viewMode) + }} + > Connect </PillButton> ) 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: { From 3160358bfdf1a69b2664bdf42797bddb2b2d809f Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 30 May 2026 16:38:10 +0000 Subject: [PATCH 6/9] fix(extension): resolve sign-in loop and stale spaces (#1016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Fixes two bugs reported by a customer (Plain T-1289) using the Supermemory browser extension. ### Bug 1 — extension sign-in loop When a user logs out of the _extension_ but is still authenticated in the _web app_, the extension's Sign-in button opens `/login`, which short-circuited via a bare `router.replace("/")`. That path never carried the `extension-auth-success` flag the dashboard waits on before `postMessage`\-ing the session token to the extension content script — so the extension never received a token and stayed stuck on "Sign in" indefinitely. Now the already-authenticated redirect carries `?extension-auth-success=true`, matching the existing fresh-login callback behavior (no new token exposure). ### Bug 2 — stale / deleted spaces The extension's stored default space (`local:sm-default-project`) was only set when none existed, never reconciled. After renaming/deleting spaces in the web app, the popup kept showing save buttons pointing at dead spaces. Now the popup reconciles the stored default against the freshly-fetched live list: resets to the first space if the stored one was deleted, and refreshes the cached copy (label/containerTag) if it was renamed. --- .../browser-extension/entrypoints/popup/App.tsx | 17 ++++++++++++++--- apps/browser-extension/wxt.config.ts | 2 +- apps/web/app/(auth)/login/page.tsx | 13 +++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) 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/(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") From 7c2de3c9e5d242094ac841447bbf1ac26a22f806 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta <ishaankone@gmail.com> Date: Sun, 31 May 2026 03:17:44 +0530 Subject: [PATCH 7/9] add web upgrade plans UI (#1022) --- apps/web/components/header.tsx | 32 ++ apps/web/components/settings/billing.tsx | 379 +++++++++++++++++++++-- apps/web/lib/url-helpers.ts | 18 ++ 3 files changed, 405 insertions(+), 24 deletions(-) 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%)", }} > + <DropdownMenuItem + asChild + 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" + > + <a href={billingSettingsUrl}> + <Logo className="h-4 w-5 shrink-0" /> + Upgrade + </a> + </DropdownMenuItem> <DropdownMenuItem onClick={onAddMemory} 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" @@ -421,6 +432,27 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) { </> ) : ( <> + <Tooltip> + <TooltipTrigger asChild> + <Button + asChild + className={cn( + "rounded-full! h-9! min-h-9 shrink-0 border border-[#2261CA33] bg-[#00173C] !text-white hover:bg-[#001F50]", + "max-lg:w-9 max-lg:min-w-9 max-lg:justify-center max-lg:gap-0 max-lg:px-0", + "lg:min-w-0 lg:gap-1.5 lg:px-3 lg:font-semibold", + dmSansClassName(), + )} + > + <a href={billingSettingsUrl} aria-label="Upgrade"> + <Logo className="h-3.5 w-[17px] shrink-0 lg:h-4 lg:w-5" /> + <span className="max-lg:sr-only">Upgrade</span> + </a> + </Button> + </TooltipTrigger> + <TooltipContent side="bottom" className={dmSansClassName()}> + Upgrade + </TooltipContent> + </Tooltip> <Tooltip> <TooltipTrigger asChild> <Button diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index b03a6659..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,8 @@ 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" @@ -64,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<PlanCardDefinition["id"], number> = { + free: 0, + pro: 1, + scale: 2, + enterprise: 3, +} + function SectionTitle({ children, aside, @@ -106,6 +196,95 @@ function SettingsCard({ ) } +function PlanCard({ + action, + plan, +}: { + action: React.ReactNode + plan: PlanCardDefinition +}) { + return ( + <div + className={cn( + "relative flex min-h-[416px] flex-col overflow-hidden rounded-[14px] border p-5", + "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", + "border-white/[0.08] bg-[#14161A]", + )} + > + <p className="font-mono text-[10px] font-medium uppercase tracking-[0.18em] text-[#737373]"> + {plan.name} + </p> + + <div className="mt-3 flex items-baseline gap-1"> + <span + className={cn( + dmSans125ClassName(), + "text-[34px] font-bold leading-none tracking-[-0.34px] text-[#FAFAFA] tabular-nums", + )} + > + {plan.price} + </span> + {plan.period ? ( + <span className="text-[13px] text-[#737373]">{plan.period}</span> + ) : null} + </div> + + <p + className={cn( + dmSans125ClassName(), + "mt-2 text-[13px] leading-snug text-[#A3A3A3]", + )} + > + {plan.description} + </p> + + {plan.isContactSales ? null : ( + <div className="mt-5 flex items-center gap-2 rounded-[8px] bg-white/[0.04] px-3 py-2.5 text-[#A3A3A3]"> + <Coins className="size-3.5 shrink-0 text-[#737373]" /> + <div className="min-w-0"> + <p className="text-[12px] font-semibold leading-none text-[#C8D0DA] tabular-nums"> + {plan.credits} + </p> + <p className="mt-0.5 text-[10px] leading-none text-[#737373]"> + of usage included + </p> + </div> + </div> + )} + + {plan.includesFrom ? ( + <div className="mt-5 flex items-center gap-3"> + <div className="h-px flex-1 bg-white/[0.08]" /> + <span className="whitespace-nowrap text-[10px] text-[#737373]"> + Everything in {plan.includesFrom}, plus + </span> + <div className="h-px flex-1 bg-white/[0.08]" /> + </div> + ) : null} + + <ul + className={cn( + "mb-6 flex flex-1 flex-col gap-3", + plan.includesFrom ? "mt-5" : "mt-5", + plan.isContactSales && !plan.includesFrom && "mt-7", + )} + > + {plan.features.map((feature) => ( + <li + className="flex items-start gap-2 text-[13px] leading-snug text-[#C8D0DA]" + key={feature} + > + <Check className="mt-0.5 size-3.5 shrink-0 text-[#737373]" /> + <span>{feature}</span> + </li> + ))} + </ul> + + {action} + </div> + ) +} + function Pill({ children, tone = "muted", @@ -239,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<number>(25) const [customTopUpAmount, setCustomTopUpAmount] = useState("") const [topUpPendingAmount, setTopUpPendingAmount] = useState<number | null>( @@ -336,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) { @@ -493,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 ( + <button + type="button" + disabled + className={cn( + dmSans125ClassName(), + PLAN_CARD_ACTION_CLASS, + "border border-white/[0.04] bg-white/[0.02] text-[#737373]", + )} + > + {hasPaidPlan ? "Included with current plan" : "Your current plan"} + </button> + ) + } + + if (isCurrentPlan) { + return ( + <button + type="button" + disabled + className={cn( + dmSans125ClassName(), + PLAN_CARD_ACTION_CLASS, + "border border-white/[0.04] bg-white/[0.02] text-[#737373]", + )} + > + Your current plan + </button> + ) + } + + if (isIncludedPlan) { + return ( + <button + type="button" + disabled + className={cn( + dmSans125ClassName(), + PLAN_CARD_ACTION_CLASS, + "border border-white/[0.04] bg-white/[0.02] text-[#737373]", + )} + > + Included with {planDisplayNames[currentPlan]} + </button> + ) + } + + if (plan.isContactSales) { + return ( + <a + href="mailto:support@supermemory.com?subject=Enterprise%20plan" + className={cn( + dmSans125ClassName(), + PLAN_CARD_ACTION_CLASS, + "border border-white/[0.08] bg-transparent text-[#FAFAFA] hover:bg-white/[0.04]", + )} + > + Contact sales + </a> + ) + } + + const checkoutPlanId = + plan.productId === "api_pro" || plan.productId === "api_scale" + ? plan.productId + : null + if (!checkoutPlanId) return null + + return ( + <button + type="button" + onClick={() => handleUpgrade(checkoutPlanId)} + disabled={disabled} + className={cn( + dmSans125ClassName(), + PLAN_CARD_ACTION_CLASS, + "bg-[#0054AD] text-[#FAFAFA] hover:bg-[#0B65C9]", + )} + > + {disabled ? <LoaderIcon className="size-4 animate-spin" /> : null} + Upgrade to {plan.name} + </button> + ) + } + return ( <div className="flex w-full flex-col gap-7"> <section id="billing-subscription" className="flex flex-col gap-4"> @@ -671,32 +943,91 @@ export default function Billing() { : "Usage resets with your billing cycle"} </p> </div> - - {!hasPaidPlan ? ( - <button - type="button" - onClick={handleUpgrade} - disabled={isUpgrading || isCheckingStatus || autumn.isLoading} - title={ - autumn.isLoading && !isUpgrading && !isCheckingStatus - ? "Loading billing details…" - : undefined - } - className={cn( - dmSans125ClassName(), - "inline-flex h-10 w-full items-center justify-center gap-2 rounded-[10px] bg-[#0054AD] text-[14px] font-semibold text-[#FAFAFA] transition-colors hover:bg-[#0B65C9] disabled:cursor-not-allowed disabled:opacity-60", - )} - > - {isUpgrading || isCheckingStatus || autumn.isLoading ? ( - <LoaderIcon className="size-4 animate-spin" /> - ) : null} - Upgrade to Pro - $19/month - </button> - ) : null} </div> </SettingsCard> </section> + <section id="billing-plans" className="flex flex-col gap-4"> + <SectionTitle + aside={ + isPlanCarouselActive ? ( + <div className="flex items-center gap-1.5"> + <button + type="button" + onClick={() => 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" + > + <ChevronLeft className="size-4" /> + </button> + <button + type="button" + onClick={() => 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" + > + <ChevronRight className="size-4" /> + </button> + </div> + ) : undefined + } + > + Plans + </SectionTitle> + <div className="overflow-hidden"> + <div + className="flex gap-4 transition-transform duration-300 ease-out" + style={{ + transform: + planPage === 1 ? "translateX(calc(-100% - 1rem))" : "none", + }} + > + <div className="grid w-full shrink-0 gap-4 md:grid-cols-2"> + {PLAN_CARDS.map((plan) => ( + <PlanCard + action={getPlanCardAction(plan)} + key={plan.id} + plan={plan} + /> + ))} + </div> + <div className="grid w-full shrink-0 gap-4 md:grid-cols-2"> + {ADVANCED_PLAN_CARDS.map((plan) => ( + <PlanCard + action={getPlanCardAction(plan)} + key={plan.id} + plan={plan} + /> + ))} + </div> + </div> + </div> + {isPlanCarouselActive ? null : ( + <div className="flex justify-end px-2 pt-1"> + <button + type="button" + onClick={() => { + 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]", + )} + > + <span className="relative after:absolute after:right-0 after:-bottom-0.5 after:left-0 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-200 hover:after:scale-x-100"> + Other plans + </span> + <span className="translate-x-1 text-[15px]" aria-hidden="true"> + → + </span> + </button> + </div> + )} + </section> + <Dialog open={isCreditsDialogOpen} onOpenChange={setIsCreditsDialogOpen}> <DialogContent showCloseButton={false} 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 { From c651fb41b10d5074c62180315b0cbbe95f2cfedd Mon Sep 17 00:00:00 2001 From: Ishaan Gupta <ishaankone@gmail.com> Date: Sun, 31 May 2026 03:26:51 +0530 Subject: [PATCH 8/9] fix settings/integrations responsiveness (#1003) --- apps/web/components/settings/integrations.tsx | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) 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() { </div> </div> - <div id="apple-shortcuts-cta" className="flex gap-4"> + <div id="apple-shortcuts-cta" className="flex gap-2 sm:gap-4"> <PillButton onClick={() => handleShortcutClick("add")} disabled={createApiKeyMutation.isPending} @@ -342,11 +351,14 @@ export default function Integrations() { ) : ( <Plus className="size-4 text-[#FAFAFA]" /> )} - <span className="text-[14px] tracking-[-0.14px] text-[#FAFAFA] font-medium"> - {createApiKeyMutation.isPending && - selectedShortcutType === "add" - ? "Creating..." - : "Add memory shortcut"} + <span className="whitespace-nowrap text-[12px] font-medium tracking-[-0.12px] text-[#FAFAFA] sm:text-[14px] sm:tracking-[-0.14px]"> + <span className="sm:hidden">{addShortcutLabel}</span> + <span className="hidden sm:inline"> + {createApiKeyMutation.isPending && + selectedShortcutType === "add" + ? "Creating..." + : "Add memory shortcut"} + </span> </span> </PillButton> <PillButton @@ -359,11 +371,14 @@ export default function Integrations() { ) : ( <Search className="size-4 text-[#FAFAFA]" /> )} - <span className="text-[14px] tracking-[-0.14px] text-[#FAFAFA] font-medium"> - {createApiKeyMutation.isPending && - selectedShortcutType === "search" - ? "Creating..." - : "Search memory shortcut"} + <span className="whitespace-nowrap text-[12px] font-medium tracking-[-0.12px] text-[#FAFAFA] sm:text-[14px] sm:tracking-[-0.14px]"> + <span className="sm:hidden">{searchShortcutLabel}</span> + <span className="hidden sm:inline"> + {createApiKeyMutation.isPending && + selectedShortcutType === "search" + ? "Creating..." + : "Search memory shortcut"} + </span> </span> </PillButton> </div> @@ -397,7 +412,7 @@ export default function Integrations() { </div> </div> - <div id="raycast-extension-cta" className="flex gap-4"> + <div id="raycast-extension-cta" className="flex gap-2 sm:gap-4"> <PillButton onClick={handleRaycastClick} disabled={createRaycastApiKeyMutation.isPending} @@ -407,7 +422,7 @@ export default function Integrations() { ) : ( <Key className="size-4 text-[#FAFAFA]" /> )} - <span className="text-[14px] tracking-[-0.14px] text-[#FAFAFA] font-medium"> + <span className="whitespace-nowrap text-[12px] font-medium tracking-[-0.12px] text-[#FAFAFA] sm:text-[14px] sm:tracking-[-0.14px]"> {createRaycastApiKeyMutation.isPending ? "Generating..." : "Get API key"} @@ -415,7 +430,7 @@ export default function Integrations() { </PillButton> <PillButton onClick={handleRaycastInstall}> <Download className="size-4 text-[#FAFAFA]" /> - <span className="text-[14px] tracking-[-0.14px] text-[#FAFAFA] font-medium"> + <span className="whitespace-nowrap text-[12px] font-medium tracking-[-0.12px] text-[#FAFAFA] sm:text-[14px] sm:tracking-[-0.14px]"> Install extension </span> </PillButton> From 4eb8399358ce0383214862b705307acc7cb06889 Mon Sep 17 00:00:00 2001 From: Dhravya Shah <dhravyashah@gmail.com> Date: Sat, 30 May 2026 16:35:37 -0700 Subject: [PATCH 9/9] Enhance README with additional context on usage Expanded description to include personal/company use. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a134afad..7760c85b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ </p> <p align="center"> - <strong>State-of-the-art memory and context engine for AI.</strong> + <strong>State-of-the-art memory and context engine for AI. And yes - you can use it as a company/personal brain.</strong> </p> <p align="center">