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;