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;
-type DocumentWithMemories = DocumentsResponse["documents"][0];
+type DocumentsResponse = z.infer
+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;
+ children?: React.ReactNode
+ documents: DocumentWithMemories[]
+ isLoading: boolean
+ isLoadingMore: boolean
+ error: Error | null
+ totalLoaded: number
+ hasMore: boolean
+ loadMoreDocuments: () => Promise
}
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 (
@@ -57,28 +66,30 @@ const GreetingMessage = memo(() => {
- );
-});
+ )
+})
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 (
{
- analytics.documentCardClicked();
- onOpenDetails(document);
+ analytics.documentCardClicked()
+ onOpenDetails(document)
}}
style={{
backgroundColor: colors.document.primary,
@@ -101,9 +112,9 @@ const DocumentCard = memo(
)}
-
- {activeMemories.length > 0 && (
-
-
- {activeMemories.length}{" "}
- {activeMemories.length === 1 ? "memory" : "memories"}
-
- )}
- {forgottenMemories.length > 0 && (
-
- {forgottenMemories.length} forgotten
-
- )}
+
+
+ {activeMemories.length > 0 && (
+
+
+ {activeMemories.length}{" "}
+ {activeMemories.length === 1 ? "memory" : "memories"}
+
+ )}
+ {forgottenMemories.length > 0 && (
+
+ {forgottenMemories.length} forgotten
+
+ )}
+
+
+
+
+
+
+
+
+ Delete Document
+
+ Are you sure you want to delete this document and all its
+ related memories? This action cannot be undone.
+
+
+
+ {
+ e.stopPropagation()
+ }}
+ >
+ Cancel
+
+ {
+ e.stopPropagation()
+ onDelete(document)
+ }}
+ >
+ Delete
+
+
+
+
- );
+ )
},
-);
+)
export const MemoryListView = ({
children,
@@ -170,29 +227,38 @@ export const MemoryListView = ({
hasMore,
loadMoreDocuments,
}: MemoryListViewProps) => {
- const [selectedSpace, _] = useState
("all");
+ const [selectedSpace, _] = useState("all")
const [selectedDocument, setSelectedDocument] =
- useState(null);
- const [isDetailOpen, setIsDetailOpen] = useState(false);
- const parentRef = useRef(null);
- const containerRef = useRef(null);
- const isMobile = useIsMobile();
+ useState(null)
+ const [isDetailOpen, setIsDetailOpen] = useState(false)
+ const parentRef = useRef(null)
+ const containerRef = useRef(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 (
<>
{error ? (
@@ -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 (
))}
- );
+ )
})}
@@ -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
+type DocumentWithMemories = DocumentsResponse["documents"][0]
export const fetchSubscriptionStatus = (
autumn: ReturnType,
@@ -84,3 +91,68 @@ export const fetchProProduct = (autumn: ReturnType) =>
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],
+ })
+ },
+ })
+}