aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2025-09-13 22:27:50 +0000
committerMaheshtheDev <[email protected]>2025-09-13 22:27:50 +0000
commit82eb17363531e75d6153a4d2095874f6d5d19eaf (patch)
treead497e31039eab7fbe0d5414692349bab66f4e45
parentfix: connections activation autumn (#419) (diff)
downloadsupermemory-82eb17363531e75d6153a4d2095874f6d5d19eaf.tar.xz
supermemory-82eb17363531e75d6153a4d2095874f6d5d19eaf.zip
ui: delete document and related memories dialog (#420)
-rw-r--r--apps/web/components/memory-list-view.tsx285
-rw-r--r--packages/lib/queries.ts74
2 files changed, 249 insertions, 110 deletions
diff --git a/apps/web/components/memory-list-view.tsx b/apps/web/components/memory-list-view.tsx
index 2cff96fd..b91e2562 100644
--- a/apps/web/components/memory-list-view.tsx
+++ b/apps/web/components/memory-list-view.tsx
@@ -1,47 +1,56 @@
-"use client";
+"use client"
-import { useIsMobile } from "@hooks/use-mobile";
-import { cn } from "@lib/utils";
-import { Badge } from "@repo/ui/components/badge";
-import { Card, CardContent, CardHeader } from "@repo/ui/components/card";
-import { colors } from "@repo/ui/memory-graph/constants";
-import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
-import { useVirtualizer } from "@tanstack/react-virtual";
+import { useIsMobile } from "@hooks/use-mobile"
+import { cn } from "@lib/utils"
+import { Badge } from "@repo/ui/components/badge"
+import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
import {
- Brain,
- ExternalLink,
- Sparkles,
-} from "lucide-react";
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
-import type { z } from "zod";
-import useResizeObserver from "@/hooks/use-resize-observer";
-import { analytics } from "@/lib/analytics";
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@repo/ui/components/alert-dialog"
+import { colors } from "@repo/ui/memory-graph/constants"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { useVirtualizer } from "@tanstack/react-virtual"
+import { Brain, ExternalLink, Sparkles, Trash2 } from "lucide-react"
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
+import type { z } from "zod"
+import useResizeObserver from "@/hooks/use-resize-observer"
+import { analytics } from "@/lib/analytics"
+import { useDeleteDocument } from "@lib/queries"
+import { useProject } from "@/stores"
-import { MemoryDetail } from "./memories/memory-detail";
-import { getDocumentIcon } from "@/lib/document-icon";
-import { formatDate, getSourceUrl } from "./memories";
+import { MemoryDetail } from "./memories/memory-detail"
+import { getDocumentIcon } from "@/lib/document-icon"
+import { formatDate, getSourceUrl } from "./memories"
-type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
-type DocumentWithMemories = DocumentsResponse["documents"][0];
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
interface MemoryListViewProps {
- children?: React.ReactNode;
- documents: DocumentWithMemories[];
- isLoading: boolean;
- isLoadingMore: boolean;
- error: Error | null;
- totalLoaded: number;
- hasMore: boolean;
- loadMoreDocuments: () => Promise<void>;
+ children?: React.ReactNode
+ documents: DocumentWithMemories[]
+ isLoading: boolean
+ isLoadingMore: boolean
+ error: Error | null
+ totalLoaded: number
+ hasMore: boolean
+ loadMoreDocuments: () => Promise<void>
}
const GreetingMessage = memo(() => {
const getGreeting = () => {
- const hour = new Date().getHours();
- if (hour < 12) return "Good morning";
- if (hour < 17) return "Good afternoon";
- return "Good evening";
- };
+ const hour = new Date().getHours()
+ if (hour < 12) return "Good morning"
+ if (hour < 17) return "Good afternoon"
+ return "Good evening"
+ }
return (
<div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3">
@@ -57,28 +66,30 @@ const GreetingMessage = memo(() => {
</p>
</div>
</div>
- );
-});
+ )
+})
const DocumentCard = memo(
({
document,
onOpenDetails,
+ onDelete,
}: {
- document: DocumentWithMemories;
- onOpenDetails: (document: DocumentWithMemories) => void;
+ document: DocumentWithMemories
+ onOpenDetails: (document: DocumentWithMemories) => void
+ onDelete: (document: DocumentWithMemories) => void
}) => {
- const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten);
+ const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
const forgottenMemories = document.memoryEntries.filter(
(m) => m.isForgotten,
- );
+ )
return (
<Card
className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full"
onClick={() => {
- analytics.documentCardClicked();
- onOpenDetails(document);
+ analytics.documentCardClicked()
+ onOpenDetails(document)
}}
style={{
backgroundColor: colors.document.primary,
@@ -101,9 +112,9 @@ const DocumentCard = memo(
<button
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
onClick={(e) => {
- e.stopPropagation();
- const sourceUrl = getSourceUrl(document);
- window.open(sourceUrl ?? undefined, "_blank");
+ e.stopPropagation()
+ const sourceUrl = getSourceUrl(document)
+ window.open(sourceUrl ?? undefined, "_blank")
}}
style={{
backgroundColor: "rgba(255, 255, 255, 0.05)",
@@ -128,38 +139,84 @@ const DocumentCard = memo(
{document.content}
</p>
)}
- <div className="flex items-center gap-2 flex-wrap">
- {activeMemories.length > 0 && (
- <Badge
- className="text-xs text-accent-foreground"
- style={{
- backgroundColor: colors.memory.secondary,
- }}
- variant="secondary"
- >
- <Brain className="w-3 h-3 mr-1" />
- {activeMemories.length}{" "}
- {activeMemories.length === 1 ? "memory" : "memories"}
- </Badge>
- )}
- {forgottenMemories.length > 0 && (
- <Badge
- className="text-xs"
- style={{
- borderColor: "rgba(255, 255, 255, 0.2)",
- color: colors.text.muted,
- }}
- variant="outline"
- >
- {forgottenMemories.length} forgotten
- </Badge>
- )}
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2 flex-wrap">
+ {activeMemories.length > 0 && (
+ <Badge
+ className="text-xs text-accent-foreground"
+ style={{
+ backgroundColor: colors.memory.secondary,
+ }}
+ variant="secondary"
+ >
+ <Brain className="w-3 h-3 mr-1" />
+ {activeMemories.length}{" "}
+ {activeMemories.length === 1 ? "memory" : "memories"}
+ </Badge>
+ )}
+ {forgottenMemories.length > 0 && (
+ <Badge
+ className="text-xs"
+ style={{
+ borderColor: "rgba(255, 255, 255, 0.2)",
+ color: colors.text.muted,
+ }}
+ variant="outline"
+ >
+ {forgottenMemories.length} forgotten
+ </Badge>
+ )}
+ </div>
+
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <button
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ style={{
+ color: colors.text.muted,
+ }}
+ type="button"
+ >
+ <Trash2 className="w-3.5 h-3.5" />
+ </button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Delete Document</AlertDialogTitle>
+ <AlertDialogDescription>
+ Are you sure you want to delete this document and all its
+ related memories? This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ >
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ className="bg-red-600 hover:bg-red-700 text-white"
+ onClick={(e) => {
+ e.stopPropagation()
+ onDelete(document)
+ }}
+ >
+ Delete
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
</div>
</CardContent>
</Card>
- );
+ )
},
-);
+)
export const MemoryListView = ({
children,
@@ -170,29 +227,38 @@ export const MemoryListView = ({
hasMore,
loadMoreDocuments,
}: MemoryListViewProps) => {
- const [selectedSpace, _] = useState<string>("all");
+ const [selectedSpace, _] = useState<string>("all")
const [selectedDocument, setSelectedDocument] =
- useState<DocumentWithMemories | null>(null);
- const [isDetailOpen, setIsDetailOpen] = useState(false);
- const parentRef = useRef<HTMLDivElement>(null);
- const containerRef = useRef<HTMLDivElement>(null);
- const isMobile = useIsMobile();
+ useState<DocumentWithMemories | null>(null)
+ const [isDetailOpen, setIsDetailOpen] = useState(false)
+ const parentRef = useRef<HTMLDivElement>(null)
+ const containerRef = useRef<HTMLDivElement>(null)
+ const isMobile = useIsMobile()
+ const { selectedProject } = useProject()
+ const deleteDocumentMutation = useDeleteDocument(selectedProject)
- const gap = 14;
+ const gap = 14
- const { width: containerWidth } = useResizeObserver(containerRef);
- const columnWidth = isMobile ? containerWidth : 320;
+ const handleDeleteDocument = useCallback(
+ (document: DocumentWithMemories) => {
+ deleteDocumentMutation.mutate(document.id)
+ },
+ [deleteDocumentMutation],
+ )
+
+ const { width: containerWidth } = useResizeObserver(containerRef)
+ const columnWidth = isMobile ? containerWidth : 320
const columns = Math.max(
1,
Math.floor((containerWidth + gap) / (columnWidth + gap)),
- );
+ )
// Filter documents based on selected space
const filteredDocuments = useMemo(() => {
- if (!documents) return [];
+ if (!documents) return []
if (selectedSpace === "all") {
- return documents;
+ return documents
}
return documents
@@ -203,44 +269,44 @@ export const MemoryListView = ({
(memory.spaceContainerTag ?? memory.spaceId) === selectedSpace,
),
}))
- .filter((doc) => doc.memoryEntries.length > 0);
- }, [documents, selectedSpace]);
+ .filter((doc) => doc.memoryEntries.length > 0)
+ }, [documents, selectedSpace])
const handleOpenDetails = useCallback((document: DocumentWithMemories) => {
- analytics.memoryDetailOpened();
- setSelectedDocument(document);
- setIsDetailOpen(true);
- }, []);
+ analytics.memoryDetailOpened()
+ setSelectedDocument(document)
+ setIsDetailOpen(true)
+ }, [])
const handleCloseDetails = useCallback(() => {
- setIsDetailOpen(false);
- setTimeout(() => setSelectedDocument(null), 300);
- }, []);
+ setIsDetailOpen(false)
+ setTimeout(() => setSelectedDocument(null), 300)
+ }, [])
const virtualItems = useMemo(() => {
- const items = [];
+ const items = []
for (let i = 0; i < filteredDocuments.length; i += columns) {
- items.push(filteredDocuments.slice(i, i + columns));
+ items.push(filteredDocuments.slice(i, i + columns))
}
- return items;
- }, [filteredDocuments, columns]);
+ return items
+ }, [filteredDocuments, columns])
const virtualizer = useVirtualizer({
count: virtualItems.length,
getScrollElement: () => parentRef.current,
overscan: 5,
estimateSize: () => 200,
- });
+ })
useEffect(() => {
- const [lastItem] = [...virtualizer.getVirtualItems()].reverse();
+ const [lastItem] = [...virtualizer.getVirtualItems()].reverse()
if (!lastItem || !hasMore || isLoadingMore) {
- return;
+ return
}
if (lastItem.index >= virtualItems.length - 1) {
- loadMoreDocuments();
+ loadMoreDocuments()
}
}, [
hasMore,
@@ -248,15 +314,15 @@ export const MemoryListView = ({
loadMoreDocuments,
virtualizer.getVirtualItems(),
virtualItems.length,
- ]);
+ ])
// Always render with consistent structure
return (
<>
<div
className="h-full overflow-hidden relative pb-20"
- style={{ backgroundColor: colors.background.primary }}
ref={containerRef}
+ style={{ backgroundColor: colors.background.primary }}
>
{error ? (
<div className="h-full flex items-center justify-center p-4">
@@ -301,8 +367,8 @@ export const MemoryListView = ({
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
- const rowItems = virtualItems[virtualRow.index];
- if (!rowItems) return null;
+ const rowItems = virtualItems[virtualRow.index]
+ if (!rowItems) return null
return (
<div
@@ -326,11 +392,12 @@ export const MemoryListView = ({
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
document={document}
onOpenDetails={handleOpenDetails}
+ onDelete={handleDeleteDocument}
/>
))}
</div>
</div>
- );
+ )
})}
</div>
@@ -355,5 +422,5 @@ export const MemoryListView = ({
isMobile={isMobile}
/>
</>
- );
-};
+ )
+}
diff --git a/packages/lib/queries.ts b/packages/lib/queries.ts
index 36338445..0902f378 100644
--- a/packages/lib/queries.ts
+++ b/packages/lib/queries.ts
@@ -1,5 +1,12 @@
-import { useQuery } from "@tanstack/react-query"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import type { useCustomer } from "autumn-js/react"
+import { $fetch } from "./api"
+import { toast } from "sonner"
+import type { DocumentsWithMemoriesResponseSchema } from "../validation/api"
+import type { z } from "zod"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
export const fetchSubscriptionStatus = (
autumn: ReturnType<typeof useCustomer>,
@@ -84,3 +91,68 @@ export const fetchProProduct = (autumn: ReturnType<typeof useCustomer>) =>
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
})
+
+export const useDeleteDocument = (selectedProject: string) => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async (documentId: string) => {
+ // context for LLM: delete/memories/:documentId is documents delete endpoint not memories delete endpoint
+ const response = await $fetch(`@delete/memories/${documentId}`)
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to delete document")
+ }
+ return response.data
+ },
+ onMutate: async (documentId: string) => {
+ await queryClient.cancelQueries({
+ queryKey: ["documents-with-memories", selectedProject],
+ })
+
+ const previousData = queryClient.getQueryData([
+ "documents-with-memories",
+ selectedProject,
+ ])
+
+ queryClient.setQueryData(
+ ["documents-with-memories", selectedProject],
+ (old: unknown) => {
+ if (!old || typeof old !== "object") return old
+ const typedOld = old as {
+ pages?: Array<{ documents?: DocumentWithMemories[] }>
+ }
+ return {
+ ...typedOld,
+ pages: typedOld.pages?.map((page) => ({
+ ...page,
+ documents: page.documents?.filter(
+ (doc: DocumentWithMemories) => doc.id !== documentId,
+ ),
+ })),
+ }
+ },
+ )
+
+ return { previousData }
+ },
+ onSuccess: () => {
+ toast.success("Memory deleted successfully")
+ },
+ onError: (error, _documentId, context) => {
+ if (context?.previousData) {
+ queryClient.setQueryData(
+ ["documents-with-memories", selectedProject],
+ context.previousData,
+ )
+ }
+ toast.error("Failed to delete memory", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({
+ queryKey: ["documents-with-memories", selectedProject],
+ })
+ },
+ })
+}