Skip to content
59 changes: 59 additions & 0 deletions webapi/Controllers/DocumentImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<IActionResult> 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

/// <summary>
Expand Down Expand Up @@ -401,6 +450,7 @@ private async Task<ImportResult> ImportDocumentHelperAsync(
return ImportResult.Fail();
}

_importResult = importResult;
return importResult;
}

Expand Down Expand Up @@ -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
}
28 changes: 28 additions & 0 deletions webapi/Models/Request/DocumentDeleteForm.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Form for deleting a document from a POST Http request.
/// </summary>
public class DocumentDeleteForm
{
/// <summary>
/// The ID of the document to delete.
/// </summary>
public Guid DocumentId { get; set; } = Guid.Empty;

/// <summary>
/// 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.
/// </summary>
public Guid ChatId { get; set; } = Guid.Empty;

}

31 changes: 28 additions & 3 deletions webapp/src/components/chat/tabs/DocumentsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<TabView
title="Documents"
Expand Down Expand Up @@ -185,7 +196,7 @@ export const DocumentsTab: React.FC = () => {
);
};

function useTable(resources: ChatMemorySource[]) {
function useTable(resources: ChatMemorySource[], handleDelete: (chatId: string, fileId: string) => Promise<void>) {
const headerSortProps = (columnId: TableColumnId): TableHeaderCellProps => ({
onClick: (e: React.MouseEvent) => {
toggleColumnSort(e, columnId);
Expand Down Expand Up @@ -293,6 +304,20 @@ function useTable(resources: ChatMemorySource[]) {
return getSortDirection('progress') === 'ascending' ? comparison : comparison * -1;
},
}),
// Add a new column for the delete button
createTableColumn<TableItem>({
columnId: 'delete',
renderHeaderCell: () => (
<TableHeaderCell key="delete">
Delete
</TableHeaderCell>
),
renderCell: (item) => (
<TableCell key={`${item.id}-delete`}>
<button onClick={() => handleDelete(item.chatId, item.id)}>Delete</button>
</TableCell>
),
}),
];

const items = resources.map((item) => ({
Expand Down Expand Up @@ -331,7 +356,7 @@ function useTable(resources: ChatMemorySource[]) {
});
}

return { columns, rows: items };
return { columns, rows: items, handleDelete };
}

function getAccessString(chatId: string) {
Expand Down
11 changes: 11 additions & 0 deletions webapp/src/libs/hooks/useFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ export const useFile = () => {
file = null;
}

async function deleteFile(chatId: string, fileId: string): Promise<void> {
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<HTMLInputElement | null>,
Expand Down Expand Up @@ -104,6 +114,7 @@ export const useFile = () => {
return {
loadFile,
downloadFile,
deleteFile,
handleImport,
getContentSafetyStatus,
};
Expand Down
19 changes: 19 additions & 0 deletions webapp/src/libs/services/DocumentImportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(
{
commandPath: 'deleteDocument',
method: 'POST',
body: formData,
},
accessToken,
);
};

public getContentSafetyStatusAsync = async (accessToken: string): Promise<boolean> => {
return await this.getResponseAsync<boolean>(
{
Expand Down