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 (