From 022fab2dca7481a949def2d7c47e7bab54a84c23 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommmu Date: Tue, 10 Mar 2026 11:06:23 +0530 Subject: [PATCH 1/4] feat: bulk delete documents in nova app --- apps/web/app/(app)/page.tsx | 63 ++- apps/web/app/upgrade-mcp/page.tsx | 8 +- apps/web/components/chat/index.tsx | 176 ++++----- apps/web/components/memories-grid.tsx | 270 +++++++++++-- .../components/memory-graph/memory-graph.tsx | 4 +- apps/web/hooks/use-document-mutations.ts | 98 ++++- apps/web/lib/analytics.ts | 3 + bun.lock | 369 ++++++++++++++++-- packages/lib/api.ts | 8 + packages/lib/constants.ts | 3 +- .../src/components/memory-graph.tsx | 7 +- 11 files changed, 840 insertions(+), 169 deletions(-) diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index b9366efb6..634db8076 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -137,13 +137,66 @@ export default function NewPage() { const resetDraft = useQuickNoteDraftReset(selectedProject) const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "") - const { noteMutation } = useDocumentMutations({ + const { noteMutation, bulkDeleteMutation } = useDocumentMutations({ onClose: () => { resetDraft() setIsFullscreen(false) }, }) + const [selectedDocumentIds, setSelectedDocumentIds] = useState>( + new Set(), + ) + const [isSelectionMode, setIsSelectionMode] = useState(false) + + const handleToggleSelection = useCallback((documentId: string) => { + setSelectedDocumentIds((prev) => { + const next = new Set(prev) + if (next.has(documentId)) { + next.delete(documentId) + } else { + next.add(documentId) + } + return next + }) + }, []) + + const handleClearSelection = useCallback(() => { + setSelectedDocumentIds(new Set()) + setIsSelectionMode(false) + }, []) + + const handleEnterSelectionMode = useCallback(() => { + setIsSelectionMode(true) + }, []) + + const handleSelectAllVisible = useCallback((visibleIds: string[]) => { + setSelectedDocumentIds((prev) => { + const next = new Set(prev) + for (const id of visibleIds) { + next.add(id) + } + return next + }) + }, []) + + const handleBulkDelete = useCallback(() => { + const ids = Array.from(selectedDocumentIds) + if (ids.length === 0) return + bulkDeleteMutation.mutate( + { documentIds: ids }, + { + onSuccess: () => { + setSelectedDocumentIds(new Set()) + setIsSelectionMode(false) + if (selectedDocument && ids.includes(selectedDocument.id ?? "")) { + setDocId(null) + } + }, + }, + ) + }, [selectedDocumentIds, bulkDeleteMutation, selectedDocument, setDocId]) + type SpaceHighlightsResponse = { highlights: HighlightItem[] questions: string[] @@ -349,6 +402,14 @@ export default function NewPage() { ) => setMcpUrl(e.target.value)} + onChange={(e: React.ChangeEvent) => + setMcpUrl(e.target.value) + } placeholder="https://mcp.supermemory.ai/userId/sse" type="url" value={mcpUrl} @@ -205,7 +207,9 @@ export default function MigrateMCPPage() { className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl" disabled={migrateMutation.isPending} id="projectId" - onChange={(e: React.ChangeEvent) => setProjectId(e.target.value)} + onChange={(e: React.ChangeEvent) => + setProjectId(e.target.value) + } placeholder="Project ID (default: 'default')" type="text" value={projectId} diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index ea1ee4c0f..bcf17ef75 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -675,104 +675,104 @@ export function ChatSidebar({ - - - Chat History - - Project: {selectedProject} - - - - {isLoadingThreads ? ( -
- -
- ) : threads.length === 0 ? ( -
- No conversations yet -
- ) : ( -
- {threads.map((thread) => { - const isActive = thread.id === currentChatId - return ( - - -
- ) : ( +
+ {formatRelativeTime(thread.updatedAt)} +
+ + {confirmingDeleteId === thread.id ? ( +
+ - )} - - ) - })} -
- )} -
- -
- + + ) : ( + + )} + + ) + })} + + )} + + + + - {facetsData?.facets.map((facet: DocumentFacet) => ( +
+
- ))} + {facetsData?.facets.map((facet: DocumentFacet) => ( + + ))} +
+ +
+
+ {isSelectionMode && ( + + )} + {!isSelectionMode && onEnterSelectionMode && ( + + )} +
+ {isSelectionMode && ( +
+ {selectedDocumentIds.size > 0 && ( + <> + + + + )} + {selectedDocumentIds.size === 0 && ( +

+ Select one or more documents +

+ )} +
+ )} +
+ + + + + + Delete selected memories? + + + This will permanently delete {selectedDocumentIds.size} memory + {selectedDocumentIds.size === 1 ? "" : "ies"}. This action cannot + be undone. + + + + setShowBulkDeleteConfirm(false)} + > + Cancel + + + {isBulkDeleting ? "Deleting…" : "Delete"} + + + + + {error ? (
@@ -411,18 +575,31 @@ function DocumentUrlDisplay({ url }: { url: string }) { ) } +function isTemporaryId(id: string | null | undefined): boolean { + if (!id) return false + return id.startsWith("temp-") || id.startsWith("temp-file-") +} + const DocumentCard = memo( ({ index: _index, data: document, width, onClick, + isSelectionMode = false, + isSelected = false, + onToggleSelection, }: { index: number data: DocumentWithMemories width: number onClick: (document: DocumentWithMemories) => void + isSelectionMode?: boolean + isSelected?: boolean + onToggleSelection?: () => void }) => { + const canSelect = + !isTemporaryId(document.id) && !isTemporaryId(document.customId) const [rotation, setRotation] = useState({ rotateX: 0, rotateY: 0 }) const cardRef = useRef(null) const [ogData, setOgData] = useState(null) @@ -465,8 +642,12 @@ const DocumentCard = memo( } }, [needsOgData, ogData, isLoadingOg, document.url]) + useEffect(() => { + if (isSelectionMode) setRotation({ rotateX: 0, rotateY: 0 }) + }, [isSelectionMode]) + const handleMouseMove = (e: React.MouseEvent) => { - if (!cardRef.current) return + if (isSelectionMode || !cardRef.current) return const rect = cardRef.current.getBoundingClientRect() const centerX = rect.left + rect.width / 2 @@ -487,12 +668,32 @@ const DocumentCard = memo( } return ( -
+
+ {isSelectionMode && canSelect && ( + + )}