Skip to content

Commit ac43fe1

Browse files
committed
fix(web): org plan badges via org-summaries and refresh Nova chat empty state (supermemoryai#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.
1 parent 6448849 commit ac43fe1

7 files changed

Lines changed: 220 additions & 108 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"use client"
2+
3+
import { Search } from "lucide-react"
4+
import NovaOrb from "@/components/nova/nova-orb"
5+
import { cn } from "@lib/utils"
6+
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
7+
8+
export const DEFAULT_CHAT_PROMPTS = [
9+
"What do you know about me?",
10+
"What have I been working on lately?",
11+
"What themes keep showing up in my memories?",
12+
] as const
13+
14+
const SUGGESTION_PILL_CLASS = cn(
15+
"inline-flex max-w-full items-center gap-2 rounded-full border border-[#2261CA33] bg-[#041127]",
16+
"px-3 py-2 text-left transition-colors cursor-pointer",
17+
"hover:border-[#3374FF]/55 hover:bg-[#0A1A3A] hover:[&_span]:text-white",
18+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3374FF]/40",
19+
)
20+
21+
export function ChatEmptyStatePlaceholder({
22+
onSuggestionClick,
23+
suggestions = [...DEFAULT_CHAT_PROMPTS],
24+
subtitle,
25+
}: {
26+
onSuggestionClick: (suggestion: string) => void
27+
suggestions?: string[]
28+
subtitle?: string
29+
}) {
30+
const prompts = suggestions.slice(0, 3)
31+
32+
return (
33+
<div
34+
id="chat-empty-state"
35+
className={cn(
36+
"flex min-h-full items-center justify-center px-4 py-10",
37+
dmSansClassName(),
38+
)}
39+
>
40+
<div className="flex w-full max-w-[min(100%,360px)] flex-col items-center gap-5">
41+
<div className="flex flex-col items-center gap-3 text-center">
42+
<NovaOrb size={44} className="blur-[1px]!" />
43+
<p
44+
className={cn(
45+
"text-lg font-semibold text-[#fafafa]",
46+
dmSans125ClassName(),
47+
)}
48+
>
49+
Nova knows you.
50+
</p>
51+
{subtitle ? (
52+
<p className="max-w-[280px] text-sm leading-snug text-[#737373]">
53+
{subtitle}
54+
</p>
55+
) : null}
56+
</div>
57+
58+
<div className="flex w-full flex-col items-center gap-2">
59+
<p className="text-[10px] font-medium uppercase tracking-[0.1em] text-[#525966]">
60+
Try asking
61+
</p>
62+
<div className="flex w-full flex-col items-center gap-2">
63+
{prompts.map((prompt) => (
64+
<button
65+
key={prompt}
66+
type="button"
67+
onClick={() => onSuggestionClick(prompt)}
68+
className={SUGGESTION_PILL_CLASS}
69+
>
70+
<Search
71+
className="size-3.5 shrink-0 text-[#4BA0FA]"
72+
aria-hidden
73+
/>
74+
<span className="text-[12px] font-medium leading-snug text-[#4BA0FA]">
75+
{prompt}
76+
</span>
77+
</button>
78+
))}
79+
</div>
80+
</div>
81+
</div>
82+
</div>
83+
)
84+
}

apps/web/components/chat/index.tsx

Lines changed: 42 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client"
22

33
import { useState, useEffect, useCallback, useRef, useMemo } from "react"
4+
import { useQuery } from "@tanstack/react-query"
5+
import { $fetch } from "@lib/api"
46
import { useQueryState } from "nuqs"
57
import type { UIMessage } from "@ai-sdk/react"
68
import { motion } from "motion/react"
@@ -49,83 +51,7 @@ import { generateId } from "@lib/generate-id"
4951
import { useViewMode } from "@/lib/view-mode-context"
5052
import { threadParam } from "@/lib/search-params"
5153
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
52-
53-
const DEFAULT_CHAT_PROMPTS = [
54-
"What do you know about me?",
55-
"What have I been working on lately?",
56-
"What themes keep showing up in my memories?",
57-
]
58-
59-
const chatEmptyCardClass = cn(
60-
"flex min-h-[76px] flex-col justify-between rounded-lg border border-[#2B3038] bg-[#14161A]/95 p-3 text-left md:min-h-[88px]",
61-
"shadow-[0_18px_50px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.04)]",
62-
"transition-colors hover:border-[#3374FF]/55 hover:bg-[#1A1F26] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3374FF]/70",
63-
)
64-
65-
function ChatEmptyStatePlaceholder({
66-
onSuggestionClick,
67-
suggestions = DEFAULT_CHAT_PROMPTS,
68-
}: {
69-
onSuggestionClick: (suggestion: string) => void
70-
suggestions?: string[]
71-
}) {
72-
const promptCards = suggestions.slice(0, 3)
73-
74-
return (
75-
<div
76-
id="chat-empty-state"
77-
className="relative flex min-h-full items-center justify-center overflow-hidden px-0 py-6 md:px-3"
78-
>
79-
<div
80-
className="pointer-events-none absolute inset-x-[-1rem] inset-y-0 bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.28)_1px,transparent_1px)] bg-size-[32px_32px] opacity-80 mask-[radial-gradient(ellipse_at_center,black_52%,transparent_100%)]"
81-
aria-hidden
82-
/>
83-
<div
84-
className="pointer-events-none absolute inset-x-[-1rem] bottom-0 h-2/3 bg-[radial-gradient(ellipse_at_bottom,rgba(20,65,255,0.42),transparent_68%)]"
85-
aria-hidden
86-
/>
87-
<div className="relative z-10 flex w-full max-w-xl flex-col items-center text-center">
88-
<NovaOrb size={52} className="mb-3 blur-[1.5px]!" />
89-
<h2
90-
className={cn(
91-
"mb-1 max-w-[420px] text-[24px] font-medium leading-[1.12] tracking-normal text-white md:text-[30px]",
92-
dmSansClassName(),
93-
)}
94-
>
95-
Nova knows you.
96-
</h2>
97-
<p
98-
className={cn(
99-
"mb-4 max-w-[420px] text-[14px] leading-5 text-[#8B8B8B] md:text-[15px]",
100-
dmSansClassName(),
101-
)}
102-
>
103-
<span className="text-[#FAFAFA]">
104-
Your personal memories are all here.
105-
</span>{" "}
106-
Chat with supermemory and ask about...
107-
</p>
108-
<div className="mb-3 grid w-full grid-cols-1 gap-2.5 sm:grid-cols-3">
109-
{promptCards.map((suggestion, index) => (
110-
<button
111-
key={suggestion}
112-
type="button"
113-
onClick={() => onSuggestionClick(suggestion)}
114-
className={chatEmptyCardClass}
115-
>
116-
<span className="flex size-5 items-center justify-center rounded-full border border-[#3374FF]/35 bg-[#071B3A] text-[11px] font-medium text-[#4BA0FA]">
117-
{index + 1}
118-
</span>
119-
<span className="mt-2 line-clamp-3 text-[13px] font-medium leading-[18px] text-white md:text-[14px] md:leading-5">
120-
{suggestion}
121-
</span>
122-
</button>
123-
))}
124-
</div>
125-
</div>
126-
</div>
127-
)
128-
}
54+
import { ChatEmptyStatePlaceholder } from "./chat-empty-state"
12955

13056
export function ChatLaunchFab({
13157
onOpen,
@@ -243,6 +169,43 @@ export function ChatSidebar({
243169
}),
244170
[chatProject, allProjects],
245171
)
172+
const isAutoChatSpace = chatProject === AUTO_CHAT_SPACE_ID
173+
const { data: chatSpaceMemoryCount } = useQuery({
174+
queryKey: ["chat-empty-space-count", chatProject],
175+
queryFn: async (): Promise<number> => {
176+
const response = await $fetch("@post/documents/documents", {
177+
body: {
178+
page: 1,
179+
limit: 1,
180+
sort: "createdAt",
181+
order: "desc",
182+
containerTags: [chatProject],
183+
},
184+
disableValidation: true,
185+
})
186+
if (response.error) return 0
187+
const data = response.data as {
188+
pagination?: { totalItems?: number }
189+
} | null
190+
return data?.pagination?.totalItems ?? 0
191+
},
192+
staleTime: 30 * 1000,
193+
enabled: !!chatProject && !isAutoChatSpace,
194+
})
195+
const emptyStateSubtitle = useMemo(() => {
196+
if (isAutoChatSpace) {
197+
return "Picks the best space for each question"
198+
}
199+
if (chatSpaceMemoryCount === undefined) {
200+
return `Grounded in ${chatSpaceLabel}`
201+
}
202+
if (chatSpaceMemoryCount === 0) {
203+
return `Nothing in ${chatSpaceLabel} yet`
204+
}
205+
const countLabel = chatSpaceMemoryCount.toLocaleString()
206+
const memoryWord = chatSpaceMemoryCount === 1 ? "memory" : "memories"
207+
return `${countLabel} ${memoryWord} in ${chatSpaceLabel}`
208+
}, [isAutoChatSpace, chatSpaceLabel, chatSpaceMemoryCount])
246209
const { viewMode } = useViewMode()
247210
const { user: _user } = useAuth()
248211
const [threadId, setThreadId] = useQueryState("thread", threadParam)
@@ -1011,6 +974,7 @@ export function ChatSidebar({
1011974
<ChatEmptyStatePlaceholder
1012975
onSuggestionClick={handleSuggestedQuestion}
1013976
suggestions={emptyStateSuggestions}
977+
subtitle={emptyStateSubtitle}
1014978
/>
1015979
)}
1016980
<div
@@ -1246,3 +1210,4 @@ export function ChatSidebar({
12461210
}
12471211

12481212
export { HomeChatComposer } from "./home-chat-composer"
1213+
export { ChatEmptyStatePlaceholder } from "./chat-empty-state"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client"
2+
3+
import { Shuffle } from "lucide-react"
4+
import NovaOrb from "./nova-orb"
5+
import { cn } from "@lib/utils"
6+
7+
/** Nova orb with a corner badge — Auto mode (Nova picks across spaces). */
8+
export function AutoSpaceIcon({
9+
size = 20,
10+
className,
11+
}: {
12+
size?: number
13+
className?: string
14+
}) {
15+
const badgeSize = Math.max(10, Math.round(size * 0.5))
16+
const badgeIcon = Math.max(6, Math.round(badgeSize * 0.55))
17+
18+
return (
19+
<span
20+
className={cn("relative shrink-0", className)}
21+
style={{ width: size, height: size }}
22+
aria-hidden
23+
>
24+
<NovaOrb size={size} className="blur-[0.45px]!" />
25+
<span
26+
className="absolute -bottom-px -right-px flex items-center justify-center rounded-full bg-[#4BA0FA] text-[#041127] ring-1 ring-[#14161A]"
27+
style={{ width: badgeSize, height: badgeSize }}
28+
>
29+
<Shuffle
30+
className="shrink-0"
31+
style={{ width: badgeIcon, height: badgeIcon }}
32+
strokeWidth={2.5}
33+
/>
34+
</span>
35+
</span>
36+
)
37+
}

apps/web/components/select-spaces-modal.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
Loader,
2020
Pencil,
2121
Check,
22-
Sparkles,
2322
} from "lucide-react"
2423
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
2524
import { toast } from "sonner"
@@ -47,6 +46,7 @@ import { InstallSteps, PillButton } from "./integrations/install-steps"
4746
import { useProjectMutations } from "@/hooks/use-project-mutations"
4847
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
4948
import NovaOrb from "@/components/nova/nova-orb"
49+
import { AutoSpaceIcon } from "@/components/nova/auto-space-icon"
5050

5151
interface SelectSpacesModalProps {
5252
isOpen: boolean
@@ -813,12 +813,7 @@ export function SelectSpacesModal({
813813
onClick={handleSelectAuto}
814814
className="flex min-w-0 flex-1 items-center gap-3 text-left cursor-pointer focus:outline-none focus:ring-0"
815815
>
816-
<span
817-
className="shrink-0 flex h-5 w-5 items-center justify-center rounded-[6px] bg-[#071B3A] text-[#4BA0FA]"
818-
aria-hidden
819-
>
820-
<Sparkles className="size-3.5" />
821-
</span>
816+
<AutoSpaceIcon size={20} />
822817
<span className="min-w-0 flex-1 truncate text-[#fafafa] text-sm font-medium">
823818
Auto
824819
<span className="ml-1.5 text-[12px] text-[#737373]">

0 commit comments

Comments
 (0)