Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/web/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"use client"

import { EnsureWorkspace } from "@/components/ensure-workspace"
import { NextAppResearchCta } from "@/components/next-app-research-cta"
import { PWAInstallPrompt } from "@/components/pwa-install-prompt"

export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<>
<EnsureWorkspace>{children}</EnsureWorkspace>
<NextAppResearchCta />
<PWAInstallPrompt />
</>
)
Expand Down
1 change: 1 addition & 0 deletions apps/web/components/chat/home-chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function HomeChatComposer({
onValueChange={setChatSpaceProjects}
variant="insideOut"
includeAuto
hideCount
triggerClassName="h-auto min-h-0 max-w-[min(160px,35vw)] rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5 shadow-none hover:bg-[#05080D]"
/>
</>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ export function ChatSidebar({
onValueChange={setChatSpaceProjects}
variant="insideOut"
includeAuto
hideCount
triggerClassName="h-10 min-h-10 max-w-[min(192px,42vw)] border border-[#73737333] bg-[#0D121A] shadow-[1.5px_1.5px_4.5px_0_rgba(0,0,0,0.70)_inset]"
/>
</>
Expand Down Expand Up @@ -1176,6 +1177,7 @@ export function ChatSidebar({
onValueChange={setChatSpaceProjects}
variant="insideOut"
includeAuto
hideCount
triggerClassName="h-auto min-h-0 max-w-[min(160px,35vw)] rounded-full border border-[#161F2C] bg-[#000000] px-3 py-1.5 shadow-none hover:bg-[#05080D]"
/>
</>
Expand Down
116 changes: 90 additions & 26 deletions apps/web/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,39 @@ import { FeedbackModal } from "./feedback-modal"
import { useViewMode } from "@/lib/view-mode-context"
import { useQueryState } from "nuqs"
import { feedbackParam } from "@/lib/search-params"
import { useCustomer } from "autumn-js/react"
import { useTokenUsage } from "@/hooks/use-token-usage"
import { useOrgSummaries } from "@/hooks/use-org-summaries"
import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge"

interface HeaderProps {
onAddMemory?: () => void
onOpenSearch?: () => void
}

const brainItemClass = (active: boolean) =>
cn(
"gap-2.5 rounded-lg px-2 py-1.5 text-sm font-medium cursor-pointer transition-colors",
"hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white",
active ? "bg-white/[0.06] text-white" : "text-white/85",
)

const brainTileClass = (active: boolean) =>
cn(
"flex size-7 shrink-0 items-center justify-center rounded-lg border text-[11px] font-semibold",
active
? "border-[#2261CA66] bg-[#0B2A57] text-[#7EB0FF]"
: "border-white/[0.08] bg-white/[0.04] text-white/70",
)

export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
const { user, isRestoring } = useAuth()
const { user, isRestoring, org, organizations, setActiveOrg } = useAuth()
const autumn = useCustomer()
const { currentPlan } = useTokenUsage(autumn)
const { data: orgSummaries } = useOrgSummaries()
const planByOrgId = new Map(
(orgSummaries ?? []).map((s) => [s.orgId, s.plan] as const),
)
const { selectedProjects, setSelectedProjects } = useProject()
const router = useRouter()
const isMobile = useIsMobile()
Expand All @@ -66,41 +91,80 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
user?.email?.split("@")[0] ||
""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
const orgLabel = org?.name.replace(/\s*organizations?\s*$/i, "").trim()
const topLabel = orgLabel || userName
const hasOrgs = (organizations?.length ?? 0) > 0

const selectOrg = (slug: string, isActive: boolean) => {
if (isActive) return
void setActiveOrg(slug).then(() => window.location.reload())
}
return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-1.5 p-2.5 md:gap-2 md:p-3">
<div className="z-10! flex min-w-0 shrink items-center justify-center gap-1.5 md:gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex shrink-0 cursor-pointer items-center rounded-lg px-1.5 py-1 transition-colors hover:bg-white/5 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none md:-ml-2"
className="relative flex shrink-0 cursor-pointer items-center rounded-lg px-1.5 py-1 transition-colors hover:bg-white/5 outline-none focus-visible:outline-none md:-ml-2 before:absolute before:-inset-x-2 before:-inset-y-2.5 before:content-['']"
>
<Logo className="h-6 md:h-7" />
{userName && (
<div className="ml-1.5 flex flex-col items-start justify-center sm:ml-2">
<p className="text-[10px] leading-tight text-[#6B6B6B] sm:text-[11px]">
{userName}
</p>
<p className="-mt-0.5 text-sm leading-none font-medium text-white/90 sm:text-lg">
supermemory
</p>
</div>
)}
<Logo className="h-6" />
<div className="ml-1.5 flex flex-col items-start justify-center sm:ml-2">
<p className="max-w-[16ch] truncate text-[10px] leading-tight text-[#6B6B6B] sm:text-[11px]">
{topLabel}
</p>
<p className="-mt-0.5 text-sm leading-none font-medium text-white/90 sm:text-lg">
supermemory
</p>
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
alignOffset={12}
className={cn(
"min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
"min-w-[244px] p-1.5 rounded-xl border border-white/[0.08] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
dmSansClassName(),
)}
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
{hasOrgs && (
<>
<p className="px-2 pt-1 pb-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-[#5B6675]">
Switch brain
</p>
<div className="flex max-h-[40vh] flex-col gap-0.5 overflow-y-auto overscroll-contain">
{organizations?.map((o) => {
const active = org?.id === o.id
const plan = resolveOrgPlan(
o.id,
active,
currentPlan,
planByOrgId,
)
return (
<DropdownMenuItem
key={o.id}
onClick={() => selectOrg(o.slug, active)}
className={cn(brainItemClass(active))}
>
<span className={cn(brainTileClass(active))}>
{o.name?.trim().charAt(0).toUpperCase() || "?"}
</span>
<span className="flex-1 truncate">{o.name}</span>
<OrgPlanBadge plan={plan} />
</DropdownMenuItem>
)
})}
</div>
<DropdownMenuSeparator className="mx-1 my-1.5 bg-white/[0.06]" />
</>
)}
<DropdownMenuItem
asChild
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<Link href="/">
<Home className="size-4 text-[#737373]" />
Expand All @@ -109,7 +173,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
</DropdownMenuItem>
<DropdownMenuItem
asChild
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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="https://console.supermemory.ai"
Expand All @@ -122,7 +186,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
</DropdownMenuItem>
<DropdownMenuItem
asChild
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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="https://supermemory.ai" target="_blank" rel="noreferrer">
<ExternalLink className="size-4 text-[#737373]" />
Expand Down Expand Up @@ -290,64 +354,64 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
>
<DropdownMenuItem
onClick={onAddMemory}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<Plus className="size-4 text-[#737373]" />
Add memory
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => void setViewMode("dashboard")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<Home className="size-4 text-[#737373]" />
Home
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setViewMode("integrations")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<Sun className="size-4 text-[#737373]" />
Integrations
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => void setViewMode("graph")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<GraphIcon className="size-4 text-[#737373]" />
Graph
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onOpenSearch?.()}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<SearchIcon className="size-4 text-[#737373]" />
Search
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => void setViewMode("list")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<LayoutGrid className="size-4 text-[#737373]" />
Memories
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => void setViewMode("chat")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<MessageCircleIcon className="size-4 text-[#737373]" />
Chat with Nova
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-[#2E3033]" />
<DropdownMenuItem
onClick={handleFeedback}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<LifeBuoy className="size-4 text-[#737373]" />
Feedback
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => router.push("/settings")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
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"
>
<Settings className="size-4 text-[#737373]" />
Settings
Expand Down
35 changes: 35 additions & 0 deletions apps/web/components/org-plan-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { PLAN_DISPLAY_NAMES, type PlanType } from "@/hooks/use-token-usage"

const orgPlanBadgeBase = cn(
dmSans125ClassName(),
"inline-flex h-[18px] min-w-[42px] shrink-0 items-center justify-center rounded-[3px] px-1.5 text-[10px] uppercase",
)

const ORG_PLAN_BADGE_STYLES: Record<PlanType, string> = {
free: "bg-[#2E353D] font-mono font-medium tracking-[0.12em] text-[#A3A3A3]",
pro: "bg-[#4BA0FA] font-bold tracking-[0.36px] text-[#00171A]",
scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]",
enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]",
}

export function OrgPlanBadge({ plan }: { plan: PlanType }) {
return (
<span className={cn(orgPlanBadgeBase, ORG_PLAN_BADGE_STYLES[plan])}>
{PLAN_DISPLAY_NAMES[plan]}
</span>
)
}

export function resolveOrgPlan(
orgId: string,
isCurrent: boolean,
currentPlan: PlanType,
planByOrgId: Map<string, PlanType>,
): PlanType {
const fromSummary = planByOrgId.get(orgId)
if (fromSummary) return fromSummary
if (isCurrent) return currentPlan
return "free"
}
33 changes: 1 addition & 32 deletions apps/web/components/settings/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ 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_DISPLAY_NAMES,
PLAN_RANK,
useTokenUsage,
type PlanType,
Expand Down Expand Up @@ -76,25 +76,6 @@ function SettingsCard({ children }: { children: React.ReactNode }) {
}

/** Matches ACTIVE / RECOMMENDED pills in billing settings. */
const orgPlanBadgeBase = cn(
dmSans125ClassName(),
"inline-flex h-[18px] min-w-[42px] shrink-0 items-center justify-center rounded-[3px] px-1.5 text-[10px] uppercase",
)

const ORG_PLAN_BADGE_STYLES: Record<PlanType, string> = {
free: "bg-[#2E353D] font-mono font-medium tracking-[0.12em] text-[#A3A3A3]",
pro: "bg-[#4BA0FA] font-bold tracking-[0.36px] text-[#00171A]",
scale: "bg-[#0054AD] font-bold tracking-[0.36px] text-[#FAFAFA]",
enterprise: "bg-[#FAFAFA] font-bold tracking-[0.36px] text-[#0D121A]",
}

function OrgPlanBadge({ plan }: { plan: PlanType }) {
return (
<span className={cn(orgPlanBadgeBase, ORG_PLAN_BADGE_STYLES[plan])}>
{PLAN_DISPLAY_NAMES[plan]}
</span>
)
}

const ROLE_LABELS: Record<string, string> = {
owner: "Owner",
Expand Down Expand Up @@ -152,18 +133,6 @@ function isPendingInvitation(invitation: {
return new Date(invitation.expiresAt).getTime() > Date.now()
}

function resolveOrgPlan(
orgId: string,
isCurrent: boolean,
currentPlan: PlanType,
planByOrgId: Map<string, PlanType>,
): PlanType {
const fromSummary = planByOrgId.get(orgId)
if (fromSummary) return fromSummary
if (isCurrent) return currentPlan
return "free"
}

export default function Account() {
const {
user,
Expand Down
15 changes: 10 additions & 5 deletions apps/web/components/space-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface SpaceSelectorProps {
enableDelete?: boolean
compact?: boolean
includeAuto?: boolean
hideCount?: boolean
}

const triggerVariants = {
Expand Down Expand Up @@ -112,6 +113,7 @@ export function SpaceSelector({
enableDelete = false,
compact = false,
includeAuto = false,
hideCount = false,
}: SpaceSelectorProps) {
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false)
Expand Down Expand Up @@ -430,11 +432,14 @@ export function SpaceSelector({
>
{isLoading ? "…" : displayInfo.name}
</span>
{!compact && spaceCountData !== undefined && spaceCountData > 0 && (
<span className="shrink-0 text-[11px] text-[#737373] tabular-nums">
· {formatCount(spaceCountData)}
</span>
)}
{!compact &&
!hideCount &&
spaceCountData !== undefined &&
spaceCountData > 0 && (
<span className="shrink-0 text-[11px] text-[#737373] tabular-nums">
· {formatCount(spaceCountData)}
</span>
)}
<ChevronDownIcon
className="size-3.5 shrink-0 text-[#737373]"
aria-hidden
Expand Down
Loading
Loading