mirror of
https://github.com/marcominerva/SqlDatabaseVectorSearch.git
synced 2026-06-20 12:23:10 +00:00
Update document handling and styling improvements
- Changed `app.css` path in `App.razor`. - Refactored `Documents.razor` to improve form handling: - Removed `VectorSearchService` injection; added `IServiceProvider` and `IJSRuntime`. - Updated header from `<h2>` to `<h4>`. - Introduced `uploadDocumentRequest` for form state management. - Modified document ID input for optional GUID with validation. - Disabled upload button when no file is selected. - Enhanced document loading logic with scoped service provider. - Updated deletion logic to handle multiple document IDs. - Added method in `DocumentService.cs` for bulk document deletion. - Restructured `app.css` for improved styling and new validation/error message styles.
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
|
||||
<script src="https://kit.fontawesome.com/f7a7b34f96.js" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["css/app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["SqlDatabaseVectorSearch.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
@page "/documents"
|
||||
@using MimeMapping
|
||||
|
||||
@inject VectorSearchService vectorSearchService
|
||||
@inject DocumentService documentService
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<Toasts class="p-3" Messages="messages" Placement="ToastsPlacement.TopRight" />
|
||||
@@ -10,12 +9,14 @@
|
||||
|
||||
<PageTitle>Documents</PageTitle>
|
||||
|
||||
<h2 class="mb-4">
|
||||
<h4 class="mb-4">
|
||||
<Icon Name="IconName.Upload" class="me-2" />
|
||||
Upload new document
|
||||
</h2>
|
||||
</h4>
|
||||
|
||||
<EditForm EditContext="@editContext" OnValidSubmit="HandleValidSubmit">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<EditForm Model="@this" OnValidSubmit="HandleValidSubmit">
|
||||
<div class="row">
|
||||
<div class="col-md-5 col-sm-4 col-5">
|
||||
<div class="input-group">
|
||||
@@ -35,23 +36,30 @@
|
||||
</Tooltip>
|
||||
Document ID
|
||||
</span>
|
||||
<TextInput Placeholder="Enter a valid GUID" @bind-Value="documentId" />
|
||||
<TextInput Placeholder="Enter a valid GUID or leave empty for auto-generation" @bind-Value="@uploadDocumentRequest.DocumentId" />
|
||||
</div>
|
||||
<ValidationMessage For="@(() => uploadDocumentRequest.DocumentId)" />
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3 col-2">
|
||||
<div class="d-grid gap-2">
|
||||
<Button @ref="uploadButton" Type="ButtonType.Submit" Color="ButtonColor.Primary" To="#"><Icon Name="IconName.Upload" /><span class="d-none d-lg-inline ps-3">Upload</span></Button>
|
||||
<Button @ref="uploadButton" Type="ButtonType.Submit" Color="ButtonColor.Primary" To="#" Disabled="@(uploadDocumentRequest.File is null)"><Icon Name="IconName.Upload" /><span class="d-none d-lg-inline ps-3">Upload</span></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (documents.Count > 0)
|
||||
@if (documents.Count == 0)
|
||||
{
|
||||
<h2 class="mt-4">
|
||||
<div class="text-center">
|
||||
<Spinner Type="SpinnerType.Dots" Class="me-3 mt-4" Color="SpinnerColor.Primary" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h4 class="mt-4">
|
||||
<Icon Name="IconName.Files" class="me-2" />
|
||||
Available documents
|
||||
</h2>
|
||||
</h4>
|
||||
|
||||
<table class="table table-bordered table-striped table-hover">
|
||||
<thead>
|
||||
@@ -100,10 +108,14 @@
|
||||
private IList<SelectableDocument> documents = [];
|
||||
private List<ToastMessage> 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<DocumentService>();
|
||||
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<VectorSearchService>();
|
||||
|
||||
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<DocumentService>();
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Guid> documentIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Documents.Where(d => documentIds.Contains(d.Id)).ExecuteDeleteAsync(cancellationToken);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user