From f397780cf14fdf9d324472401267f11f44e025bc Mon Sep 17 00:00:00 2001 From: tehoang Date: Wed, 16 Aug 2023 17:27:14 -0400 Subject: [PATCH 01/15] Delete Chat --- webapi/Controllers/ChatHistoryController.cs | 81 +++++++++++++++++ webapi/Models/Request/DeleteChatRequest.cs | 23 +++++ webapp/src/Constants.ts | 4 + webapp/src/components/chat/ChatInput.tsx | 8 +- webapp/src/components/chat/ChatWindow.tsx | 5 +- .../chat/chat-list/ChatListSection.tsx | 17 +--- .../chat/chat-list/ListItemActions.tsx | 89 +++++++++++-------- .../chat-list/dialogs/DeleteChatDialog.tsx | 23 ++--- .../chat/persona/MemoryBiasSlider.tsx | 1 + .../components/chat/persona/PromptEditor.tsx | 8 +- .../src/components/chat/tabs/DocumentsTab.tsx | 47 +++++----- .../src/components/chat/tabs/PersonaTab.tsx | 18 ++-- webapp/src/components/views/ChatView.tsx | 5 +- webapp/src/libs/hooks/useChat.ts | 47 +++++++++- webapp/src/libs/services/BaseService.ts | 2 +- webapp/src/libs/services/ChatService.ts | 16 ++++ webapp/src/redux/features/app/AppState.ts | 4 +- .../redux/features/conversations/ChatState.ts | 1 + .../conversations/conversationsSlice.ts | 24 +++++ .../message-relay/signalRMiddleware.ts | 34 ++++++- 20 files changed, 351 insertions(+), 106 deletions(-) create mode 100644 webapi/Models/Request/DeleteChatRequest.cs diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 621704d1c..1db371f3a 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -37,6 +37,7 @@ public class ChatHistoryController : ControllerBase private readonly ChatMemorySourceRepository _sourceRepository; private readonly PromptsOptions _promptOptions; private const string ChatEditedClientCall = "ChatEdited"; + private const string ChatDeletedClientCall = "ChatDeleted"; /// /// Initializes a new instance of the class. @@ -143,6 +144,7 @@ public async Task GetAllChatSessionsAsync(string userId) foreach (var chatParticipant in chatParticipants) { ChatSession? chat = null; + // TODO: TryFindByIdAsync should check for deletion if (await this._sessionRepository.TryFindByIdAsync(chatParticipant.ChatId, v => chat = v)) { chats.Add(chat!); @@ -240,4 +242,83 @@ public async Task>> GetSourcesAsync( return this.NotFound($"No chat session found for chat id '{chatId}'."); } + + /// + /// Delete a chat session. + /// + /// Object that contains the parameters to delete the chat. + [HttpPost] + [Route("chatSession/delete")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteChatSessionAsync([FromServices] IHubContext messageRelayHubContext, [FromBody] DeleteChatRequest requestParamaters) + { + string? chatId = requestParamaters.ChatId; + string? userId = requestParamaters.UserId; + + if (chatId == null || userId == null) + { + return this.BadRequest("Chat session parameters cannot be null."); + } + + ChatSession? chatToDelete = null; + try + { + // Make sure the chat session exists + chatToDelete = await this._sessionRepository.FindByIdAsync(chatId); + } + catch (KeyNotFoundException) + { + return this.NotFound($"No chat session found for chat id '{chatId}'."); + } + + // Delete message and broadcast update to all participants. + await this._sessionRepository.DeleteAsync(chatToDelete); + await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, userId); + + // Create and store the tasks for deleting all users tied to the chat. + var participants = await this._participantRepository.FindByChatIdAsync(chatId); + var participantsTasks = new List(); + foreach (var participant in participants) + { + participantsTasks.Add(this._participantRepository.DeleteAsync(participant)); + } + + // Create and store the tasks for deleting chat messages. + var messages = await this._messageRepository.FindByChatIdAsync(chatId); + var messageTasks = new List(); + foreach (var message in messages) + { + messageTasks.Add(this._messageRepository.DeleteAsync(message)); + } + + // Create and store the tasks for deleting memory sources. + var sources = await this._sourceRepository.FindByChatIdAsync(chatId, false); + var sourceTasks = new List(); + foreach (var source in sources) + { + sourceTasks.Add(this._sourceRepository.DeleteAsync(source)); + } + + // Await all the tasks in parallel and handle the exceptions + try + { + await Task.WhenAll(participantsTasks.Concat(messageTasks).Concat(sourceTasks)); + } + catch (Exception) + { + // Iterate over the tasks and check their status and exception + foreach (var task in messageTasks.Concat(sourceTasks)) + { + if (task.IsFaulted && task.Exception != null) + { + // Handle the exception as needed + this._logger.LogInformation("Failed to delete an entity of chat {0}: {1}", chatId, task.Exception.Message); + } + } + } + + return this.NoContent(); + } } diff --git a/webapi/Models/Request/DeleteChatRequest.cs b/webapi/Models/Request/DeleteChatRequest.cs new file mode 100644 index 000000000..825ce6ccd --- /dev/null +++ b/webapi/Models/Request/DeleteChatRequest.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Json body for deleting a chat session. +/// +public class DeleteChatRequest +{ + /// + /// Id of the user who sent this message. + /// + [JsonPropertyName("userId")] + public string? UserId { get; set; } + + /// + /// Id of the chat to delete. + /// + [JsonPropertyName("chatId")] + public string? ChatId { get; set; } +} \ No newline at end of file diff --git a/webapp/src/Constants.ts b/webapp/src/Constants.ts index 73db5cfde..c51abcafa 100644 --- a/webapp/src/Constants.ts +++ b/webapp/src/Constants.ts @@ -55,4 +55,8 @@ export const Constants = { }, KEYSTROKE_DEBOUNCE_TIME_MS: 250, STEPWISE_RESULT_NOT_FOUND_REGEX: /(Result not found, review _stepsTaken to see what happened\.)\s+(\[{.*}])/g, + CHAT_DELETED_MESSAGE: (chatName?: string) => + `Chat ${ + chatName ? `{${chatName}} ` : '' + }has been deleted by another user. Please save any resources you need and refresh the page.`, }; diff --git a/webapp/src/components/chat/ChatInput.tsx b/webapp/src/components/chat/ChatInput.tsx index 15c387097..ce261a518 100644 --- a/webapp/src/components/chat/ChatInput.tsx +++ b/webapp/src/components/chat/ChatInput.tsx @@ -113,7 +113,7 @@ export const ChatInput: React.FC = ({ isDraggingOver, onDragLeav React.useEffect(() => { const chatState = conversations[selectedId]; - setValue(chatState.input); + setValue(chatState.disabled ? Constants.CHAT_DELETED_MESSAGE() : chatState.input); }, [conversations, selectedId]); const handleSpeech = () => { @@ -188,6 +188,7 @@ export const ChatInput: React.FC = ({ isDraggingOver, onDragLeav ref={textAreaRef} id="chat-input" resize="vertical" + disabled={conversations[selectedId].disabled} textarea={{ className: isDraggingOver ? mergeClasses(classes.dragAndDrop, classes.textarea) @@ -243,7 +244,7 @@ export const ChatInput: React.FC = ({ isDraggingOver, onDragLeav }} /> - diff --git a/webapp/src/components/chat/persona/MemoryBiasSlider.tsx b/webapp/src/components/chat/persona/MemoryBiasSlider.tsx index 1488916e2..04e6c2ad7 100644 --- a/webapp/src/components/chat/persona/MemoryBiasSlider.tsx +++ b/webapp/src/components/chat/persona/MemoryBiasSlider.tsx @@ -90,6 +90,7 @@ export const MemoryBiasSlider: React.FC = () => { onChange={(_, data) => { sliderValueChange(data.value); }} + disabled={conversations[selectedId].disabled} /> diff --git a/webapp/src/components/chat/persona/PromptEditor.tsx b/webapp/src/components/chat/persona/PromptEditor.tsx index 0d77a66d2..41b367a71 100644 --- a/webapp/src/components/chat/persona/PromptEditor.tsx +++ b/webapp/src/components/chat/persona/PromptEditor.tsx @@ -12,7 +12,7 @@ import { } from '@fluentui/react-components'; import React from 'react'; import { AlertType } from '../../../libs/models/AlertType'; -import { useAppDispatch } from '../../../redux/app/hooks'; +import { useAppDispatch, useAppSelector } from '../../../redux/app/hooks'; import { addAlert } from '../../../redux/features/app/appSlice'; import { Info16 } from '../../shared/BundledIcons'; @@ -60,6 +60,7 @@ export const PromptEditor: React.FC = ({ const classes = useClasses(); const dispatch = useAppDispatch(); const [value, setValue] = React.useState(prompt); + const { conversations } = useAppSelector((state) => state.conversations); React.useEffect(() => { // Taking a dependency on the chatId because the value state needs @@ -106,7 +107,10 @@ export const PromptEditor: React.FC = ({ /> {isEditable && (
-
diff --git a/webapp/src/components/chat/tabs/DocumentsTab.tsx b/webapp/src/components/chat/tabs/DocumentsTab.tsx index fd0afefd3..8d4469565 100644 --- a/webapp/src/components/chat/tabs/DocumentsTab.tsx +++ b/webapp/src/components/chat/tabs/DocumentsTab.tsx @@ -95,25 +95,26 @@ export const DocumentsTab: React.FC = () => { const documentFileRef = useRef(null); React.useEffect(() => { - const importingResources = importingDocuments - ? importingDocuments.map((document, index) => { - return { - id: `in-progress-${index}`, - chatId: selectedId, - sourceType: 'N/A', - name: document, - sharedBy: 'N/A', - createdOn: 0, - tokens: 0, - } as ChatMemorySource; - }) - : []; - setResources(importingResources); - - void chat.getChatMemorySources(selectedId).then((sources) => { - setResources([...importingResources, ...sources]); - }); + if (!conversations[selectedId].disabled) { + const importingResources = importingDocuments + ? importingDocuments.map((document, index) => { + return { + id: `in-progress-${index}`, + chatId: selectedId, + sourceType: 'N/A', + name: document, + sharedBy: 'N/A', + createdOn: 0, + tokens: 0, + } as ChatMemorySource; + }) + : []; + setResources(importingResources); + void chat.getChatMemorySources(selectedId).then((sources) => { + setResources([...importingResources, ...sources]); + }); + } // We don't want to have chat as one of the dependencies as it will cause infinite loop. // eslint-disable-next-line react-hooks/exhaustive-deps }, [importingDocuments, selectedId]); @@ -171,7 +172,9 @@ export const DocumentsTab: React.FC = () => {
[JsonPropertyName("chatId")] public string? ChatId { get; set; } -} \ No newline at end of file +} From 854703cb288e1ad886714a9caa0158ab9bfd2ce9 Mon Sep 17 00:00:00 2001 From: tehoang Date: Thu, 17 Aug 2023 14:29:04 -0400 Subject: [PATCH 04/15] Returning 424 on resource delete error --- webapi/Controllers/ChatHistoryController.cs | 56 +++++++++++++++------ webapi/CopilotChatWebApi.csproj | 24 +++------ webapi/Extensions/ServiceExtensions.cs | 1 + webapi/Models/Request/DeleteChatRequest.cs | 6 --- webapp/src/libs/hooks/useChat.ts | 6 ++- webapp/src/libs/services/ChatService.ts | 11 ++-- 6 files changed, 59 insertions(+), 45 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 4070c0553..1711fd7e9 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -245,20 +245,21 @@ public async Task>> GetSourcesAsync( /// /// Delete a chat session. /// - /// Object that contains the parameters to delete the chat. - [HttpPost] - [Route("chatSession/delete")] + /// Unique Id of user who initiated the request. + /// The chat id. + [HttpDelete] + [Route("chatSession/{sessionId:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteChatSessionAsync([FromServices] IHubContext messageRelayHubContext, [FromBody] DeleteChatRequest requestParameters) + public async Task DeleteChatSessionAsync([FromServices] IHubContext messageRelayHubContext, [FromBody] DeleteChatRequest requestBody, Guid sessionId) { - string? chatId = requestParameters.ChatId; - string? userId = requestParameters.UserId; + var chatId = sessionId.ToString(); + var userId = requestBody.UserId; - if (chatId == null || userId == null) + if (userId == null) { - return this.BadRequest("Chat session parameters cannot be null."); + return this.BadRequest("UserId cannot be null."); } ChatSession? chatToDelete = null; @@ -276,43 +277,66 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte await this._sessionRepository.DeleteAsync(chatToDelete); await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, userId); + var deleteResourcesResult = await this.DeleteChatResourcesAsync(messageRelayHubContext, sessionId) as StatusCodeResult; + if (deleteResourcesResult?.StatusCode != 204) + { + return this.StatusCode(424, $"Failed to delete resources for chat id '{chatId}'."); + } + + return this.NoContent(); + } + + /// + /// Deletes all associated resources (messages, memories, participants) associated with a chat session. + /// + /// The chat id. + /// Tasks defining the items to be deleted. If this is null or empty, all items will be deleted. + [HttpDelete] + [Route("chatSession/{sessionId:guid}/resources")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteChatResourcesAsync([FromServices] IHubContext messageRelayHubContext, Guid sessionId) + { + var chatId = sessionId.ToString(); + var cleanupTasks = new List(); + // Create and store the tasks for deleting all users tied to the chat. var participants = await this._participantRepository.FindByChatIdAsync(chatId); - var participantsTasks = new List(); foreach (var participant in participants) { - participantsTasks.Add(this._participantRepository.DeleteAsync(participant)); + cleanupTasks.Add(this._participantRepository.DeleteAsync(participant)); } // Create and store the tasks for deleting chat messages. var messages = await this._messageRepository.FindByChatIdAsync(chatId); - var messageTasks = new List(); foreach (var message in messages) { - messageTasks.Add(this._messageRepository.DeleteAsync(message)); + cleanupTasks.Add(this._messageRepository.DeleteAsync(message)); } // Create and store the tasks for deleting memory sources. var sources = await this._sourceRepository.FindByChatIdAsync(chatId, false); - var sourceTasks = new List(); foreach (var source in sources) { - sourceTasks.Add(this._sourceRepository.DeleteAsync(source)); + cleanupTasks.Add(this._sourceRepository.DeleteAsync(source)); } // Await all the tasks in parallel and handle the exceptions - var cleanupTasks = participantsTasks.Concat(messageTasks).Concat(sourceTasks); await Task.WhenAll(cleanupTasks); + // var failedTasks = false; // Iterate over the tasks and check their status and exception foreach (var task in cleanupTasks) { if (task.IsFaulted && task.Exception != null) { + // failedTasks = true; this._logger.LogInformation("Failed to delete an entity of chat {0}: {1}", chatId, task.Exception.Message); } } - return this.NoContent(); + return this.StatusCode(500); + + // return failedTasks ? this.StatusCode(500) : this.NoContent(); } } diff --git a/webapi/CopilotChatWebApi.csproj b/webapi/CopilotChatWebApi.csproj index 8fc3cc647..e84a17bba 100644 --- a/webapi/CopilotChatWebApi.csproj +++ b/webapi/CopilotChatWebApi.csproj @@ -13,22 +13,14 @@ - - - - - - - - + + + + + + + + diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index e3a2fd966..8640649f8 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -160,6 +160,7 @@ internal static IServiceCollection AddCorsPolicy(this IServiceCollection service policy => { policy.WithOrigins(allowedOrigins) + .AllowAnyMethod() .AllowAnyHeader(); }); }); diff --git a/webapi/Models/Request/DeleteChatRequest.cs b/webapi/Models/Request/DeleteChatRequest.cs index 6e35cecbc..4cdaa3e8c 100644 --- a/webapi/Models/Request/DeleteChatRequest.cs +++ b/webapi/Models/Request/DeleteChatRequest.cs @@ -14,10 +14,4 @@ public class DeleteChatRequest /// [JsonPropertyName("userId")] public string? UserId { get; set; } - - /// - /// Id of the chat to delete. - /// - [JsonPropertyName("chatId")] - public string? ChatId { get; set; } } diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index 8fce86d48..38720e3a4 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -356,7 +356,11 @@ export const useChat = () => { await createChat(); } } catch (e: any) { - const errorMessage = `Unable to delete chat. Details: ${(e as Error).message}`; + let errorMessage = `Unable to delete chat. Details: ${(e as Error).message}`; + if ((e as Error).message.includes('Error: 424')) { + errorMessage = "Chat was deleted, but some or all resources couldn't be deleted. Please try again."; + } + dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); } }; diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 6fe45f733..97ef51cdc 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -103,14 +103,13 @@ export class ChatService extends BaseService { return result; }; - public deleteChatAsync = async (chatId: string, userId: string, accessToken: string): Promise => { - const result = await this.getResponseAsync( + public deleteChatAsync = async (chatId: string, userId: string, accessToken: string): Promise => { + const result = await this.getResponseAsync( { - commandPath: 'chatSession/delete', - method: 'POST', + commandPath: 'chatSession/' + chatId, + method: 'DELETE', body: { - userId: userId, - chatId: chatId, + userId, }, }, accessToken, From cf2f47663f670658ff96be94695b781b8626c456 Mon Sep 17 00:00:00 2001 From: tehoang Date: Fri, 18 Aug 2023 11:08:16 -0400 Subject: [PATCH 05/15] Adding retry on delete if resource deletion failed --- webapi/Controllers/ChatHistoryController.cs | 20 ++++----- webapp/src/components/shared/Alerts.tsx | 34 ++++++++++----- webapp/src/libs/hooks/useChat.ts | 43 ++++++++++--------- webapp/src/libs/services/ChatService.ts | 14 +++++- webapp/src/redux/features/app/AppState.ts | 1 + .../message-relay/signalRHubConnection.ts | 9 ++-- 6 files changed, 75 insertions(+), 46 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 1711fd7e9..731239017 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -273,16 +273,17 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte return this.NotFound($"No chat session found for chat id '{chatId}'."); } - // Delete message and broadcast update to all participants. - await this._sessionRepository.DeleteAsync(chatToDelete); - await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, userId); - + // Delete any resources associated with the chat session. var deleteResourcesResult = await this.DeleteChatResourcesAsync(messageRelayHubContext, sessionId) as StatusCodeResult; if (deleteResourcesResult?.StatusCode != 204) { return this.StatusCode(424, $"Failed to delete resources for chat id '{chatId}'."); } + // Delete chat session and broadcast update to all participants. + await this._sessionRepository.DeleteAsync(chatToDelete); + await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, userId); + return this.NoContent(); } @@ -290,12 +291,11 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte /// Deletes all associated resources (messages, memories, participants) associated with a chat session. /// /// The chat id. - /// Tasks defining the items to be deleted. If this is null or empty, all items will be deleted. [HttpDelete] [Route("chatSession/{sessionId:guid}/resources")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task DeleteChatResourcesAsync([FromServices] IHubContext messageRelayHubContext, Guid sessionId) + private async Task DeleteChatResourcesAsync([FromServices] IHubContext messageRelayHubContext, Guid sessionId) { var chatId = sessionId.ToString(); var cleanupTasks = new List(); @@ -323,20 +323,18 @@ public async Task DeleteChatResourcesAsync([FromServices] IHubCon // Await all the tasks in parallel and handle the exceptions await Task.WhenAll(cleanupTasks); - // var failedTasks = false; + var failedTasks = false; // Iterate over the tasks and check their status and exception foreach (var task in cleanupTasks) { if (task.IsFaulted && task.Exception != null) { - // failedTasks = true; + failedTasks = true; this._logger.LogInformation("Failed to delete an entity of chat {0}: {1}", chatId, task.Exception.Message); } } - return this.StatusCode(500); - - // return failedTasks ? this.StatusCode(500) : this.NoContent(); + return failedTasks ? this.StatusCode(500) : this.NoContent(); } } diff --git a/webapp/src/components/shared/Alerts.tsx b/webapp/src/components/shared/Alerts.tsx index eb0386dee..6784cd558 100644 --- a/webapp/src/components/shared/Alerts.tsx +++ b/webapp/src/components/shared/Alerts.tsx @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -import { makeStyles, tokens } from '@fluentui/react-components'; +import { makeStyles, shorthands, tokens } from '@fluentui/react-components'; import { Alert } from '@fluentui/react-components/unstable'; -import { Dismiss16Regular } from '@fluentui/react-icons'; import React from 'react'; import { useAppDispatch, useAppSelector } from '../../redux/app/hooks'; import { RootState } from '../../redux/app/store'; import { removeAlert } from '../../redux/features/app/appSlice'; +import { Dismiss16 } from './BundledIcons'; const useClasses = makeStyles({ alert: { @@ -16,6 +16,14 @@ const useClasses = makeStyles({ fontSize: tokens.fontSizeBase200, lineHeight: tokens.lineHeightBase200, }, + actionItems: { + display: 'flex', + flexDirection: 'row', + ...shorthands.gap(tokens.spacingHorizontalMNudge), + }, + button: { + alignSelf: 'center', + }, }); export const Alerts: React.FC = () => { @@ -25,19 +33,23 @@ export const Alerts: React.FC = () => { return (
- {alerts.map(({ type, message }, index) => { + {alerts.map(({ type, message, onRetry }, index) => { return ( { - dispatch(removeAlert(index)); - }} - color="black" - /> + children: ( +
+ {onRetry &&
Retry
} + { + dispatch(removeAlert(index)); + }} + color="black" + className={classes.button} + /> +
), }} key={`${index}-${type}`} diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index 38720e3a4..e63b466c5 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -343,26 +343,29 @@ export const useChat = () => { }; const deleteChat = async (chatId: string) => { - try { - await chatService.deleteChatAsync( - chatId, - loggedInUser.id, - await AuthHelper.getSKaaSAccessToken(instance, inProgress), - ); - dispatch(deleteConversation(chatId)); - - // If there is only one chat left, create a new chat - if (Object.keys(conversations).length <= 1) { - await createChat(); - } - } catch (e: any) { - let errorMessage = `Unable to delete chat. Details: ${(e as Error).message}`; - if ((e as Error).message.includes('Error: 424')) { - errorMessage = "Chat was deleted, but some or all resources couldn't be deleted. Please try again."; - } - - dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); - } + const friendlyChatName = getFriendlyChatName(conversations[chatId]); + await chatService + .deleteChatAsync(chatId, loggedInUser.id, await AuthHelper.getSKaaSAccessToken(instance, inProgress)) + .then(() => { + dispatch(deleteConversation(chatId)); + + // If there is only one chat left, create a new chat + if (Object.keys(conversations).length <= 1) { + void createChat(); + } + }) + .catch((e: any) => { + const errorDetails = (e as Error).message.includes('Error: 424') + ? "Some or all resources associated with chat couldn't be deleted. Please try again." + : `Details: ${(e as Error).message}`; + dispatch( + addAlert({ + message: `Unable to delete chat {${friendlyChatName}}. ${errorDetails}`, + type: AlertType.Error, + onRetry: () => void deleteChat(chatId), + }), + ); + }); }; return { diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 97ef51cdc..9118b3ac3 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -106,7 +106,7 @@ export class ChatService extends BaseService { public deleteChatAsync = async (chatId: string, userId: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: 'chatSession/' + chatId, + commandPath: `chatSession/${chatId}`, method: 'DELETE', body: { userId, @@ -118,6 +118,18 @@ export class ChatService extends BaseService { return result; }; + public deleteChatResourcesAsync = async (chatId: string, accessToken: string): Promise => { + const result = await this.getResponseAsync( + { + commandPath: `chatSession/${chatId}/resources`, + method: 'DELETE', + }, + accessToken, + ); + + return result; + }; + public getBotResponseAsync = async ( ask: IAsk, accessToken: string, diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index 9200da37b..a2a460ca5 100644 --- a/webapp/src/redux/features/app/AppState.ts +++ b/webapp/src/redux/features/app/AppState.ts @@ -14,6 +14,7 @@ export interface Alert { message: string; type: AlertType; id?: string; + onRetry?: () => void; } interface Feature { diff --git a/webapp/src/redux/features/message-relay/signalRHubConnection.ts b/webapp/src/redux/features/message-relay/signalRHubConnection.ts index 7f28eb32a..884123d6e 100644 --- a/webapp/src/redux/features/message-relay/signalRHubConnection.ts +++ b/webapp/src/redux/features/message-relay/signalRHubConnection.ts @@ -189,14 +189,16 @@ const registerSignalREvents = (hubConnection: signalR.HubConnection, store: Stor // User Id is that of the user who initiated the deletion. hubConnection.on(SignalRCallbackMethods.ChatDeleted, (chatId: string, userId: string) => { - if (!(chatId in store.getState().conversations.conversations)) { + const conversations = store.getState().conversations.conversations; + if (!(chatId in conversations)) { store.dispatch({ - message: `Chat ${chatId} not found in store. Chat deleted signal from server was not processed. ${COPY.REFRESH_APP_ADVISORY}`, + message: `Chat ${chatId} not found in store. ChatDeleted signal from server was not processed. ${COPY.REFRESH_APP_ADVISORY}`, type: AlertType.Error, }); } else { - const friendlyChatName = getFriendlyChatName(store.getState().conversations.conversations[chatId]); + const friendlyChatName = getFriendlyChatName(conversations[chatId]); const deletedByAnotherUser = userId !== store.getState().app.activeUserInfo?.id; + store.dispatch( addAlert({ message: deletedByAnotherUser @@ -205,6 +207,7 @@ const registerSignalREvents = (hubConnection: signalR.HubConnection, store: Stor type: AlertType.Warning, }), ); + if (deletedByAnotherUser) store.dispatch({ type: 'conversations/disableConversation', From da5cdbfc438549e8dc528a22f01e7136e8ebf91d Mon Sep 17 00:00:00 2001 From: tehoang Date: Fri, 18 Aug 2023 11:08:40 -0400 Subject: [PATCH 06/15] Removing unused deleteChatResourcesAsync from webapp --- webapp/src/libs/services/ChatService.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 9118b3ac3..8cd45261f 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -118,18 +118,6 @@ export class ChatService extends BaseService { return result; }; - public deleteChatResourcesAsync = async (chatId: string, accessToken: string): Promise => { - const result = await this.getResponseAsync( - { - commandPath: `chatSession/${chatId}/resources`, - method: 'DELETE', - }, - accessToken, - ); - - return result; - }; - public getBotResponseAsync = async ( ask: IAsk, accessToken: string, From 43a9914c5dd2670a8a9f21710dfca8d5aca93eba Mon Sep 17 00:00:00 2001 From: tehoang Date: Fri, 18 Aug 2023 13:18:32 -0400 Subject: [PATCH 07/15] throwing 500 on resource deletion error + using friendlyName in delete chat dialog --- webapi/Controllers/ChatHistoryController.cs | 6 +----- webapp/src/components/chat/chat-list/ChatListItem.tsx | 1 - webapp/src/components/chat/chat-list/ListItemActions.tsx | 9 ++++----- .../chat/chat-list/dialogs/DeleteChatDialog.tsx | 8 ++++++-- webapp/src/libs/hooks/useChat.ts | 6 +++--- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 731239017..83c796a7b 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -277,7 +277,7 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte var deleteResourcesResult = await this.DeleteChatResourcesAsync(messageRelayHubContext, sessionId) as StatusCodeResult; if (deleteResourcesResult?.StatusCode != 204) { - return this.StatusCode(424, $"Failed to delete resources for chat id '{chatId}'."); + return this.StatusCode(500, $"Failed to delete resources for chat id '{chatId}'."); } // Delete chat session and broadcast update to all participants. @@ -291,10 +291,6 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte /// Deletes all associated resources (messages, memories, participants) associated with a chat session. /// /// The chat id. - [HttpDelete] - [Route("chatSession/{sessionId:guid}/resources")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] private async Task DeleteChatResourcesAsync([FromServices] IHubContext messageRelayHubContext, Guid sessionId) { var chatId = sessionId.ToString(); diff --git a/webapp/src/components/chat/chat-list/ChatListItem.tsx b/webapp/src/components/chat/chat-list/ChatListItem.tsx index b12dad6c5..91092234e 100644 --- a/webapp/src/components/chat/chat-list/ChatListItem.tsx +++ b/webapp/src/components/chat/chat-list/ChatListItem.tsx @@ -157,7 +157,6 @@ export const ChatListItem: FC = ({ {showActions && ( { setEditingTitle(true); }} diff --git a/webapp/src/components/chat/chat-list/ListItemActions.tsx b/webapp/src/components/chat/chat-list/ListItemActions.tsx index f63973f5a..18299b337 100644 --- a/webapp/src/components/chat/chat-list/ListItemActions.tsx +++ b/webapp/src/components/chat/chat-list/ListItemActions.tsx @@ -24,11 +24,10 @@ const useClasses = makeStyles({ interface IListItemActionsProps { chatId: string; - chatName: string; onEditTitleClick: () => void; } -export const ListItemActions: React.FC = ({ chatId, chatName, onEditTitleClick }) => { +export const ListItemActions: React.FC = ({ chatId, onEditTitleClick }) => { const classes = useClasses(); const { features } = useAppSelector((state: RootState) => state.app); const { conversations } = useAppSelector((state: RootState) => state.conversations); @@ -41,12 +40,12 @@ export const ListItemActions: React.FC = ({ chatId, chatN // TODO: [Issue #47] Add a loading indicator void chat.downloadBot(chatId).then((content) => { downloadFile( - `chat-history-${chatName}-${new Date().toISOString()}.json`, + `chat-history-${chatId}-${new Date().toISOString()}.json`, JSON.stringify(content), 'text/json', ); }); - }, [chat, chatId, chatName, downloadFile]); + }, [chat, chatId, downloadFile]); return (
@@ -88,7 +87,7 @@ export const ListItemActions: React.FC = ({ chatId, chatN }} /> - + {isGettingInvitationId && ( { diff --git a/webapp/src/components/chat/chat-list/dialogs/DeleteChatDialog.tsx b/webapp/src/components/chat/chat-list/dialogs/DeleteChatDialog.tsx index 194bbf047..7eecbfb44 100644 --- a/webapp/src/components/chat/chat-list/dialogs/DeleteChatDialog.tsx +++ b/webapp/src/components/chat/chat-list/dialogs/DeleteChatDialog.tsx @@ -10,6 +10,8 @@ import { DialogTrigger, } from '@fluentui/react-dialog'; import { useChat } from '../../../../libs/hooks'; +import { getFriendlyChatName } from '../../../../libs/hooks/useChat'; +import { useAppSelector } from '../../../../redux/app/hooks'; import { Delete16 } from '../../../shared/BundledIcons'; const useClasses = makeStyles({ @@ -23,13 +25,15 @@ const useClasses = makeStyles({ interface IEditChatNameProps { chatId: string; - chatName: string; } -export const DeleteChatDialog: React.FC = ({ chatId, chatName }) => { +export const DeleteChatDialog: React.FC = ({ chatId }) => { const classes = useClasses(); const chat = useChat(); + const { conversations } = useAppSelector((state) => state.conversations); + const chatName = getFriendlyChatName(conversations[chatId]); + const onDeleteChat = () => { void chat.deleteChat(chatId); }; diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index e63b466c5..f7134cc1d 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -355,8 +355,8 @@ export const useChat = () => { } }) .catch((e: any) => { - const errorDetails = (e as Error).message.includes('Error: 424') - ? "Some or all resources associated with chat couldn't be deleted. Please try again." + const errorDetails = (e as Error).message.includes('Failed to delete resources for chat id') + ? "Some or all resources associated with this chat couldn't be deleted. Please try again." : `Details: ${(e as Error).message}`; dispatch( addAlert({ @@ -407,5 +407,5 @@ export function getFriendlyChatName(convo: ChatState): string { : convo.title; // Truncate the title if it is too long - return friendlyTitle.length > 80 ? friendlyTitle.substring(0, 80) + '...' : friendlyTitle; + return friendlyTitle.length > 60 ? friendlyTitle.substring(0, 60) + '...' : friendlyTitle; } From 1b04107e848d3e2dbfbc17f6533bb68f9c70d28a Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 14:05:28 -0700 Subject: [PATCH 08/15] Adding userId from authinfo --- webapi/Controllers/ChatHistoryController.cs | 19 +++++++++++-------- webapi/Models/Request/DeleteChatRequest.cs | 17 ----------------- webapp/src/libs/hooks/useChat.ts | 2 +- webapp/src/libs/services/ChatService.ts | 5 +---- 4 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 webapi/Models/Request/DeleteChatRequest.cs diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index f4de3c3a5..355bd9c20 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -277,16 +277,19 @@ public async Task>> GetSourcesAsync( [HttpDelete] [Route("chatSession/{sessionId:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteChatSessionAsync([FromServices] IHubContext messageRelayHubContext, [FromBody] DeleteChatRequest requestBody, Guid sessionId, CancellationToken cancellationToken) + public async Task DeleteChatSessionAsync( + [FromServices] IHubContext messageRelayHubContext, + [FromServices] IAuthInfo authInfo, + Guid sessionId, + CancellationToken cancellationToken) { var chatId = sessionId.ToString(); - var userId = requestBody.UserId; - if (userId == null) + if (!(await this._participantRepository.IsUserInChatAsync(authInfo.UserId, chatId))) { - return this.BadRequest("UserId cannot be null."); + return this.Forbid("User is unauthorized to delete this chat."); } ChatSession? chatToDelete = null; @@ -301,7 +304,7 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte } // Delete any resources associated with the chat session. - var deleteResourcesResult = await this.DeleteChatResourcesAsync(messageRelayHubContext, sessionId, cancellationToken) as StatusCodeResult; + var deleteResourcesResult = await this.DeleteChatResourcesAsync(sessionId, cancellationToken) as StatusCodeResult; if (deleteResourcesResult?.StatusCode != 204) { return this.StatusCode(500, $"Failed to delete resources for chat id '{chatId}'."); @@ -309,7 +312,7 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte // Delete chat session and broadcast update to all participants. await this._sessionRepository.DeleteAsync(chatToDelete); - await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, userId, cancellationToken: cancellationToken); + await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, authInfo.UserId, cancellationToken: cancellationToken); return this.NoContent(); } @@ -318,7 +321,7 @@ public async Task DeleteChatSessionAsync([FromServices] IHubConte /// Deletes all associated resources (messages, memories, participants) associated with a chat session. /// /// The chat id. - private async Task DeleteChatResourcesAsync([FromServices] IHubContext messageRelayHubContext, Guid sessionId, CancellationToken cancellationToken) + private async Task DeleteChatResourcesAsync(Guid sessionId, CancellationToken cancellationToken) { var chatId = sessionId.ToString(); var cleanupTasks = new List(); diff --git a/webapi/Models/Request/DeleteChatRequest.cs b/webapi/Models/Request/DeleteChatRequest.cs deleted file mode 100644 index 4cdaa3e8c..000000000 --- a/webapi/Models/Request/DeleteChatRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace CopilotChat.WebApi.Models.Request; - -/// -/// Json body for deleting a chat session. -/// -public class DeleteChatRequest -{ - /// - /// Id of the user who initiated chat deletion. - /// - [JsonPropertyName("userId")] - public string? UserId { get; set; } -} diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index 4084f67a5..f4e40f664 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -356,7 +356,7 @@ export const useChat = () => { const deleteChat = async (chatId: string) => { const friendlyChatName = getFriendlyChatName(conversations[chatId]); await chatService - .deleteChatAsync(chatId, loggedInUser.id, await AuthHelper.getSKaaSAccessToken(instance, inProgress)) + .deleteChatAsync(chatId, await AuthHelper.getSKaaSAccessToken(instance, inProgress)) .then(() => { dispatch(deleteConversation(chatId)); diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 4ca5730fc..9b015ace5 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -98,14 +98,11 @@ export class ChatService extends BaseService { return result; }; - public deleteChatAsync = async (chatId: string, userId: string, accessToken: string): Promise => { + public deleteChatAsync = async (chatId: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { commandPath: `chatSession/${chatId}`, method: 'DELETE', - body: { - userId, - }, }, accessToken, ); From 92cc0267d85b0b2381c3bb325fa11ab419ed4d75 Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 14:12:16 -0700 Subject: [PATCH 09/15] Using RequireChatParticipant policy --- webapi/Controllers/ChatHistoryController.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 355bd9c20..a6d342f02 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -279,6 +279,7 @@ public async Task>> GetSourcesAsync( [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] public async Task DeleteChatSessionAsync( [FromServices] IHubContext messageRelayHubContext, [FromServices] IAuthInfo authInfo, @@ -286,12 +287,6 @@ public async Task DeleteChatSessionAsync( CancellationToken cancellationToken) { var chatId = sessionId.ToString(); - - if (!(await this._participantRepository.IsUserInChatAsync(authInfo.UserId, chatId))) - { - return this.Forbid("User is unauthorized to delete this chat."); - } - ChatSession? chatToDelete = null; try { From 4edf3cea82dc9e3a5a7e74531d676c5744617cfb Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 14:38:29 -0700 Subject: [PATCH 10/15] Addressing comments --- webapi/Controllers/ChatHistoryController.cs | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index a6d342f02..465ebdffb 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -272,26 +272,25 @@ public async Task>> GetSourcesAsync( /// /// Delete a chat session. /// - /// Unique Id of user who initiated the request. - /// The chat id. + /// The chat id. [HttpDelete] - [Route("chatSession/{sessionId:guid}")] + [Route("chatSession/{chatId:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] public async Task DeleteChatSessionAsync( [FromServices] IHubContext messageRelayHubContext, - [FromServices] IAuthInfo authInfo, - Guid sessionId, + Guid chatId, CancellationToken cancellationToken) { - var chatId = sessionId.ToString(); + var chatIdString = chatId.ToString(); ChatSession? chatToDelete = null; try { // Make sure the chat session exists - chatToDelete = await this._sessionRepository.FindByIdAsync(chatId); + chatToDelete = await this._sessionRepository.FindByIdAsync(chatIdString); } catch (KeyNotFoundException) { @@ -299,7 +298,7 @@ public async Task DeleteChatSessionAsync( } // Delete any resources associated with the chat session. - var deleteResourcesResult = await this.DeleteChatResourcesAsync(sessionId, cancellationToken) as StatusCodeResult; + var deleteResourcesResult = await this.DeleteChatResourcesAsync(chatIdString, cancellationToken) as StatusCodeResult; if (deleteResourcesResult?.StatusCode != 204) { return this.StatusCode(500, $"Failed to delete resources for chat id '{chatId}'."); @@ -307,7 +306,7 @@ public async Task DeleteChatSessionAsync( // Delete chat session and broadcast update to all participants. await this._sessionRepository.DeleteAsync(chatToDelete); - await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatDeletedClientCall, chatId, authInfo.UserId, cancellationToken: cancellationToken); + await messageRelayHubContext.Clients.Group(chatIdString).SendAsync(ChatDeletedClientCall, chatIdString, this._authInfo.UserId, cancellationToken: cancellationToken); return this.NoContent(); } @@ -315,10 +314,9 @@ public async Task DeleteChatSessionAsync( /// /// Deletes all associated resources (messages, memories, participants) associated with a chat session. /// - /// The chat id. - private async Task DeleteChatResourcesAsync(Guid sessionId, CancellationToken cancellationToken) + /// The chat id. + private async Task DeleteChatResourcesAsync(string chatId, CancellationToken cancellationToken) { - var chatId = sessionId.ToString(); var cleanupTasks = new List(); // Create and store the tasks for deleting all users tied to the chat. From b6b86a42dbd69ee939b9e0059c55f0a655ad1b83 Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 15:46:44 -0700 Subject: [PATCH 11/15] Handling exceptions from parallel tasks --- webapi/Controllers/ChatHistoryController.cs | 33 ++++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 465ebdffb..f14005128 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -298,8 +298,11 @@ public async Task DeleteChatSessionAsync( } // Delete any resources associated with the chat session. - var deleteResourcesResult = await this.DeleteChatResourcesAsync(chatIdString, cancellationToken) as StatusCodeResult; - if (deleteResourcesResult?.StatusCode != 204) + try + { + var deleteResourcesResult = await this.DeleteChatResourcesAsync(chatIdString, cancellationToken) as StatusCodeResult; + } + catch (AggregateException) { return this.StatusCode(500, $"Failed to delete resources for chat id '{chatId}'."); } @@ -349,20 +352,28 @@ private async Task DeleteChatResourcesAsync(string chatId, Cancel cleanupTasks.Add(this._memoryStore.DeleteCollectionAsync(collection, cancellationToken)); } - // Await all the tasks in parallel and handle the exceptions - await Task.WhenAll(cleanupTasks); - var failedTasks = false; - // Iterate over the tasks and check their status and exception - foreach (var task in cleanupTasks) + // Create a task that represents the completion of all cleanupTasks + Task aggregationTask = Task.WhenAll(cleanupTasks); + try { - if (task.IsFaulted && task.Exception != null) + // Await the completion of all tasks in parallel + await aggregationTask; + } + catch (Exception) + { + // Handle any exceptions that occurred during the tasks + if (aggregationTask?.Exception?.InnerExceptions != null && aggregationTask.Exception.InnerExceptions.Count != 0) { - failedTasks = true; - this._logger.LogInformation("Failed to delete an entity of chat {0}: {1}", chatId, task.Exception.Message); + foreach (var innerEx in aggregationTask.Exception.InnerExceptions) + { + this._logger.LogInformation("Failed to delete an entity of chat {0}: {1}", chatId, innerEx.Message); + } + + throw aggregationTask.Exception; } } - return failedTasks ? this.StatusCode(500) : this.NoContent(); + return this.NoContent(); } } From 0e303d3b33dfb28a711f90b59ba93edbd92cb843 Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 15:48:55 -0700 Subject: [PATCH 12/15] formtting --- webapi/Controllers/ChatHistoryController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index f14005128..e2a163a69 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -352,7 +352,6 @@ private async Task DeleteChatResourcesAsync(string chatId, Cancel cleanupTasks.Add(this._memoryStore.DeleteCollectionAsync(collection, cancellationToken)); } - // Create a task that represents the completion of all cleanupTasks Task aggregationTask = Task.WhenAll(cleanupTasks); try From 30e2f42bcc5f91a6895bfe522e41c344f34b1194 Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 15:53:16 -0700 Subject: [PATCH 13/15] removing NoContent response + throwing exception --- webapi/Controllers/ChatHistoryController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index e2a163a69..57bf127bd 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -300,7 +300,7 @@ public async Task DeleteChatSessionAsync( // Delete any resources associated with the chat session. try { - var deleteResourcesResult = await this.DeleteChatResourcesAsync(chatIdString, cancellationToken) as StatusCodeResult; + await this.DeleteChatResourcesAsync(chatIdString, cancellationToken) as StatusCodeResult; } catch (AggregateException) { @@ -359,7 +359,7 @@ private async Task DeleteChatResourcesAsync(string chatId, Cancel // Await the completion of all tasks in parallel await aggregationTask; } - catch (Exception) + catch (Exception ex) { // Handle any exceptions that occurred during the tasks if (aggregationTask?.Exception?.InnerExceptions != null && aggregationTask.Exception.InnerExceptions.Count != 0) @@ -371,8 +371,8 @@ private async Task DeleteChatResourcesAsync(string chatId, Cancel throw aggregationTask.Exception; } - } - return this.NoContent(); + throw new AggregateException($"Resource deletion failed for chat {chatId}.", ex); + } } } From 4bebf442f7a34df03a063ab7a5aa101d80e2f155 Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Mon, 21 Aug 2023 15:54:50 -0700 Subject: [PATCH 14/15] removing NoContent response + throwing exception --- webapi/Controllers/ChatHistoryController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 57bf127bd..58f1078c7 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -300,7 +300,7 @@ public async Task DeleteChatSessionAsync( // Delete any resources associated with the chat session. try { - await this.DeleteChatResourcesAsync(chatIdString, cancellationToken) as StatusCodeResult; + await this.DeleteChatResourcesAsync(chatIdString, cancellationToken); } catch (AggregateException) { @@ -318,7 +318,7 @@ public async Task DeleteChatSessionAsync( /// Deletes all associated resources (messages, memories, participants) associated with a chat session. /// /// The chat id. - private async Task DeleteChatResourcesAsync(string chatId, CancellationToken cancellationToken) + private async Task DeleteChatResourcesAsync(string chatId, CancellationToken cancellationToken) { var cleanupTasks = new List(); From 53b81db4a2f7c5d7462ec17117cbe94a32505119 Mon Sep 17 00:00:00 2001 From: Teresa Hoang Date: Tue, 22 Aug 2023 10:56:51 -0700 Subject: [PATCH 15/15] Changing string --- webapp/src/assets/strings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/assets/strings.ts b/webapp/src/assets/strings.ts index f342c5eae..03e0c8dee 100644 --- a/webapp/src/assets/strings.ts +++ b/webapp/src/assets/strings.ts @@ -3,6 +3,6 @@ export const COPY = { CHAT_DELETED_MESSAGE: (chatName?: string) => `Chat ${ chatName ? `{${chatName}} ` : '' - }has been deleted by another user. Please save any resources you need and refresh the page.`, + }has been removed by another user. You can still access the latest chat history for now. All chat content will be cleared once you refresh or exit the application.`, REFRESH_APP_ADVISORY: 'Please refresh the page to ensure you have the latest data.', };