Skip to content

Commit 24751b0

Browse files
authored
feat: add button to delete conversations (#55)
* feat: add button to delete conversations * fix: return on api conversation delete failure * fix: typo in filename * fix: use button and stop click propagation * fix: e2e test not passing
1 parent 7023116 commit 24751b0

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

e2e/conversation.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ test.describe('Conversation Flow', () => {
122122
const message = 'Hello. We are testing, just say exactly "Hello world" without anything else.';
123123

124124
// Type a message
125-
await page.getByRole('textbox').fill(message);
125+
await page.getByPlaceholder('Send a message...').fill(message);
126126
await page.keyboard.press('Enter');
127127

128128
// Should show the message in the conversation

src/components/ConversationList.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { Clock, MessageSquare, Lock, Loader2, Signal } from 'lucide-react';
1+
import { Clock, MessageSquare, Lock, Loader2, Signal, Trash } from 'lucide-react';
22
import { Button } from '@/components/ui/button';
33
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
44
import { getRelativeTimeString } from '@/utils/time';
55
import { useApi } from '@/contexts/ApiContext';
66
import { demoConversations } from '@/democonversations';
77

88
import type { MessageRole } from '@/types/conversation';
9-
import type { FC } from 'react';
9+
import { useState, type FC } from 'react';
1010
import { Computed, use$ } from '@legendapp/state/react';
1111
import { type Observable } from '@legendapp/state';
1212
import { conversations$ } from '@/stores/conversations';
13+
import { DeleteConversationConfirmationDialog } from './DeleteConversationConfirmationDialog';
1314

1415
type MessageBreakdown = Partial<Record<MessageRole, number>>;
1516

@@ -42,6 +43,8 @@ export const ConversationList: FC<Props> = ({
4243
}) => {
4344
const { isConnected$ } = useApi();
4445
const isConnected = use$(isConnected$);
46+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
47+
const [conversationToDelete, setConversationToDelete] = useState<string | null>(null);
4548

4649
if (!conversations) {
4750
return null;
@@ -198,6 +201,23 @@ export const ConversationList: FC<Props> = ({
198201
<TooltipContent>This conversation is read-only</TooltipContent>
199202
</Tooltip>
200203
)}
204+
<Tooltip>
205+
<TooltipTrigger asChild>
206+
<button
207+
type="button"
208+
aria-label="Delete conversation"
209+
className="flex items-center"
210+
onClick={(e) => {
211+
e.stopPropagation();
212+
setConversationToDelete(conv.name);
213+
setDeleteDialogOpen(true);
214+
}}
215+
>
216+
<Trash className="h-4 w-4" />
217+
</button>
218+
</TooltipTrigger>
219+
<TooltipContent>Delete conversation</TooltipContent>
220+
</Tooltip>
201221
</div>
202222
</div>
203223
</div>
@@ -209,6 +229,14 @@ export const ConversationList: FC<Props> = ({
209229

210230
return (
211231
<div data-testid="conversation-list" className="h-full space-y-2 overflow-y-auto p-4">
232+
<DeleteConversationConfirmationDialog
233+
conversationName={conversationToDelete ?? ''}
234+
open={deleteDialogOpen}
235+
onOpenChange={setDeleteDialogOpen}
236+
onDelete={() => {
237+
setConversationToDelete(null);
238+
}}
239+
/>
212240
{isLoading && (
213241
<div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
214242
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useState } from 'react';
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogDescription,
6+
DialogFooter,
7+
DialogHeader,
8+
DialogTitle,
9+
} from '@/components/ui/dialog';
10+
import { Button } from '@/components/ui/button';
11+
import { Loader2 } from 'lucide-react';
12+
import { useApi } from '@/contexts/ApiContext';
13+
import { conversations$, selectedConversation$ } from '@/stores/conversations';
14+
import { useQueryClient } from '@tanstack/react-query';
15+
import { use$ } from '@legendapp/state/react';
16+
import { demoConversations } from '@/democonversations';
17+
18+
interface Props {
19+
conversationName: string;
20+
open: boolean;
21+
onOpenChange: (open: boolean) => void;
22+
onDelete: () => void;
23+
}
24+
25+
export function DeleteConversationConfirmationDialog({
26+
conversationName,
27+
open,
28+
onOpenChange,
29+
onDelete,
30+
}: Props) {
31+
const { deleteConversation, connectionConfig, isConnected$ } = useApi();
32+
const queryClient = useQueryClient();
33+
const isConnected = use$(isConnected$);
34+
const [isDeleting, setIsDeleting] = useState(false);
35+
const [isError, setIsError] = useState(false);
36+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
37+
38+
const handleDelete = async () => {
39+
// Show loading indicator
40+
setIsDeleting(true);
41+
42+
// Delete conversation
43+
try {
44+
await deleteConversation(conversationName);
45+
} catch (error) {
46+
setIsError(true);
47+
if (error instanceof Error) {
48+
setErrorMessage(error.message);
49+
} else {
50+
setErrorMessage('An unknown error occurred');
51+
}
52+
setIsDeleting(false);
53+
return;
54+
}
55+
conversations$.delete(conversationName);
56+
queryClient.invalidateQueries({
57+
queryKey: ['conversations', connectionConfig.baseUrl, isConnected],
58+
});
59+
selectedConversation$.set(demoConversations[0].name);
60+
61+
// Reset state
62+
await onDelete();
63+
setIsDeleting(false);
64+
onOpenChange(false);
65+
};
66+
67+
return (
68+
<Dialog open={open} onOpenChange={onOpenChange}>
69+
<DialogContent>
70+
<DialogHeader>
71+
<DialogTitle>Delete Conversation</DialogTitle>
72+
<DialogDescription>
73+
Are you sure you want to delete the conversation <strong>{conversationName}</strong>?
74+
This action cannot be undone.
75+
</DialogDescription>
76+
</DialogHeader>
77+
{isError && (
78+
<div className="mb-4 rounded-md bg-destructive/10 p-4 text-sm text-destructive">
79+
<p>{errorMessage}</p>
80+
</div>
81+
)}
82+
<DialogFooter>
83+
<Button variant="outline" onClick={() => onOpenChange(false)}>
84+
Cancel
85+
</Button>
86+
<Button variant="destructive" onClick={handleDelete} disabled={isDeleting}>
87+
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
88+
Delete
89+
</Button>
90+
</DialogFooter>
91+
</DialogContent>
92+
</Dialog>
93+
);
94+
}

src/contexts/ApiContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface ApiContextType {
3131
closeEventStream: ApiClient['closeEventStream'];
3232
getChatConfig: ApiClient['getChatConfig'];
3333
updateChatConfig: ApiClient['updateChatConfig'];
34+
deleteConversation: ApiClient['deleteConversation'];
3435
}
3536

3637
const ApiContext = createContext<ApiContextType | null>(null);
@@ -217,6 +218,7 @@ export function ApiProvider({
217218
closeEventStream: api.closeEventStream.bind(api),
218219
getChatConfig: api.getChatConfig.bind(api),
219220
updateChatConfig: api.updateChatConfig.bind(api),
221+
deleteConversation: api.deleteConversation.bind(api),
220222
}}
221223
>
222224
{children}

src/utils/api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,17 @@ export class ApiClient {
714714
}
715715
);
716716
}
717+
718+
async deleteConversation(logfile: string): Promise<void> {
719+
if (!this.isConnected) {
720+
throw new ApiClientError('Not connected to API');
721+
}
722+
723+
await this.fetchJson<{ status: string }>(`${this.baseUrl}/api/v2/conversations/${logfile}`, {
724+
method: 'DELETE',
725+
signal: this.controller?.signal,
726+
});
727+
}
717728
}
718729

719730
export const createApiClient = (baseUrl?: string, authHeader?: string | null): ApiClient => {

0 commit comments

Comments
 (0)