diff --git a/e2e/conversation.spec.ts b/e2e/conversation.spec.ts index 8b788ec..d3d342a 100644 --- a/e2e/conversation.spec.ts +++ b/e2e/conversation.spec.ts @@ -122,7 +122,7 @@ test.describe('Conversation Flow', () => { const message = 'Hello. We are testing, just say exactly "Hello world" without anything else.'; // Type a message - await page.getByRole('textbox').fill(message); + await page.getByPlaceholder('Send a message...').fill(message); await page.keyboard.press('Enter'); // Should show the message in the conversation diff --git a/src/components/ConversationList.tsx b/src/components/ConversationList.tsx index f94d521..17dc096 100644 --- a/src/components/ConversationList.tsx +++ b/src/components/ConversationList.tsx @@ -1,4 +1,4 @@ -import { Clock, MessageSquare, Lock, Loader2, Signal } from 'lucide-react'; +import { Clock, MessageSquare, Lock, Loader2, Signal, Trash } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { getRelativeTimeString } from '@/utils/time'; @@ -6,10 +6,11 @@ import { useApi } from '@/contexts/ApiContext'; import { demoConversations } from '@/democonversations'; import type { MessageRole } from '@/types/conversation'; -import type { FC } from 'react'; +import { useState, type FC } from 'react'; import { Computed, use$ } from '@legendapp/state/react'; import { type Observable } from '@legendapp/state'; import { conversations$ } from '@/stores/conversations'; +import { DeleteConversationConfirmationDialog } from './DeleteConversationConfirmationDialog'; type MessageBreakdown = Partial>; @@ -42,6 +43,8 @@ export const ConversationList: FC = ({ }) => { const { isConnected$ } = useApi(); const isConnected = use$(isConnected$); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [conversationToDelete, setConversationToDelete] = useState(null); if (!conversations) { return null; @@ -198,6 +201,23 @@ export const ConversationList: FC = ({ This conversation is read-only )} + + + + + Delete conversation + @@ -209,6 +229,14 @@ export const ConversationList: FC = ({ return (
+ { + setConversationToDelete(null); + }} + /> {isLoading && (
diff --git a/src/components/DeleteConversationConfirmationDialog.tsx b/src/components/DeleteConversationConfirmationDialog.tsx new file mode 100644 index 0000000..882e4b1 --- /dev/null +++ b/src/components/DeleteConversationConfirmationDialog.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Loader2 } from 'lucide-react'; +import { useApi } from '@/contexts/ApiContext'; +import { conversations$, selectedConversation$ } from '@/stores/conversations'; +import { useQueryClient } from '@tanstack/react-query'; +import { use$ } from '@legendapp/state/react'; +import { demoConversations } from '@/democonversations'; + +interface Props { + conversationName: string; + open: boolean; + onOpenChange: (open: boolean) => void; + onDelete: () => void; +} + +export function DeleteConversationConfirmationDialog({ + conversationName, + open, + onOpenChange, + onDelete, +}: Props) { + const { deleteConversation, connectionConfig, isConnected$ } = useApi(); + const queryClient = useQueryClient(); + const isConnected = use$(isConnected$); + const [isDeleting, setIsDeleting] = useState(false); + const [isError, setIsError] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const handleDelete = async () => { + // Show loading indicator + setIsDeleting(true); + + // Delete conversation + try { + await deleteConversation(conversationName); + } catch (error) { + setIsError(true); + if (error instanceof Error) { + setErrorMessage(error.message); + } else { + setErrorMessage('An unknown error occurred'); + } + setIsDeleting(false); + return; + } + conversations$.delete(conversationName); + queryClient.invalidateQueries({ + queryKey: ['conversations', connectionConfig.baseUrl, isConnected], + }); + selectedConversation$.set(demoConversations[0].name); + + // Reset state + await onDelete(); + setIsDeleting(false); + onOpenChange(false); + }; + + return ( + + + + Delete Conversation + + Are you sure you want to delete the conversation {conversationName}? + This action cannot be undone. + + + {isError && ( +
+

{errorMessage}

+
+ )} + + + + +
+
+ ); +} diff --git a/src/contexts/ApiContext.tsx b/src/contexts/ApiContext.tsx index 55001e8..eb30a91 100644 --- a/src/contexts/ApiContext.tsx +++ b/src/contexts/ApiContext.tsx @@ -31,6 +31,7 @@ interface ApiContextType { closeEventStream: ApiClient['closeEventStream']; getChatConfig: ApiClient['getChatConfig']; updateChatConfig: ApiClient['updateChatConfig']; + deleteConversation: ApiClient['deleteConversation']; } const ApiContext = createContext(null); @@ -217,6 +218,7 @@ export function ApiProvider({ closeEventStream: api.closeEventStream.bind(api), getChatConfig: api.getChatConfig.bind(api), updateChatConfig: api.updateChatConfig.bind(api), + deleteConversation: api.deleteConversation.bind(api), }} > {children} diff --git a/src/utils/api.ts b/src/utils/api.ts index 4a177fe..460f811 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -714,6 +714,17 @@ export class ApiClient { } ); } + + async deleteConversation(logfile: string): Promise { + if (!this.isConnected) { + throw new ApiClientError('Not connected to API'); + } + + await this.fetchJson<{ status: string }>(`${this.baseUrl}/api/v2/conversations/${logfile}`, { + method: 'DELETE', + signal: this.controller?.signal, + }); + } } export const createApiClient = (baseUrl?: string, authHeader?: string | null): ApiClient => {