diff --git a/SqlDatabaseVectorSearch/Components/App.razor b/SqlDatabaseVectorSearch/Components/App.razor index 8babb62..6b1533a 100644 --- a/SqlDatabaseVectorSearch/Components/App.razor +++ b/SqlDatabaseVectorSearch/Components/App.razor @@ -9,7 +9,7 @@ - + diff --git a/SqlDatabaseVectorSearch/Components/Pages/Documents.razor b/SqlDatabaseVectorSearch/Components/Pages/Documents.razor index 9b42243..a9ba723 100644 --- a/SqlDatabaseVectorSearch/Components/Pages/Documents.razor +++ b/SqlDatabaseVectorSearch/Components/Pages/Documents.razor @@ -1,8 +1,7 @@ @page "/documents" @using MimeMapping -@inject VectorSearchService vectorSearchService -@inject DocumentService documentService +@inject IServiceProvider ServiceProvider @inject IJSRuntime JSRuntime @@ -10,12 +9,14 @@ Documents -

+

Upload new document -

+ + + + -
@@ -35,23 +36,30 @@ Document ID - +
+
- +
-@if (documents.Count > 0) +@if (documents.Count == 0) { -

+
+ +
+} +else +{ +

Available documents -

+ @@ -100,10 +108,14 @@ private IList documents = []; private List messages = []; - [SupplyParameterFromForm] - public IBrowserFile? File { get; set; } + private UploadDocumentRequest uploadDocumentRequest = new(); + private EditContext? editContext; - public string? documentId { get; set; } + protected override void OnInitialized() + { + editContext = new EditContext(uploadDocumentRequest); + base.OnInitialized(); + } protected override async Task OnAfterRenderAsync(bool firstRender) { @@ -112,11 +124,14 @@ return; } - await LoadDocumentsAsync(); + await Task.Delay(2000); + await using var scope = ServiceProvider.CreateAsyncScope(); + await LoadDocumentsAsync(scope.ServiceProvider); } - private async Task LoadDocumentsAsync() + private async Task LoadDocumentsAsync(IServiceProvider services) { + var documentService = services.GetRequiredService(); var dbDocuments = await documentService.GetAsync(); documents.Clear(); @@ -133,12 +148,12 @@ private void HandleFileSelected(InputFileChangeEventArgs e) { - File = e.File; + uploadDocumentRequest.File = e.File; } private async Task HandleValidSubmit() { - if (File is null) + if (uploadDocumentRequest.File is null) { return; } @@ -147,15 +162,19 @@ try { - await using var inputStream = File.OpenReadStream(20 * 1024 * 1024); // 20 MB + var fileName = uploadDocumentRequest.File.Name; + await using var inputStream = uploadDocumentRequest.File.OpenReadStream(20 * 1024 * 1024); // 20 MB await using var stream = await inputStream.GetMemoryStreamAsync(); - await vectorSearchService.ImportAsync(stream, File.Name, MimeUtility.GetMimeMapping(File.Name), Guid.Parse(documentId)); + await using var scope = ServiceProvider.CreateAsyncScope(); + var vectorSearchService = scope.ServiceProvider.GetRequiredService(); + var documentId = string.IsNullOrWhiteSpace(uploadDocumentRequest.DocumentId) ? null : (Guid?)Guid.Parse(uploadDocumentRequest.DocumentId); + await vectorSearchService.ImportAsync(stream, fileName, MimeUtility.GetMimeMapping(fileName), documentId); - CreateToastMessage(ToastType.Success, "Upload document", $"The document {File.Name} has been successfully uploaded and indexed.", await GetLocalDateTimeStringAsync(DateTimeOffset.UtcNow)); + CreateToastMessage(ToastType.Success, "Upload document", $"The document {fileName} has been successfully uploaded and indexed.", await GetLocalDateTimeStringAsync(DateTimeOffset.UtcNow)); - await LoadDocumentsAsync(); + await LoadDocumentsAsync(scope.ServiceProvider); } finally { @@ -165,7 +184,7 @@ private async Task DeleteSelectedDocuments() { - var selectedDocuments = documents?.Where(d => d.IsSelected) ?? []; + var selectedDocumentIds = documents?.Where(d => d.IsSelected).Select(d => d.Id) ?? []; var confirmation = await dialog.ShowAsync( title: "Delete the selected document?", @@ -181,12 +200,12 @@ { deleteButton.ShowLoading(); - foreach (var document in selectedDocuments) - { - await documentService.DeleteAsync(document.Id); - } + await using var scope = ServiceProvider.CreateAsyncScope(); + var documentService = scope.ServiceProvider.GetRequiredService(); - await LoadDocumentsAsync(); + await documentService.DeleteAsync(selectedDocumentIds); + + await LoadDocumentsAsync(scope.ServiceProvider); CreateToastMessage(ToastType.Info, "Delete documents", "The selected documents have been successfully deleted.", await GetLocalDateTimeStringAsync(DateTimeOffset.UtcNow)); } finally @@ -222,4 +241,13 @@ public string LocalCreationDateString { get; set; } = string.Empty; } + + public class UploadDocumentRequest + { + [SupplyParameterFromForm] + public IBrowserFile? File { get; set; } + + [RegularExpression(@"^(\{|\()?[0-9a-fA-F]{8}(-?)[0-9a-fA-F]{4}(-?)[0-9a-fA-F]{4}(-?)[0-9a-fA-F]{4}(-?)[0-9a-fA-F]{12}(\}|\))?$", ErrorMessage = "Invalid GUID format.")] + public string? DocumentId { get; set; } + } } diff --git a/SqlDatabaseVectorSearch/Components/_Imports.razor b/SqlDatabaseVectorSearch/Components/_Imports.razor index f24de82..06b940f 100644 --- a/SqlDatabaseVectorSearch/Components/_Imports.razor +++ b/SqlDatabaseVectorSearch/Components/_Imports.razor @@ -1,4 +1,5 @@ -@using System.Net.Http +@using System.ComponentModel.DataAnnotations +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing diff --git a/SqlDatabaseVectorSearch/Services/DocumentService.cs b/SqlDatabaseVectorSearch/Services/DocumentService.cs index 5896ecc..2a6c6ef 100644 --- a/SqlDatabaseVectorSearch/Services/DocumentService.cs +++ b/SqlDatabaseVectorSearch/Services/DocumentService.cs @@ -36,4 +36,7 @@ public class DocumentService(ApplicationDbContext dbContext) public Task DeleteAsync(Guid documentId, CancellationToken cancellationToken = default) => dbContext.Documents.Where(d => d.Id == documentId).ExecuteDeleteAsync(cancellationToken); + + public Task DeleteAsync(IEnumerable documentIds, CancellationToken cancellationToken = default) + => dbContext.Documents.Where(d => documentIds.Contains(d.Id)).ExecuteDeleteAsync(cancellationToken); } \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/wwwroot/app.css b/SqlDatabaseVectorSearch/wwwroot/css/app.css similarity index 95% rename from SqlDatabaseVectorSearch/wwwroot/app.css rename to SqlDatabaseVectorSearch/wwwroot/css/app.css index 2839617..a1da72e 100644 --- a/SqlDatabaseVectorSearch/wwwroot/app.css +++ b/SqlDatabaseVectorSearch/wwwroot/css/app.css @@ -29,6 +29,18 @@ h1:focus { color: var(--bb-sidebar2-nav-item-text-active-color) !important; } +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem;