diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index aa565ca891..d838520c98 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -23,6 +23,7 @@ from .model import ( ChatSession, append_and_save_message, create_chat_session, + delete_chat_session, get_chat_session, get_user_sessions, ) @@ -211,6 +212,43 @@ async def create_session( ) +@router.delete( + "/sessions/{session_id}", + dependencies=[Security(auth.requires_user)], + status_code=204, + responses={404: {"description": "Session not found or access denied"}}, +) +async def delete_session( + session_id: str, + user_id: Annotated[str, Security(auth.get_user_id)], +) -> Response: + """ + Delete a chat session. + + Permanently removes a chat session and all its messages. + Only the owner can delete their sessions. + + Args: + session_id: The session ID to delete. + user_id: The authenticated user's ID. + + Returns: + 204 No Content on success. + + Raises: + HTTPException: 404 if session not found or not owned by user. + """ + deleted = await delete_chat_session(session_id, user_id) + + if not deleted: + raise HTTPException( + status_code=404, + detail=f"Session {session_id} not found or access denied", + ) + + return Response(status_code=204) + + @router.get( "/sessions/{session_id}", ) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx index 0d403b1a79..35b34890ce 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx @@ -1,6 +1,8 @@ "use client"; import { SidebarProvider } from "@/components/ui/sidebar"; +// TODO: Replace with modern Dialog component when available +import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; @@ -31,6 +33,12 @@ export function CopilotPage() { handleDrawerOpenChange, handleSelectSession, handleNewChat, + // Delete functionality + sessionToDelete, + isDeleting, + handleDeleteClick, + handleConfirmDelete, + handleCancelDelete, } = useCopilotPage(); if (isUserLoading || !isLoggedIn) { @@ -48,7 +56,19 @@ export function CopilotPage() { > {!isMobile && }
- {isMobile && } + {isMobile && ( + { + const session = sessions.find((s) => s.id === sessionId); + if (session) { + handleDeleteClick(session.id, session.title); + } + }} + /> + )}
)} + {/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */} + {isMobile && ( + !open && handleCancelDelete()} + onDoDelete={handleConfirmDelete} + /> + )} ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx index 6b7398b4ba..8e785dd9d3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx @@ -1,8 +1,15 @@ "use client"; -import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + useDeleteV2DeleteSession, + useGetV2ListSessions, +} from "@/app/api/__generated__/endpoints/chat/chat"; import { Button } from "@/components/atoms/Button/Button"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { Text } from "@/components/atoms/Text/Text"; +import { toast } from "@/components/molecules/Toast/use-toast"; +// TODO: Replace with modern Dialog component when available +import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { Sidebar, SidebarContent, @@ -12,18 +19,52 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; -import { PlusCircleIcon, PlusIcon } from "@phosphor-icons/react"; +import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react"; +import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; +import { useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; export function ChatSidebar() { const { state } = useSidebar(); const isCollapsed = state === "collapsed"; const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); + const [sessionToDelete, setSessionToDelete] = useState<{ + id: string; + title: string | null | undefined; + } | null>(null); + + const queryClient = useQueryClient(); const { data: sessionsResponse, isLoading: isLoadingSessions } = useGetV2ListSessions({ limit: 50 }); + const { mutate: deleteSession, isPending: isDeleting } = + useDeleteV2DeleteSession({ + mutation: { + onSuccess: () => { + // Invalidate sessions list to refetch + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + // If we deleted the current session, clear selection + if (sessionToDelete?.id === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + toast({ + title: "Failed to delete chat", + description: + error instanceof Error ? error.message : "An error occurred", + variant: "destructive", + }); + setSessionToDelete(null); + }, + }, + }); + const sessions = sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; @@ -35,6 +76,22 @@ export function ChatSidebar() { setSessionId(id); } + function handleDeleteClick( + e: React.MouseEvent, + id: string, + title: string | null | undefined, + ) { + e.stopPropagation(); // Prevent session selection + if (isDeleting) return; // Prevent double-click during deletion + setSessionToDelete({ id, title }); + } + + function handleConfirmDelete() { + if (sessionToDelete) { + deleteSession({ sessionId: sessionToDelete.id }); + } + } + function formatDate(dateString: string) { const date = new Date(dateString); const now = new Date(); @@ -61,128 +118,152 @@ export function ChatSidebar() { } return ( - - {isCollapsed && ( - - -
- - -
-
-
- )} - - {!isCollapsed && ( - - - Your chats - -
- -
-
- )} - - {!isCollapsed && ( - - {isLoadingSessions ? ( -
- -
- ) : sessions.length === 0 ? ( -

- No conversations yet -

- ) : ( - sessions.map((session) => ( - - )) + <> + + {isCollapsed && ( + - )} -
- {!isCollapsed && sessionId && ( - - - - - - )} -
+
+ + +
+ + + )} + + {!isCollapsed && ( + + + Your chats + +
+ +
+
+ )} + + {!isCollapsed && ( + + {isLoadingSessions ? ( +
+ +
+ ) : sessions.length === 0 ? ( +

+ No conversations yet +

+ ) : ( + sessions.map((session) => ( +
+ + +
+ )) + )} +
+ )} +
+ {!isCollapsed && sessionId && ( + + + + + + )} + + + !open && setSessionToDelete(null)} + onDoDelete={handleConfirmDelete} + /> + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx index e0d6161744..b4b7636c81 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx @@ -1,22 +1,46 @@ import { Button } from "@/components/atoms/Button/Button"; import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; -import { ListIcon } from "@phosphor-icons/react"; +import { ListIcon, TrashIcon } from "@phosphor-icons/react"; interface Props { onOpenDrawer: () => void; + showDelete?: boolean; + isDeleting?: boolean; + onDelete?: () => void; } -export function MobileHeader({ onOpenDrawer }: Props) { +export function MobileHeader({ + onOpenDrawer, + showDelete, + isDeleting, + onDelete, +}: Props) { return ( - + + {showDelete && onDelete && ( + + )} +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index 28e9ba7cfb..444e745ec6 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -1,10 +1,15 @@ -import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + useDeleteV2DeleteSession, + useGetV2ListSessions, +} from "@/app/api/__generated__/endpoints/chat/chat"; import { toast } from "@/components/molecules/Toast/use-toast"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useChat } from "@ai-sdk/react"; +import { useQueryClient } from "@tanstack/react-query"; import { DefaultChatTransport } from "ai"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useChatSession } from "./useChatSession"; import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling"; @@ -14,6 +19,11 @@ export function useCopilotPage() { const { isUserLoading, isLoggedIn } = useSupabase(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [pendingMessage, setPendingMessage] = useState(null); + const [sessionToDelete, setSessionToDelete] = useState<{ + id: string; + title: string | null | undefined; + } | null>(null); + const queryClient = useQueryClient(); const { sessionId, @@ -24,6 +34,30 @@ export function useCopilotPage() { isCreatingSession, } = useChatSession(); + const { mutate: deleteSessionMutation, isPending: isDeleting } = + useDeleteV2DeleteSession({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + if (sessionToDelete?.id === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + toast({ + title: "Failed to delete chat", + description: + error instanceof Error ? error.message : "An error occurred", + variant: "destructive", + }); + setSessionToDelete(null); + }, + }, + }); + const breakpoint = useBreakpoint(); const isMobile = breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; @@ -143,6 +177,24 @@ export function useCopilotPage() { if (isMobile) setIsDrawerOpen(false); } + const handleDeleteClick = useCallback( + (id: string, title: string | null | undefined) => { + if (isDeleting) return; + setSessionToDelete({ id, title }); + }, + [isDeleting], + ); + + const handleConfirmDelete = useCallback(() => { + if (sessionToDelete) { + deleteSessionMutation({ sessionId: sessionToDelete.id }); + } + }, [sessionToDelete, deleteSessionMutation]); + + const handleCancelDelete = useCallback(() => { + setSessionToDelete(null); + }, []); + return { sessionId, messages, @@ -165,5 +217,11 @@ export function useCopilotPage() { handleDrawerOpenChange, handleSelectSession, handleNewChat, + // Delete functionality + sessionToDelete, + isDeleting, + handleDeleteClick, + handleConfirmDelete, + handleCancelDelete, }; } diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 63a8a856b9..feabc9b51d 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -1151,6 +1151,36 @@ } }, "/api/chat/sessions/{session_id}": { + "delete": { + "tags": ["v2", "chat", "chat"], + "summary": "Delete Session", + "description": "Delete a chat session.\n\nPermanently removes a chat session and all its messages.\nOnly the owner can delete their sessions.\n\nArgs:\n session_id: The session ID to delete.\n user_id: The authenticated user's ID.\n\nReturns:\n 204 No Content on success.\n\nRaises:\n HTTPException: 404 if session not found or not owned by user.", + "operationId": "deleteV2DeleteSession", + "security": [{ "HTTPBearerJWT": [] }], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { "type": "string", "title": "Session Id" } + } + ], + "responses": { + "204": { "description": "Successful Response" }, + "401": { + "$ref": "#/components/responses/HTTP401NotAuthenticatedError" + }, + "404": { "description": "Session not found or access denied" }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + }, "get": { "tags": ["v2", "chat", "chat"], "summary": "Get Session", diff --git a/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx b/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx index 4ce998b6f6..10af4fa3c8 100644 --- a/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx +++ b/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx @@ -115,7 +115,7 @@ const DialogFooter = ({ }: React.HTMLAttributes) => (