From ac43fe157fc12bdb11898bdd7b470aed8fa4e9fc Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 18 May 2026 23:44:09 +0000 Subject: [PATCH] fix(web): org plan badges via org-summaries and refresh Nova chat empty state (#968) Use /v3/auth/org-summaries for per-org plan tiers in settings (same as console v2). Extract chat empty state, add space-aware subtitle, and use AutoSpaceIcon in selectors. --- apps/web/components/chat/chat-empty-state.tsx | 84 +++++++++++++ apps/web/components/chat/index.tsx | 119 +++++++----------- apps/web/components/nova/auto-space-icon.tsx | 37 ++++++ apps/web/components/select-spaces-modal.tsx | 9 +- apps/web/components/settings/account.tsx | 36 +++--- apps/web/components/space-selector.tsx | 5 +- apps/web/hooks/use-org-summaries.ts | 38 ++++++ 7 files changed, 220 insertions(+), 108 deletions(-) create mode 100644 apps/web/components/chat/chat-empty-state.tsx create mode 100644 apps/web/components/nova/auto-space-icon.tsx create mode 100644 apps/web/hooks/use-org-summaries.ts diff --git a/apps/web/components/chat/chat-empty-state.tsx b/apps/web/components/chat/chat-empty-state.tsx new file mode 100644 index 00000000..1fe230bf --- /dev/null +++ b/apps/web/components/chat/chat-empty-state.tsx @@ -0,0 +1,84 @@ +"use client" + +import { Search } from "lucide-react" +import NovaOrb from "@/components/nova/nova-orb" +import { cn } from "@lib/utils" +import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" + +export const DEFAULT_CHAT_PROMPTS = [ + "What do you know about me?", + "What have I been working on lately?", + "What themes keep showing up in my memories?", +] as const + +const SUGGESTION_PILL_CLASS = cn( + "inline-flex max-w-full items-center gap-2 rounded-full border border-[#2261CA33] bg-[#041127]", + "px-3 py-2 text-left transition-colors cursor-pointer", + "hover:border-[#3374FF]/55 hover:bg-[#0A1A3A] hover:[&_span]:text-white", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3374FF]/40", +) + +export function ChatEmptyStatePlaceholder({ + onSuggestionClick, + suggestions = [...DEFAULT_CHAT_PROMPTS], + subtitle, +}: { + onSuggestionClick: (suggestion: string) => void + suggestions?: string[] + subtitle?: string +}) { + const prompts = suggestions.slice(0, 3) + + return ( +
+
+
+ +

+ Nova knows you. +

+ {subtitle ? ( +

+ {subtitle} +

+ ) : null} +
+ +
+

+ Try asking +

+
+ {prompts.map((prompt) => ( + + ))} +
+
+
+
+ ) +} diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index faef8554..40e112e3 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -1,6 +1,8 @@ "use client" import { useState, useEffect, useCallback, useRef, useMemo } from "react" +import { useQuery } from "@tanstack/react-query" +import { $fetch } from "@lib/api" import { useQueryState } from "nuqs" import type { UIMessage } from "@ai-sdk/react" import { motion } from "motion/react" @@ -49,83 +51,7 @@ import { generateId } from "@lib/generate-id" import { useViewMode } from "@/lib/view-mode-context" import { threadParam } from "@/lib/search-params" import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" - -const DEFAULT_CHAT_PROMPTS = [ - "What do you know about me?", - "What have I been working on lately?", - "What themes keep showing up in my memories?", -] - -const chatEmptyCardClass = cn( - "flex min-h-[76px] flex-col justify-between rounded-lg border border-[#2B3038] bg-[#14161A]/95 p-3 text-left md:min-h-[88px]", - "shadow-[0_18px_50px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.04)]", - "transition-colors hover:border-[#3374FF]/55 hover:bg-[#1A1F26] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3374FF]/70", -) - -function ChatEmptyStatePlaceholder({ - onSuggestionClick, - suggestions = DEFAULT_CHAT_PROMPTS, -}: { - onSuggestionClick: (suggestion: string) => void - suggestions?: string[] -}) { - const promptCards = suggestions.slice(0, 3) - - return ( -
-
-
-
- -

- Nova knows you. -

-

- - Your personal memories are all here. - {" "} - Chat with supermemory and ask about... -

-
- {promptCards.map((suggestion, index) => ( - - ))} -
-
-
- ) -} +import { ChatEmptyStatePlaceholder } from "./chat-empty-state" export function ChatLaunchFab({ onOpen, @@ -243,6 +169,43 @@ export function ChatSidebar({ }), [chatProject, allProjects], ) + const isAutoChatSpace = chatProject === AUTO_CHAT_SPACE_ID + const { data: chatSpaceMemoryCount } = useQuery({ + queryKey: ["chat-empty-space-count", chatProject], + queryFn: async (): Promise => { + const response = await $fetch("@post/documents/documents", { + body: { + page: 1, + limit: 1, + sort: "createdAt", + order: "desc", + containerTags: [chatProject], + }, + disableValidation: true, + }) + if (response.error) return 0 + const data = response.data as { + pagination?: { totalItems?: number } + } | null + return data?.pagination?.totalItems ?? 0 + }, + staleTime: 30 * 1000, + enabled: !!chatProject && !isAutoChatSpace, + }) + const emptyStateSubtitle = useMemo(() => { + if (isAutoChatSpace) { + return "Picks the best space for each question" + } + if (chatSpaceMemoryCount === undefined) { + return `Grounded in ${chatSpaceLabel}` + } + if (chatSpaceMemoryCount === 0) { + return `Nothing in ${chatSpaceLabel} yet` + } + const countLabel = chatSpaceMemoryCount.toLocaleString() + const memoryWord = chatSpaceMemoryCount === 1 ? "memory" : "memories" + return `${countLabel} ${memoryWord} in ${chatSpaceLabel}` + }, [isAutoChatSpace, chatSpaceLabel, chatSpaceMemoryCount]) const { viewMode } = useViewMode() const { user: _user } = useAuth() const [threadId, setThreadId] = useQueryState("thread", threadParam) @@ -1011,6 +974,7 @@ export function ChatSidebar({ )}
+ + + + + + ) +} diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index 7529d521..5fde6122 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -19,7 +19,6 @@ import { Loader, Pencil, Check, - Sparkles, } from "lucide-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" @@ -47,6 +46,7 @@ import { InstallSteps, PillButton } from "./integrations/install-steps" import { useProjectMutations } from "@/hooks/use-project-mutations" import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import NovaOrb from "@/components/nova/nova-orb" +import { AutoSpaceIcon } from "@/components/nova/auto-space-icon" interface SelectSpacesModalProps { isOpen: boolean @@ -813,12 +813,7 @@ export function SelectSpacesModal({ onClick={handleSelectAuto} className="flex min-w-0 flex-1 items-center gap-3 text-left cursor-pointer focus:outline-none focus:ring-0" > - - - + Auto diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 978d8fc3..92fa8093 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -7,9 +7,9 @@ import { useAccountMemberships, useDeleteUserAccount, } from "@/hooks/use-account-settings" +import { useOrgSummaries } from "@/hooks/use-org-summaries" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { - normalizePlanType, PLAN_DISPLAY_NAMES, PLAN_RANK, useTokenUsage, @@ -216,18 +216,14 @@ function OrgPlanBadge({ plan }: { plan: PlanType }) { function resolveOrgPlan( orgId: string, - organization: { metadata?: unknown }, isCurrent: boolean, currentPlan: PlanType, - membershipPlanByOrgId: Map, + planByOrgId: Map, ): PlanType { + const fromSummary = planByOrgId.get(orgId) + if (fromSummary) return fromSummary if (isCurrent) return currentPlan - - const fromMembership = membershipPlanByOrgId.get(orgId) - if (fromMembership) return fromMembership - - const metadata = organization.metadata as Record | null - return normalizePlanType(metadata?.plan ?? metadata?.subscriptionPlan) + return "free" } export default function Account() { @@ -250,6 +246,7 @@ export default function Account() { const canSwitchOrg = (allOrgs?.length ?? 0) > 1 const { data: memberships, isPending: membershipsPending } = useAccountMemberships() + const { data: orgSummaries } = useOrgSummaries() const sortedMemberships = useMemo(() => { if (!memberships?.length) return [] @@ -306,38 +303,34 @@ export default function Account() { const planDisplayNames = PLAN_DISPLAY_NAMES - const membershipPlanByOrgId = useMemo(() => { + const planByOrgId = useMemo(() => { const map = new Map() - for (const membership of memberships ?? []) { - if (membership.plan) { - map.set(membership.orgId, normalizePlanType(membership.plan)) - } + for (const summary of orgSummaries ?? []) { + map.set(summary.orgId, summary.plan) } return map - }, [memberships]) + }, [orgSummaries]) const sortedOrgsForMenu = useMemo(() => { if (!allOrgs?.length) return [] return [...allOrgs].sort((a, b) => { const planA = resolveOrgPlan( a.id, - a, a.id === org?.id, currentPlan, - membershipPlanByOrgId, + planByOrgId, ) const planB = resolveOrgPlan( b.id, - b, b.id === org?.id, currentPlan, - membershipPlanByOrgId, + planByOrgId, ) const rankDiff = PLAN_RANK[planB] - PLAN_RANK[planA] if (rankDiff !== 0) return rankDiff return a.name.localeCompare(b.name) }) - }, [allOrgs, org?.id, currentPlan, membershipPlanByOrgId]) + }, [allOrgs, org?.id, currentPlan, planByOrgId]) // Handlers const handleUpgrade = async () => { @@ -508,10 +501,9 @@ export default function Account() { const isSwitching = switchingOrgId === organization.id const plan = resolveOrgPlan( organization.id, - organization, isCurrent, currentPlan, - membershipPlanByOrgId, + planByOrgId, ) return (