diff --git a/webapi/Controllers/DocumentImportController.cs b/webapi/Controllers/DocumentImportController.cs index 1f5cb3032..e150e552f 100644 --- a/webapi/Controllers/DocumentImportController.cs +++ b/webapi/Controllers/DocumentImportController.cs @@ -80,6 +80,7 @@ private enum SupportedFileType private const string GlobalDocumentUploadedClientCall = "GlobalDocumentUploaded"; private const string ReceiveMessageClientCall = "ReceiveMessage"; private readonly IOcrEngine _ocrEngine; + private ImportResult _importResult; private readonly IAuthInfo _authInfo; private readonly IContentSafetyService? _contentSafetyService; @@ -193,6 +194,54 @@ await messageRelayHubContext.Clients.All.SendAsync( return this.Ok("Documents imported successfully to global scope."); } + [Route("deleteDocument")] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteDocumentAsync( + [FromServices] IKernel kernel, + [FromForm] DocumentDeleteForm documentDeleteForm) + { + try + { + await ValidateDocumentDeleteFormAsync(documentDeleteForm); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + + // Check if the document exists in the specified chat session. + var memorySource = await _sourceRepository.FindByIdAsync(documentDeleteForm.DocumentId.ToString()); + + if (memorySource == null || memorySource.ChatId.ToString() != documentDeleteForm.ChatId.ToString()) + { + return BadRequest("Document not found in the specified chat session."); + } + + // Check if the user has access to the chat session. + if (!await UserHasAccessToChatAsync(this._authInfo.UserId, documentDeleteForm.ChatId)) + { + return BadRequest("User does not have access to the chat session."); + } + + try + { + // Delete the document from the repository. + await _sourceRepository.DeleteAsync(memorySource); + await RemoveMemoriesAsync(kernel, _importResult); + } + catch (Exception ex) + { + // Handle any exceptions that occur during the deletion process. + return BadRequest($"Failed to delete the document: {ex.Message}"); + } + + return Ok("Document deleted successfully."); + } + + + #region Private /// @@ -401,6 +450,7 @@ private async Task ImportDocumentHelperAsync( return ImportResult.Fail(); } + _importResult = importResult; return importResult; } @@ -642,5 +692,14 @@ private async Task RemoveMemoriesAsync(IKernel kernel, ImportResult importResult } } + private async Task ValidateDocumentDeleteFormAsync(DocumentDeleteForm documentDeleteForm) + { + // Make sure the user has access to the chat session where the document exists. + if (!(await UserHasAccessToChatAsync(this._authInfo.UserId, documentDeleteForm.ChatId))) + { + throw new ArgumentException("User does not have access to the chat session."); + } + + } #endregion } diff --git a/webapi/Models/Request/DocumentDeleteForm.cs b/webapi/Models/Request/DocumentDeleteForm.cs new file mode 100644 index 000000000..f27d1d8db --- /dev/null +++ b/webapi/Models/Request/DocumentDeleteForm.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Form for deleting a document from a POST Http request. +/// +public class DocumentDeleteForm +{ + /// + /// The ID of the document to delete. + /// + public Guid DocumentId { get; set; } = Guid.Empty; + + /// + /// The ID of the chat that owns the document. + /// This is used to verify if the user has access to the chat session and delete the document from the appropriate chat. + /// + public Guid ChatId { get; set; } = Guid.Empty; + +} + diff --git a/webapp/src/components/chat/tabs/DocumentsTab.tsx b/webapp/src/components/chat/tabs/DocumentsTab.tsx index b36c9e04b..499df6c71 100644 --- a/webapp/src/components/chat/tabs/DocumentsTab.tsx +++ b/webapp/src/components/chat/tabs/DocumentsTab.tsx @@ -118,7 +118,18 @@ export const DocumentsTab: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [importingDocuments, selectedId]); - const { columns, rows } = useTable(resources); + const handleDelete = async (chatId: string, fileId: string) => { + try { + await fileHandler.deleteFile(chatId, fileId); + // Update the state immediately after deleting the file + setResources((prevResources) => prevResources.filter((resource) => resource.id !== fileId)); + } catch (error) { + console.error('Failed to delete the file:', error); + } + }; + + const { columns, rows } = useTable(resources, handleDelete); + return ( { ); }; -function useTable(resources: ChatMemorySource[]) { +function useTable(resources: ChatMemorySource[], handleDelete: (chatId: string, fileId: string) => Promise) { const headerSortProps = (columnId: TableColumnId): TableHeaderCellProps => ({ onClick: (e: React.MouseEvent) => { toggleColumnSort(e, columnId); @@ -293,6 +304,20 @@ function useTable(resources: ChatMemorySource[]) { return getSortDirection('progress') === 'ascending' ? comparison : comparison * -1; }, }), + // Add a new column for the delete button + createTableColumn({ + columnId: 'delete', + renderHeaderCell: () => ( + + Delete + + ), + renderCell: (item) => ( + + + + ), + }), ]; const items = resources.map((item) => ({ @@ -331,7 +356,7 @@ function useTable(resources: ChatMemorySource[]) { }); } - return { columns, rows: items }; + return { columns, rows: items, handleDelete }; } function getAccessString(chatId: string) { diff --git a/webapp/src/libs/hooks/useFile.ts b/webapp/src/libs/hooks/useFile.ts index ea2a114de..8feada6a9 100644 --- a/webapp/src/libs/hooks/useFile.ts +++ b/webapp/src/libs/hooks/useFile.ts @@ -48,6 +48,16 @@ export const useFile = () => { file = null; } + async function deleteFile(chatId: string, fileId: string): Promise { + try { + + // Call the deleteDocumentAsync method from the DocumentDeleteService + await documentImportService.deleteDocumentAsync(chatId, fileId, await AuthHelper.getSKaaSAccessToken(instance, inProgress)); + + } catch (error) { + console.error('Failed to delete the file:', error); + } + } const handleImport = async ( chatId: string, documentFileRef: React.MutableRefObject, @@ -104,6 +114,7 @@ export const useFile = () => { return { loadFile, downloadFile, + deleteFile, handleImport, getContentSafetyStatus, }; diff --git a/webapp/src/libs/services/DocumentImportService.ts b/webapp/src/libs/services/DocumentImportService.ts index 377d5ba92..c7cbed271 100644 --- a/webapp/src/libs/services/DocumentImportService.ts +++ b/webapp/src/libs/services/DocumentImportService.ts @@ -28,6 +28,25 @@ export class DocumentImportService extends BaseService { ); }; + public deleteDocumentAsync = async ( + chatId: string, + documentId: string, + accessToken: string, + ) => { + const formData = new FormData(); + formData.append('chatId', chatId); + formData.append('documentId', documentId); + + return await this.getResponseAsync( + { + commandPath: 'deleteDocument', + method: 'POST', + body: formData, + }, + accessToken, + ); + }; + public getContentSafetyStatusAsync = async (accessToken: string): Promise => { return await this.getResponseAsync( {