Skip to content

Commit 962fb85

Browse files
MaheshtheDevgithub-actions[bot]claude
authored
feat: empty state action for new spaces (supermemoryai#780)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ba56b36 commit 962fb85

13 files changed

Lines changed: 369 additions & 123 deletions

File tree

apps/mcp/src/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,11 @@ export class SupermemoryClient {
221221
}
222222

223223
// Search memories using SDK
224-
async search(query: string, limit = 10, threshold?: number): Promise<SearchResult> {
224+
async search(
225+
query: string,
226+
limit = 10,
227+
threshold?: number,
228+
): Promise<SearchResult> {
225229
try {
226230
const result = await this.client.search.memories({
227231
q: query,

apps/web/app/(app)/page.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { AnimatePresence } from "motion/react"
1919
import { useIsMobile } from "@hooks/use-mobile"
2020
import { useAuth } from "@lib/auth-context"
2121
import { useProject } from "@/stores"
22+
import { useContainerTags } from "@/hooks/use-container-tags"
23+
import { DEFAULT_PROJECT_ID } from "@lib/constants"
2224
import {
2325
useQuickNoteDraftReset,
2426
useQuickNoteDraft,
@@ -38,6 +40,7 @@ import {
3840
docParam,
3941
fullscreenParam,
4042
chatParam,
43+
integrationParam,
4144
} from "@/lib/search-params"
4245

4346
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
@@ -63,7 +66,21 @@ function ViewErrorFallback() {
6366
export default function NewPage() {
6467
const isMobile = useIsMobile()
6568
const { user, session } = useAuth()
66-
const { selectedProject, isNovaSpaces, novaContainerTags } = useProject()
69+
const { selectedProject, isNovaSpaces, novaContainerTags, selectedProjects } =
70+
useProject()
71+
const selectedProjectTag = selectedProjects[0]
72+
const isNovaContext =
73+
isNovaSpaces ||
74+
(selectedProjectTag !== undefined &&
75+
novaContainerTags.includes(selectedProjectTag))
76+
const { allProjects } = useContainerTags()
77+
const emptyStateSpaceName =
78+
!isNovaSpaces && selectedProjectTag
79+
? selectedProjectTag === DEFAULT_PROJECT_ID
80+
? "My Space"
81+
: (allProjects.find((p) => p.containerTag === selectedProjectTag)
82+
?.name ?? selectedProjectTag)
83+
: undefined
6784
const { viewMode, setViewMode } = useViewMode()
6885
const queryClient = useQueryClient()
6986

@@ -93,6 +110,7 @@ export default function NewPage() {
93110
fullscreenParam,
94111
)
95112
const [isChatOpen, setIsChatOpen] = useQueryState("chat", chatParam)
113+
const [, setIntegration] = useQueryState("integration", integrationParam)
96114

97115
// Ephemeral local state (not worth URL-encoding)
98116
const [fullscreenInitialContent, setFullscreenInitialContent] = useState("")
@@ -348,6 +366,26 @@ export default function NewPage() {
348366
[setSearchPrefill, setIsSearchOpen],
349367
)
350368

369+
const handleOpenIntegrations = useCallback(
370+
(integration?: "import" | "chrome" | "connections") => {
371+
setViewMode("integrations")
372+
if (integration) {
373+
setIntegration(integration)
374+
} else {
375+
setIntegration(null)
376+
}
377+
},
378+
[setViewMode, setIntegration],
379+
)
380+
381+
const handleAddMemory = useCallback(
382+
(tab: "note" | "link") => {
383+
analytics.addDocumentModalOpened()
384+
setAddDoc(tab)
385+
},
386+
[setAddDoc],
387+
)
388+
351389
const chatOpen = isChatOpen !== null ? isChatOpen : !isMobile
352390
const isGraphMode = viewMode === "graph" && !isMobile
353391

@@ -421,6 +459,16 @@ export default function NewPage() {
421459
onShowRelated: handleHighlightsShowRelated,
422460
isLoading: isLoadingHighlights,
423461
}}
462+
emptyStateProps={
463+
isNovaContext
464+
? {
465+
onAddMemory: handleAddMemory,
466+
onOpenIntegrations: handleOpenIntegrations,
467+
isAllSpaces: isNovaSpaces,
468+
spaceName: emptyStateSpaceName,
469+
}
470+
: undefined
471+
}
424472
/>
425473
</div>
426474
)}

apps/web/components/integrations-view.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client"
22

3-
import { useState } from "react"
3+
import { useState, useEffect } from "react"
4+
import { useQueryState } from "nuqs"
45
import { cn } from "@lib/utils"
56
import { dmSansClassName } from "@/lib/fonts"
67
import { Button } from "@ui/components/button"
@@ -18,6 +19,10 @@ import {
1819
} from "@/components/integration-icons"
1920
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
2021
import { ArrowLeft, Sun } from "lucide-react"
22+
import {
23+
integrationParam,
24+
type IntegrationParamValue,
25+
} from "@/lib/search-params"
2126
import Image from "next/image"
2227

2328
type CardId =
@@ -140,10 +145,29 @@ function DetailWrapper({
140145
)
141146
}
142147

148+
const INTEGRATION_TO_CARD: Record<IntegrationParamValue, CardId> = {
149+
import: "import",
150+
chrome: "chrome",
151+
connections: "connections",
152+
}
153+
143154
export function IntegrationsView() {
155+
const [integration, setIntegration] = useQueryState(
156+
"integration",
157+
integrationParam,
158+
)
144159
const [selectedCard, setSelectedCard] = useState<CardId | null>(null)
145160

146-
const handleBack = () => setSelectedCard(null)
161+
useEffect(() => {
162+
if (integration && INTEGRATION_TO_CARD[integration]) {
163+
setSelectedCard(INTEGRATION_TO_CARD[integration])
164+
}
165+
}, [integration])
166+
167+
const handleBack = () => {
168+
setSelectedCard(null)
169+
setIntegration(null)
170+
}
147171

148172
switch (selectedCard) {
149173
case "mcp":

apps/web/components/memories-grid.tsx

Lines changed: 105 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { HighlightsCard, type HighlightItem } from "./highlights-card"
3131
import { GraphCard } from "./memory-graph"
3232
import { Button } from "@ui/components/button"
3333
import { categoriesParam } from "@/lib/search-params"
34+
import { NovaEmptyState } from "@/components/nova/nova-empty-state"
3435
import {
3536
AlertDialog,
3637
AlertDialogAction,
@@ -92,6 +93,15 @@ interface HighlightsProps {
9293
isLoading: boolean
9394
}
9495

96+
interface NovaEmptyStateProps {
97+
onAddMemory: (tab: "note" | "link") => void
98+
onOpenIntegrations: (
99+
integration?: "import" | "chrome" | "connections",
100+
) => void
101+
isAllSpaces: boolean
102+
spaceName?: string
103+
}
104+
95105
interface MemoriesGridProps {
96106
isChatOpen: boolean
97107
onOpenDocument: (document: DocumentWithMemories) => void
@@ -105,6 +115,7 @@ interface MemoriesGridProps {
105115
isBulkDeleting?: boolean
106116
quickNoteProps?: QuickNoteProps
107117
highlightsProps?: HighlightsProps
118+
emptyStateProps?: NovaEmptyStateProps
108119
}
109120

110121
export function MemoriesGrid({
@@ -120,6 +131,7 @@ export function MemoriesGrid({
120131
isBulkDeleting = false,
121132
quickNoteProps,
122133
highlightsProps,
134+
emptyStateProps,
123135
}: MemoriesGridProps) {
124136
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
125137
const { user } = useAuth()
@@ -340,99 +352,107 @@ export function MemoriesGrid({
340352
)
341353
}
342354

355+
const isEmpty = documents.length === 0 && !isPending
356+
const showNovaEmptyState = isEmpty && emptyStateProps
357+
343358
return (
344359
<div className="relative">
345-
<div
346-
id="filter-pills"
347-
className="flex items-center justify-between gap-4 mb-3"
348-
>
349-
<div className="flex flex-wrap items-center gap-1.5">
350-
<Button
351-
className={cn(
352-
dmSansClassName(),
353-
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
354-
selectedCategories.length === 0 &&
355-
"bg-[#00173C] border-[#2261CA33]",
356-
)}
357-
onClick={handleSelectAll}
358-
>
359-
All
360-
{facetsData?.total !== undefined && (
361-
<span className="ml-1 text-[#737373]">({facetsData.total})</span>
362-
)}
363-
</Button>
364-
{facetsData?.facets.map((facet: DocumentFacet) => (
360+
{!isEmpty && (
361+
<div
362+
id="filter-pills"
363+
className="flex items-center justify-between gap-4 mb-3"
364+
>
365+
<div className="flex flex-wrap items-center gap-1.5">
365366
<Button
366-
key={facet.category}
367367
className={cn(
368368
dmSansClassName(),
369369
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
370-
selectedCategories.includes(facet.category) &&
370+
selectedCategories.length === 0 &&
371371
"bg-[#00173C] border-[#2261CA33]",
372372
)}
373-
onClick={() => handleCategoryToggle(facet.category)}
373+
onClick={handleSelectAll}
374374
>
375-
{facet.label}
376-
<span className="ml-1 text-[#737373]">({facet.count})</span>
375+
All
376+
{facetsData?.total !== undefined && (
377+
<span className="ml-1 text-[#737373]">
378+
({facetsData.total})
379+
</span>
380+
)}
377381
</Button>
378-
))}
379-
</div>
380-
381-
<div className="flex items-center gap-2 shrink-0">
382-
{isSelectionMode && (
383-
<>
382+
{facetsData?.facets.map((facet: DocumentFacet) => (
383+
<Button
384+
key={facet.category}
385+
className={cn(
386+
dmSansClassName(),
387+
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
388+
selectedCategories.includes(facet.category) &&
389+
"bg-[#00173C] border-[#2261CA33]",
390+
)}
391+
onClick={() => handleCategoryToggle(facet.category)}
392+
>
393+
{facet.label}
394+
<span className="ml-1 text-[#737373]">({facet.count})</span>
395+
</Button>
396+
))}
397+
</div>
398+
<div className="flex items-center gap-2 shrink-0">
399+
{isSelectionMode && (
400+
<>
401+
<button
402+
type="button"
403+
aria-label="Exit selection mode"
404+
className="w-8 h-8 flex items-center justify-center rounded-full border border-[#161F2C] bg-[#0D121A] hover:bg-[#00173C] transition-colors cursor-pointer"
405+
onClick={onClearSelection}
406+
>
407+
<XIcon className="w-4 h-4 text-[#737373]" />
408+
</button>
409+
{selectedDocumentIds.size > 0 ? (
410+
<>
411+
<button
412+
type="button"
413+
className={cn(
414+
dmSansClassName(),
415+
"text-xs text-[#737373] hover:text-white transition-colors cursor-pointer",
416+
)}
417+
onClick={handleSelectAllVisible}
418+
>
419+
Select all
420+
</button>
421+
<button
422+
type="button"
423+
className={cn(
424+
dmSansClassName(),
425+
"flex items-center gap-1 text-xs text-red-400 hover:text-red-300 transition-colors cursor-pointer disabled:opacity-50",
426+
)}
427+
onClick={handleBulkDeleteClick}
428+
disabled={isBulkDeleting}
429+
>
430+
<Trash2Icon className="w-3 h-3" />
431+
Delete ({selectedDocumentIds.size})
432+
</button>
433+
</>
434+
) : (
435+
<p
436+
className={cn(dmSansClassName(), "text-xs text-[#737373]")}
437+
>
438+
Select one or more documents
439+
</p>
440+
)}
441+
</>
442+
)}
443+
{!isSelectionMode && onEnterSelectionMode && (
384444
<button
385445
type="button"
386-
aria-label="Exit selection mode"
446+
aria-label="Enter selection mode"
387447
className="w-8 h-8 flex items-center justify-center rounded-full border border-[#161F2C] bg-[#0D121A] hover:bg-[#00173C] transition-colors cursor-pointer"
388-
onClick={onClearSelection}
448+
onClick={onEnterSelectionMode}
389449
>
390-
<XIcon className="w-4 h-4 text-[#737373]" />
450+
<div className="w-3 h-3 rounded-[2.25px] border border-[#737373]" />
391451
</button>
392-
{selectedDocumentIds.size > 0 ? (
393-
<>
394-
<button
395-
type="button"
396-
className={cn(
397-
dmSansClassName(),
398-
"text-xs text-[#737373] hover:text-white transition-colors cursor-pointer",
399-
)}
400-
onClick={handleSelectAllVisible}
401-
>
402-
Select all
403-
</button>
404-
<button
405-
type="button"
406-
className={cn(
407-
dmSansClassName(),
408-
"flex items-center gap-1 text-xs text-red-400 hover:text-red-300 transition-colors cursor-pointer disabled:opacity-50",
409-
)}
410-
onClick={handleBulkDeleteClick}
411-
disabled={isBulkDeleting}
412-
>
413-
<Trash2Icon className="w-3 h-3" />
414-
Delete ({selectedDocumentIds.size})
415-
</button>
416-
</>
417-
) : (
418-
<p className={cn(dmSansClassName(), "text-xs text-[#737373]")}>
419-
Select one or more documents
420-
</p>
421-
)}
422-
</>
423-
)}
424-
{!isSelectionMode && onEnterSelectionMode && (
425-
<button
426-
type="button"
427-
aria-label="Enter selection mode"
428-
className="w-8 h-8 flex items-center justify-center rounded-full border border-[#161F2C] bg-[#0D121A] hover:bg-[#00173C] transition-colors cursor-pointer"
429-
onClick={onEnterSelectionMode}
430-
>
431-
<div className="w-3 h-3 rounded-[2.25px] border border-[#737373]" />
432-
</button>
433-
)}
452+
)}
453+
</div>
434454
</div>
435-
</div>
455+
)}
436456

437457
<AlertDialog
438458
open={showBulkDeleteConfirm}
@@ -486,7 +506,14 @@ export function MemoriesGrid({
486506
<div className="h-full flex items-center justify-center p-4">
487507
<SuperLoader />
488508
</div>
489-
) : documents.length === 0 && !isPending ? (
509+
) : showNovaEmptyState ? (
510+
<NovaEmptyState
511+
onAddMemory={emptyStateProps.onAddMemory}
512+
onOpenIntegrations={emptyStateProps.onOpenIntegrations}
513+
isAllSpaces={emptyStateProps.isAllSpaces}
514+
spaceName={emptyStateProps.spaceName}
515+
/>
516+
) : isEmpty ? (
490517
<div className="h-full flex items-center justify-center p-4">
491518
<div className="text-center text-muted-foreground">
492519
No memories found

0 commit comments

Comments
 (0)