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.
This commit is contained in:
Marco Minerva
2025-02-14 16:58:51 +01:00
parent 5a507e972c
commit 09f15a9cb7
19 changed files with 592 additions and 9 deletions
+1 -1
View File
@@ -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).
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<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["SqlDatabaseVectorSearch.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- Add chart.js reference if chart components are used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add chartjs-plugin-datalabels.min.js reference if chart components with data label feature is used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add sortable.js reference if SortableList component is used in your application. -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js" asp-append-version="true"></script>
<script src="js/functions.js" asp-append-version="true"></script>
</body>
</html>
@@ -0,0 +1,56 @@
@inherits LayoutComponentBase
<BlazorBootstrapLayout StickyHeader="true">
<HeaderSection>
<ThemeSwitcher Class="ps-3 ps-lg-2" Position="DropdownMenuPosition.End" />
</HeaderSection>
<SidebarSection>
<Sidebar2 Href="/"
IconName="IconName.BookHalf"
Title="Vector Search"
DataProvider="Sidebar2DataProvider"
WidthUnit="Unit.Px" />
</SidebarSection>
<ContentSection>
@Body
</ContentSection>
<FooterSection>
Footer links...
</FooterSection>
</BlazorBootstrapLayout>
@code {
private IEnumerable<NavItem> navItems = default!;
private Task<Sidebar2DataProviderResult> Sidebar2DataProvider(Sidebar2DataProviderRequest request)
{
if (navItems is null)
{
navItems = GetNavItems();
}
var result = request.ApplyTo(navItems);
return Task.FromResult(result);
}
private IEnumerable<NavItem> 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;
}
}
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@@ -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;
}
@@ -0,0 +1,18 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
@@ -0,0 +1,169 @@
@page "/documents"
@using MimeMapping
@inject VectorSearchService vectorSearchService
@inject DocumentService documentService
@inject IJSRuntime JSRuntime
<Toasts class="p-3" Messages="messages" Placement="ToastsPlacement.TopRight" />
<PageTitle>Documents</PageTitle>
<h2 class="mb-4">Upload new document</h2>
<EditForm Model="@this" OnValidSubmit="HandleValidSubmit">
<div class="row">
<div class="col-md-10 col-sm-9 col-10">
<div class="input-group">
<span class="input-group-text">
<Tooltip Title="PDF, DOCX, TXT and MD files are supported" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
</Tooltip>
</span>
<InputFile class="form-control" OnChange="HandleFileSelected" />
</div>
</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>
</div>
</div>
</div>
</EditForm>
@if (documents.Count > 0)
{
<h2 class="mt-4">Available documents</h2>
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Number of chunks</th>
<th>Creation Date</th>
</tr>
</thead>
<tbody>
@foreach (var document in documents)
{
<tr>
<td>
<InputCheckbox class="form-check-input" @bind-Value="document.IsSelected" @onchange="StateHasChanged" />
</td>
<td>@document.Name</td>
<td>@document.ChunkCount</td>
<td>@document.LocalCreationDateString</td>
</tr>
}
</tbody>
</table>
<Button @ref="deleteButton" Color="ButtonColor.Danger" Disabled="@(!documents.Any(d => d.IsSelected))" @onclick="DeleteSelectedDocuments">
<Icon Name="IconName.X" /><span class="d-none d-lg-inline ps-3">Delete</span>
</Button>
}
@code {
private Button uploadButton = default!;
private Button deleteButton = default!;
private IList<SelectableDocument> documents = [];
private List<ToastMessage> 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<string>("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;
}
}
@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@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;
}
@@ -0,0 +1,12 @@
@page "/"
<PageTitle>Blazor Bootstrap - Web App (.NET 9) - Starter Template</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new Blazor Bootstrap - Web App (.NET 9).
<br />
<br />
Interactive render mode: Server
<br />
Interactivity location: Global
@@ -0,0 +1,63 @@
@page "/weather"
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@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);
}
}
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@@ -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
@@ -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(@"^/(?<culture>[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;
}
}
@@ -0,0 +1,16 @@
namespace SqlDatabaseVectorSearch.Extensions;
public static class StreamExtensions
{
public static async Task<MemoryStream> 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;
}
}
+39 -6
View File
@@ -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<AzureOpenAISettings>(builder.Configuration, "AzureOpenAI")!;
var appSettings = builder.Services.ConfigureAndGet<AppSettings>(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<App>()
.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) =>
{
@@ -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"
}
@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor.Bootstrap" Version="3.3.1" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
<PackageReference Include="EFCore.SqlServer.VectorSearch" Version="9.0.0-preview.2" />
<PackageReference Include="EntityFrameworkCore.Exceptions.SqlServer" Version="8.1.3" />
+40
View File
@@ -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."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,3 @@
function getLocalTime(utcDateTime) {
return new Date(utcDateTime).toLocaleString();
}