From 09f15a9cb7ea698b48a6ca8e53f4eb0117aa6a2e Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Fri, 14 Feb 2025 16:58:51 +0100 Subject: [PATCH] Enhance application structure and functionality - Updated README.md for clarity on application features. - Added using directives and improved service configuration in Program.cs. - Enhanced error handling and status code management in Program.cs. - Changed application URL port in launchSettings.json. - Added package references for Blazor Bootstrap and other libraries. - Created new HTML structure in App.razor and implemented routing in Routes.razor. - Updated MainLayout.razor for Blazor Bootstrap layout and sidebar navigation. - Developed new components: Counter.razor, Documents.razor, Error.razor, Home.razor, and Weather.razor. - Added utility classes: RequestExtensions.cs and StreamExtensions.cs. - Updated app.css for custom styles and added favicon.png. - Created functions.js for local time conversion utility. --- README.md | 2 +- SqlDatabaseVectorSearch/Components/App.razor | 33 ++++ .../Components/Layout/MainLayout.razor | 56 ++++++ .../Components/Layout/MainLayout.razor.css | 20 +++ .../Components/Pages/Counter.razor | 18 ++ .../Components/Pages/Documents.razor | 169 ++++++++++++++++++ .../Components/Pages/Error.razor | 36 ++++ .../Components/Pages/Home.razor | 12 ++ .../Components/Pages/Weather.razor | 63 +++++++ .../Components/Routes.razor | 6 + .../Components/_Imports.razor | 14 ++ .../Extensions/RequestExtensions.cs | 64 +++++++ .../Extensions/StreamExtensions.cs | 16 ++ SqlDatabaseVectorSearch/Program.cs | 45 ++++- .../Properties/launchSettings.json | 3 +- .../SqlDatabaseVectorSearch.csproj | 1 + SqlDatabaseVectorSearch/wwwroot/app.css | 40 +++++ SqlDatabaseVectorSearch/wwwroot/favicon.png | Bin 0 -> 2193 bytes .../wwwroot/js/functions.js | 3 + 19 files changed, 592 insertions(+), 9 deletions(-) create mode 100644 SqlDatabaseVectorSearch/Components/App.razor create mode 100644 SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor create mode 100644 SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor.css create mode 100644 SqlDatabaseVectorSearch/Components/Pages/Counter.razor create mode 100644 SqlDatabaseVectorSearch/Components/Pages/Documents.razor create mode 100644 SqlDatabaseVectorSearch/Components/Pages/Error.razor create mode 100644 SqlDatabaseVectorSearch/Components/Pages/Home.razor create mode 100644 SqlDatabaseVectorSearch/Components/Pages/Weather.razor create mode 100644 SqlDatabaseVectorSearch/Components/Routes.razor create mode 100644 SqlDatabaseVectorSearch/Components/_Imports.razor create mode 100644 SqlDatabaseVectorSearch/Extensions/RequestExtensions.cs create mode 100644 SqlDatabaseVectorSearch/Extensions/StreamExtensions.cs create mode 100644 SqlDatabaseVectorSearch/wwwroot/app.css create mode 100644 SqlDatabaseVectorSearch/wwwroot/favicon.png create mode 100644 SqlDatabaseVectorSearch/wwwroot/js/functions.js diff --git a/README.md b/README.md index a62f962..ccb1791 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # SQL Database Vector Search Sample A repository that showcases the native VECTOR type in Azure SQL Database to perform embeddings and RAG with Azure OpenAI. -The application is a Minimal API that exposes endpoints to load documents, generate embeddings and save them into the database as Vectors, and perform searches using Vector Search and RAG. Currently, PDF, DOCX, TXT and MD files are supported. Vectors are saved and retrieved with Entity Framework Core using the [EFCore.SqlServer.VectorSearch](https://github.com/efcore/EfCore.SqlServer.VectorSearch) library. Embedding and Chat Completion are integrated with [Semantic Kernel](https://github.com/microsoft/semantic-kernel). +The application is a Minimal API that exposes endpoints to load documents, generate embeddings and save them into the database as Vectors, and perform searches using Vector Search and RAG. Currently, PDF, DOCX, TXT and MD files are supported. Vectors are saved and retrieved with Entity Framework Core using the [EFCore.SqlServer.VectorSearch](https://github.com/efcore/EfCore.SqlServer.VectorSearch) library. Embedding and Chat Completion are integrated with [Semantic Kernel](https://github.com/microsoft/semantic-kernel). > [!NOTE] > If you prefer to use straight SQL, check out the [sql branch](https://github.com/marcominerva/SqlDatabaseVectorSearch/tree/sql). diff --git a/SqlDatabaseVectorSearch/Components/App.razor b/SqlDatabaseVectorSearch/Components/App.razor new file mode 100644 index 0000000..8babb62 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/App.razor @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor b/SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..54a9c59 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor @@ -0,0 +1,56 @@ +@inherits LayoutComponentBase + + + + + + + + + + + + @Body + + + + Footer links... + + + +@code { + private IEnumerable navItems = default!; + + private Task Sidebar2DataProvider(Sidebar2DataProviderRequest request) + { + if (navItems is null) + { + navItems = GetNavItems(); + } + + var result = request.ApplyTo(navItems); + return Task.FromResult(result); + } + + private IEnumerable GetNavItems() + { + navItems = [ + new() { Id = "1", Href = "/", IconName = IconName.HouseDoorFill, Text = "Home", Match = NavLinkMatch.All}, + new() { Id = "2", Href = "/counter", IconName = IconName.PlusSquareFill, Text = "Counter"}, + new() { Id = "3", Href = "/weather", IconName = IconName.Table, Text = "Weather"}, + new() { Id = "4", Href= "/documents", IconName = IconName.FileTypeTxt, Text = "Documents" } + ]; + + return navItems; + } +} + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor.css b/SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..a3ae64a --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Components/Pages/Counter.razor b/SqlDatabaseVectorSearch/Components/Pages/Counter.razor new file mode 100644 index 0000000..ef23cb3 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Pages/Counter.razor @@ -0,0 +1,18 @@ +@page "/counter" + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/SqlDatabaseVectorSearch/Components/Pages/Documents.razor b/SqlDatabaseVectorSearch/Components/Pages/Documents.razor new file mode 100644 index 0000000..2751635 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Pages/Documents.razor @@ -0,0 +1,169 @@ +@page "/documents" +@using MimeMapping + +@inject VectorSearchService vectorSearchService +@inject DocumentService documentService +@inject IJSRuntime JSRuntime + + +Documents + +

Upload new document

+ + +
+
+
+ + + + + + +
+
+
+
+ +
+
+
+
+ +@if (documents.Count > 0) +{ +

Available documents

+ + + + + + + + + + + + @foreach (var document in documents) + { + + + + + + + } + +
NameNumber of chunksCreation Date
+ + @document.Name@document.ChunkCount@document.LocalCreationDateString
+ + +} + +@code { + private Button uploadButton = default!; + private Button deleteButton = default!; + + private IList documents = []; + private List messages = []; + + [SupplyParameterFromForm] + public IBrowserFile? File { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadDocumentsAsync(); + } + + private async Task LoadDocumentsAsync() + { + var dbDocuments = await documentService.GetAsync(); + + documents.Clear(); + foreach (var dbDocument in dbDocuments) + { + documents.Add(new SelectableDocument(dbDocument.Id, dbDocument.Name, dbDocument.CreationDate, dbDocument.ChunkCount) + { + LocalCreationDateString = await JSRuntime.InvokeAsync("getLocalTime", dbDocument.CreationDate) + }); + } + } + + private void HandleFileSelected(InputFileChangeEventArgs e) + { + File = e.File; + } + + private async Task HandleValidSubmit() + { + if (File is null) + { + return; + } + + uploadButton.ShowLoading(); + + try + { + await using var inputStream = File.OpenReadStream(20 * 1024 * 1024); // 20 MB + await using var stream = await inputStream.GetMemoryStreamAsync(); + + await vectorSearchService.ImportAsync(stream, File.Name, MimeUtility.GetMimeMapping(File.Name), null); + + + CreateToastMessage(ToastType.Success, "Upload document", $"The document {File.Name} has been successfully uploaded and indexed.", DateTime.Now.ToString("g")); + + await LoadDocumentsAsync(); + } + finally + { + uploadButton.HideLoading(); + } + } + + private async Task DeleteSelectedDocuments() + { + var selectedDocuments = documents?.Where(d => d.IsSelected) ?? []; + + try + { + uploadButton.ShowLoading(); + + foreach (var document in selectedDocuments) + { + await documentService.DeleteAsync(document.Id); + } + + await LoadDocumentsAsync(); + CreateToastMessage(ToastType.Info, "Delete documents", "The selected documents have been successfully deleted.", DateTime.Now.ToString("g")); + } + finally + { + uploadButton.HideLoading(); + } + } + + private void CreateToastMessage(ToastType toastType, string title, string message, string? helpText = null) + { + var toastMessage = new ToastMessage + { + Type = toastType, + Title = title, + HelpText = helpText, + Message = message, + AutoHide = true, + }; + + messages.Add(toastMessage); + } + + private record class SelectableDocument(Guid Id, string Name, DateTimeOffset CreationDate, int ChunkCount) : Document(Id, Name, CreationDate, ChunkCount) + { + public bool IsSelected { get; set; } + + public string LocalCreationDateString { get; set; } = string.Empty; + } +} diff --git a/SqlDatabaseVectorSearch/Components/Pages/Error.razor b/SqlDatabaseVectorSearch/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/SqlDatabaseVectorSearch/Components/Pages/Home.razor b/SqlDatabaseVectorSearch/Components/Pages/Home.razor new file mode 100644 index 0000000..9773793 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Pages/Home.razor @@ -0,0 +1,12 @@ +@page "/" + +Blazor Bootstrap - Web App (.NET 9) - Starter Template + +

Hello, world!

+ +Welcome to your new Blazor Bootstrap - Web App (.NET 9). +
+
+Interactive render mode: Server +
+Interactivity location: Global \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Components/Pages/Weather.razor b/SqlDatabaseVectorSearch/Components/Pages/Weather.razor new file mode 100644 index 0000000..dd36b18 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Pages/Weather.razor @@ -0,0 +1,63 @@ +@page "/weather" + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate a loading indicator + await Task.Delay(500); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + } + + private class WeatherForecast + { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} diff --git a/SqlDatabaseVectorSearch/Components/Routes.razor b/SqlDatabaseVectorSearch/Components/Routes.razor new file mode 100644 index 0000000..f756e19 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/SqlDatabaseVectorSearch/Components/_Imports.razor b/SqlDatabaseVectorSearch/Components/_Imports.razor new file mode 100644 index 0000000..f24de82 --- /dev/null +++ b/SqlDatabaseVectorSearch/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using SqlDatabaseVectorSearch +@using SqlDatabaseVectorSearch.Components +@using SqlDatabaseVectorSearch.Extensions +@using SqlDatabaseVectorSearch.Models +@using SqlDatabaseVectorSearch.Services +@using BlazorBootstrap \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Extensions/RequestExtensions.cs b/SqlDatabaseVectorSearch/Extensions/RequestExtensions.cs new file mode 100644 index 0000000..0be6602 --- /dev/null +++ b/SqlDatabaseVectorSearch/Extensions/RequestExtensions.cs @@ -0,0 +1,64 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; + +namespace SqlDatabaseVectorSearch.Extensions; + +public static partial class RequestExtensions +{ + [GeneratedRegex("(android|bb\\d+|meego).+mobile|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled)] + private static partial Regex MobileBrowserRegex { get; } + + [GeneratedRegex("1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled)] + private static partial Regex MobileBrowserVersionRegex { get; } + + [GeneratedRegex(@"^/(?[a-z]{2})(/|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled)] + private static partial Regex RouteCultureRegex { get; } + + public static bool IsMobileRequest(this HttpContext httpContext) + => httpContext.Request.IsMobile(); + + public static bool IsMobile(this HttpRequest request) + { + var userAgent = request.Headers[HeaderNames.UserAgent].ToString(); + var isMobileBrowser = false; + if (userAgent?.Length > 4 && (MobileBrowserRegex.IsMatch(userAgent) || MobileBrowserVersionRegex.IsMatch(userAgent.AsSpan(0, 4)))) + { + isMobileBrowser = true; + } + + return isMobileBrowser; + } + + public static string GetCultureFromRoute(this HttpContext httpContext) + => RouteCultureRegex.Match(httpContext.Request.Path).Groups["culture"].Value.ToLowerInvariant(); + + public static string GetPathWithCulture(this HttpContext httpContext, string culture) + { + var request = httpContext.Request; + var path = RouteCultureRegex.Replace(request.Path.ToString(), "/"); + var newPath = $"/{culture}{path}{request.QueryString}"; + + return newPath; + } + + public static bool IsApiRequest(this HttpContext httpContext) + => httpContext.Request.Path.StartsWithSegments("/api"); + + public static bool IsSwaggerRequest(this HttpContext httpContext) + => httpContext.Request.Path.StartsWithSegments("/swagger"); + + public static bool IsWebRequest(this HttpContext httpContext) + => !httpContext.IsApiRequest() && !httpContext.IsSwaggerRequest(); + + public static string Content(this IUrlHelper urlHelper, HttpContext httpContext, string contentPath) + => urlHelper.Content(httpContext.Request, contentPath); + + public static string Content(this IUrlHelper urlHelper, HttpRequest request, string contentPath) + { + var path = urlHelper.Content(contentPath); + var url = $"{request.Scheme}://{request.Host}{request.PathBase}{path}"; + + return url; + } +} \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/Extensions/StreamExtensions.cs b/SqlDatabaseVectorSearch/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..415eb53 --- /dev/null +++ b/SqlDatabaseVectorSearch/Extensions/StreamExtensions.cs @@ -0,0 +1,16 @@ +namespace SqlDatabaseVectorSearch.Extensions; + +public static class StreamExtensions +{ + public static async Task GetMemoryStreamAsync(this Stream stream) + { + // Use a BufferedStream to read the file in chunks + using var bufferedStream = new BufferedStream(stream); + + var ms = new MemoryStream(); + await bufferedStream.CopyToAsync(ms); + + ms.Position = 0; + return ms; + } +} diff --git a/SqlDatabaseVectorSearch/Program.cs b/SqlDatabaseVectorSearch/Program.cs index 1455b34..7f2c198 100644 --- a/SqlDatabaseVectorSearch/Program.cs +++ b/SqlDatabaseVectorSearch/Program.cs @@ -5,8 +5,10 @@ using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; using Microsoft.SemanticKernel; using MimeMapping; +using SqlDatabaseVectorSearch.Components; using SqlDatabaseVectorSearch.ContentDecoders; using SqlDatabaseVectorSearch.DataAccessLayer; +using SqlDatabaseVectorSearch.Extensions; using SqlDatabaseVectorSearch.Models; using SqlDatabaseVectorSearch.Services; using SqlDatabaseVectorSearch.Settings; @@ -21,6 +23,11 @@ builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, relo var aiSettings = builder.Services.ConfigureAndGet(builder.Configuration, "AzureOpenAI")!; var appSettings = builder.Services.ConfigureAndGet(builder.Configuration, nameof(AppSettings))!; +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +builder.Services.AddBlazorBootstrap(); + builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); @@ -83,24 +90,50 @@ var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); -app.UseExceptionHandler(new ExceptionHandlerOptions +app.UseWhen(context => context.IsWebRequest(), builder => { - StatusCodeSelector = exception => exception switch + if (!app.Environment.IsDevelopment()) { - NotSupportedException => StatusCodes.Status501NotImplemented, - _ => StatusCodes.Status500InternalServerError + builder.UseExceptionHandler("/error"); + + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + builder.UseHsts(); } + + builder.UseStatusCodePagesWithReExecute("/error"); }); -app.UseStatusCodePages(); +app.UseWhen(context => context.IsApiRequest(), builder => +{ + app.UseExceptionHandler(new ExceptionHandlerOptions + { + StatusCodeSelector = exception => exception switch + { + NotSupportedException => StatusCodes.Status501NotImplemented, + _ => StatusCodes.Status500InternalServerError + } + }); + + builder.UseStatusCodePages(); +}); app.MapOpenApi(); app.UseSwaggerUI(options => { - options.RoutePrefix = string.Empty; options.SwaggerEndpoint("/openapi/v1.json", builder.Environment.ApplicationName); }); +app.UseRouting(); +// app.UseRateLimiter(); +app.UseRequestLocalization(); +// app.UseCors(); + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + app.MapPost("/api/ask", async (Question question, VectorSearchService vectorSearchService, CancellationToken cancellationToken, [Description("If true, the question will be reformulated taking into account the context of the chat identified by the given ConversationId.")] bool reformulate = true) => { diff --git a/SqlDatabaseVectorSearch/Properties/launchSettings.json b/SqlDatabaseVectorSearch/Properties/launchSettings.json index f677316..3306ea1 100644 --- a/SqlDatabaseVectorSearch/Properties/launchSettings.json +++ b/SqlDatabaseVectorSearch/Properties/launchSettings.json @@ -5,8 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", - "applicationUrl": "https://localhost:7024;http://localhost:5178", + "applicationUrl": "https://localhost:7025;http://localhost:5178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj index c8e83e5..3a3f57f 100644 --- a/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj +++ b/SqlDatabaseVectorSearch/SqlDatabaseVectorSearch.csproj @@ -8,6 +8,7 @@ + diff --git a/SqlDatabaseVectorSearch/wwwroot/app.css b/SqlDatabaseVectorSearch/wwwroot/app.css new file mode 100644 index 0000000..2839617 --- /dev/null +++ b/SqlDatabaseVectorSearch/wwwroot/app.css @@ -0,0 +1,40 @@ +:root { + --bs-body-bg: #fff; + --bb-sidebar2-top-row-background-color: var(--bb-violet-bg); + --bb-sidebar2-top-row-border-color: var(--bb-violet-bg); + --bb-sidebar2-navbar-toggler-icon-color: var(--bb-violet-bg); + --bb-sidebar2-nav-item-text-active-color-rgb: 112.520718, 44.062154, 249.437846; + --bb-sidebar2-nav-item-text-color: rgba(0,0,0,0.9); + --bb-sidebar2-nav-item-text-hover-color: rgba(var(--bb-sidebar2-nav-item-text-active-color-rgb), 0.9); + --bb-sidebar2-nav-item-text-active-color: rgba(var(--bb-sidebar2-nav-item-text-active-color-rgb), 0.9); + --bb-sidebar2-nav-item-background-hover-color: rgba(var(--bb-sidebar2-nav-item-text-active-color-rgb), 0.08); + --bb-sidebar2-nav-item-group-background-color: rgba(var(--bb-sidebar2-nav-item-text-active-color-rgb), 0.08); +} + +h1:focus { + outline: none; +} + +.bb-sidebar2 nav .nav-item a { + color: var(--bb-sidebar2-nav-item-text-color) !important; +} + + .bb-sidebar2 nav .nav-item a:hover { + background-color: var(--bb-sidebar2-nav-item-background-hover-color) !important; + color: var(--bb-sidebar2-nav-item-text-hover-color) !important; + } + + .bb-sidebar2 nav .nav-item a.active { + background-color: var(--bb-sidebar2-nav-item-background-hover-color) !important; + color: var(--bb-sidebar2-nav-item-text-active-color) !important; + } + +.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; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } \ No newline at end of file diff --git a/SqlDatabaseVectorSearch/wwwroot/favicon.png b/SqlDatabaseVectorSearch/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1b2564caad5b2e021ae65ec36bb8a165686e57 GIT binary patch literal 2193 zcmZ{ldpy&N8^^ycF}K2$$~sQ5QoA#0YHp40K`)OU|WFV=K}`R)Usdv^GE+b#!cZ3@06+@-OF}??0Ym`BiD)M~@j3A$5^^9z zl?UGhL=|m|bo1=$$+;AyFm|{%Zf@wbF`~6mkrU}e5sEO*@qB7Tu$iQw2(oBSKI&j1 z-7UBRGgH_?ce5rVI}iRvt}CHcTg+nLd(cq(j+Im8l?>pfjdXnzE_-7-Akz8BD7W^Z z>KUgGF6Qs^RuJ3tz1_K;;s~*ShqWhc?q1%@No8n^Wo4Teq;>1g^b#1CC zr>)k0e!2qCJ_fqT|3h<+yae^jD$hYk+-*4<6nE%e%$Fa9bG(Aucd4;_5hkNz3d39%Viz{keQ)a7(QY^qFbkB+#wvhKm} zL>W2J->0zC8IZ0Zvh1^Ks#)7-JL{2^EVL%9i26QS6lhOdM)Xvjuf!rqOUH_$grp&0 zgH1w&0i5EK|1q(1IE<1*#?t=uekkDlAtGd7ZiFQDql{h zx#s65%=y8}$a8Ocjjd(p^!uZEp-Fo$V`s(r!jl6Qee(cTbyW$?G zXt{XoiRz{_NsT|SmYwbE!r6FV)+Vk4q#*YKO7X~WpwcJa#{ z`)rYW5Zo%cxWB*O9&+7K;ZZNTa-@cl{09gcm8(2AU>`R-y5UfpQ?zw1fOK_nS~N~~ z8j+i|*~Suz@(Qfnh$NHAE>0xwLm@TChv73_q<-%m!~!)SY~GzR;S%hYO%&^n+=YeEW?(ULE(L(*!sOudNkGj4BPv2Hk>@o+h zYnURw_Tp2lP5@A?GuDB$RQLz(t%eCbq-?DhG5rPX)fZ}q&%_n6yX__5_yHZvVo(AjG*5bm>^ zB2pao(BHbDu-&&~;W+fWqMz;D=b@f??-In~%_El}Yf%NaEsGoUQVxl**=*ebS5W`d zlEU1-Yl!W!o(Ut^88cH1eo-ufe|M^=HjFf?-5hXWxQ%~hyM?T3$V{MS)HB-~{4KID z6F!STwMPxy!eS`T_*w=Q(+TVTL4I0Ud130_tW@LW*45veH-EQ4?5fNx;Ta)U15RF= z^|nkzNi*B#4w?kVrQPGM`Dh=omV4BDaUB088$Ust66|AgIEKze7I_(1zf>d!ec{5` z6D6gGVjD4qc?)`RF_T)S5@l(jJcH zqmDK7>B&kZwg;<4mQ;T{VPj*H4G&09PzUwR+)R-cMJDn6ifZOKPgcWJ4laBxtI?u@ zdmF?dQPEcH!0m801Vhk+{_XYIY=3j`$NuRf5hWdUbA!rHLYCFA1N4;zf#Ni;MT_xCU9? zAGGooZxrzdO==7qW{y|OcCL}F=%Vp(?W2^{NoJkLCv?l#tWgNAUe7@sDH}>lf9{!j zqiGQh#p!QTu@3XMy^2R;?27RhuhZO{yq2e0Nuiku;N#Bo9UUExWz2iCyy(S^Z5-js zTeTl(DpnM!?ojbQKS}(+*@SB0Cs`ORZyklN^Yc0t*s71TtFH(&sDdPQ5Px7b-c^V? zIP;RpWTJ<88G2LEZIWPXn5CunyE$HFie-B`mSGw?{d({go5p|yr>5`O6n-2+L6zw% za|{NV*o5F$`RMCPh|;C O0BAdB+d6BX#Qy-V#`@9# literal 0 HcmV?d00001 diff --git a/SqlDatabaseVectorSearch/wwwroot/js/functions.js b/SqlDatabaseVectorSearch/wwwroot/js/functions.js new file mode 100644 index 0000000..75b35c4 --- /dev/null +++ b/SqlDatabaseVectorSearch/wwwroot/js/functions.js @@ -0,0 +1,3 @@ +function getLocalTime(utcDateTime) { + return new Date(utcDateTime).toLocaleString(); +}