mirror of
https://github.com/marcominerva/SqlDatabaseVectorSearch.git
synced 2026-06-20 12:23:10 +00:00
Add footer to layout and enhance document handling
- Updated `MainLayout.razor` to include a footer displaying the framework description, styled with new CSS. - Modified `MainLayout.razor.css` to add styles for the footer. - Enhanced `Ask.razor` with a new `ToastService` for user notifications and improved message handling. - Updated `Documents.razor` to restrict file uploads to specific formats and improved error handling with notifications for uploads and deletions.
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
@inherits LayoutComponentBase
|
@using System.Runtime.InteropServices
|
||||||
|
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<Toasts class="p-3" AutoHide="true" Placement="ToastsPlacement.TopRight" />
|
<Toasts class="p-3" AutoHide="true" Placement="ToastsPlacement.TopRight" />
|
||||||
|
|
||||||
@@ -21,6 +23,12 @@
|
|||||||
@Body
|
@Body
|
||||||
</ContentSection>
|
</ContentSection>
|
||||||
|
|
||||||
|
<FooterSection>
|
||||||
|
<div class="footer-content">
|
||||||
|
<span class="ms-auto">@RuntimeInformation.FrameworkDescription</span>
|
||||||
|
</div>
|
||||||
|
</FooterSection>
|
||||||
|
|
||||||
</BlazorBootstrapLayout>
|
</BlazorBootstrapLayout>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
.footer-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content .ms-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#blazor-error-ui {
|
#blazor-error-ui {
|
||||||
color-scheme: light only;
|
color-scheme: light only;
|
||||||
background: lightyellow;
|
background: lightyellow;
|
||||||
|
|||||||
@@ -52,15 +52,17 @@
|
|||||||
@if (message.IsCompleted)
|
@if (message.IsCompleted)
|
||||||
{
|
{
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
@* <div class="text-start bg-transparent">
|
<div class="text-start bg-transparent mt-3">
|
||||||
<Tooltip Title="@tokenUsage" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
|
<Tooltip Title="@message.TokenUsage" IsHtml="true" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
|
||||||
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
|
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div> *@
|
</div>
|
||||||
<div class="text-end bg-transparent">
|
<div class="text-end bg-transparent">
|
||||||
<Button TooltipColor="TooltipColor.Dark" TooltipTitle="Copy to Clipboard" TooltipPlacement="TooltipPlacement.Bottom" Outline="false" @onclick="@(async () => await CopyToClipboard(message.Text))">
|
<Tooltip Title="Copy to Clipboard" Color="TooltipColor.Dark" Placement="TooltipPlacement.Bottom">
|
||||||
<Icon @ref="clipboardIcon" Name="IconName.Clipboard" />
|
<Button Outline="false" @onclick="@(async () => await CopyToClipboard(message.Text))">
|
||||||
</Button>
|
<Icon @ref="clipboardIcon" Name="IconName.Clipboard" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -105,6 +107,9 @@
|
|||||||
private Guid conversationId = Guid.NewGuid();
|
private Guid conversationId = Guid.NewGuid();
|
||||||
private bool isAsking = false;
|
private bool isAsking = false;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected ToastService ToastService { get; set; } = default!;
|
||||||
|
|
||||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == "Enter" && !isAsking && !string.IsNullOrWhiteSpace(question))
|
if (e.Key == "Enter" && !isAsking && !string.IsNullOrWhiteSpace(question))
|
||||||
@@ -117,24 +122,28 @@
|
|||||||
{
|
{
|
||||||
isAsking = true;
|
isAsking = true;
|
||||||
|
|
||||||
|
var userQuestion = new Question(conversationId, question!);
|
||||||
|
var userMessage = new Message { Text = userQuestion.Text, Role = "user" };
|
||||||
|
messages.Add(userMessage);
|
||||||
|
|
||||||
|
var assistantMessage = new Message { Role = "assistant" };
|
||||||
|
messages.Add(assistantMessage);
|
||||||
|
|
||||||
|
question = null;
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userQuestion = new Question(conversationId, question!);
|
|
||||||
var userMessage = new Message { Text = userQuestion.Text, Role = "user" };
|
|
||||||
messages.Add(userMessage);
|
|
||||||
|
|
||||||
var assistantMessage = new Message { Role = "assistant" };
|
|
||||||
messages.Add(assistantMessage);
|
|
||||||
|
|
||||||
question = null;
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
await using var scope = ServiceProvider.CreateAsyncScope();
|
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||||
var vectorSearchService = scope.ServiceProvider.GetRequiredService<VectorSearchService>();
|
var vectorSearchService = scope.ServiceProvider.GetRequiredService<VectorSearchService>();
|
||||||
|
|
||||||
var response = vectorSearchService.AskStreamingAsync(userQuestion);
|
var response = vectorSearchService.AskStreamingAsync(userQuestion);
|
||||||
await foreach (var delta in response)
|
await foreach (var delta in response)
|
||||||
{
|
{
|
||||||
|
if (delta.StreamState == StreamState.Start)
|
||||||
|
{
|
||||||
|
assistantMessage.TokenUsage = FormatTokenUsage(delta.TokenUsage);
|
||||||
|
}
|
||||||
if (delta.StreamState == StreamState.Append)
|
if (delta.StreamState == StreamState.Append)
|
||||||
{
|
{
|
||||||
assistantMessage.Text += delta.Answer;
|
assistantMessage.Text += delta.Answer;
|
||||||
@@ -142,12 +151,18 @@
|
|||||||
else if (delta.StreamState == StreamState.End)
|
else if (delta.StreamState == StreamState.End)
|
||||||
{
|
{
|
||||||
assistantMessage.IsCompleted = true;
|
assistantMessage.IsCompleted = true;
|
||||||
|
assistantMessage.TokenUsage += FormatTokenUsage(delta.TokenUsage);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
assistantMessage.Text = $"There was an error while processing the question: {ex.Message}";
|
||||||
|
assistantMessage.IsCompleted = true;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isAsking = false;
|
isAsking = false;
|
||||||
@@ -166,6 +181,40 @@
|
|||||||
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
|
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string FormatTokenUsage(TokenUsageResponse? tokenUsageResponse)
|
||||||
|
{
|
||||||
|
if (tokenUsageResponse is null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reformulation = tokenUsageResponse.Reformulation is not null
|
||||||
|
? $"<p><strong>Reformulation:</strong><br />{FormatTokenUsageDetails(tokenUsageResponse.Reformulation)}</p>"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
var embeddingTokenCount = tokenUsageResponse.EmbeddingTokenCount.HasValue
|
||||||
|
? $"<p><strong>Embedding Token Count:</strong> {tokenUsageResponse.EmbeddingTokenCount}</p>"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
var question = tokenUsageResponse.Question is not null
|
||||||
|
? $"<p><strong>Question:</strong><br />{FormatTokenUsageDetails(tokenUsageResponse.Question)}</p>"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
return $"{reformulation}{embeddingTokenCount}{question}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatTokenUsageDetails(TokenUsage? tokenUsage)
|
||||||
|
{
|
||||||
|
if (tokenUsage is null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"Input Token Count: {tokenUsage.InputTokenCount}<br />" +
|
||||||
|
$"Output Token Count: {tokenUsage.OutputTokenCount}<br />" +
|
||||||
|
$"Total Token Count: {tokenUsage.TotalTokenCount}";
|
||||||
|
}
|
||||||
|
|
||||||
public class Message
|
public class Message
|
||||||
{
|
{
|
||||||
public string? Text { get; set; }
|
public string? Text { get; set; }
|
||||||
@@ -173,5 +222,7 @@
|
|||||||
public required string Role { get; set; }
|
public required string Role { get; set; }
|
||||||
|
|
||||||
public bool IsCompleted { get; set; }
|
public bool IsCompleted { get; set; }
|
||||||
|
|
||||||
|
public string? TokenUsage { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
|
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<InputFile class="form-control" OnChange="HandleFileSelected" />
|
<InputFile class="form-control" OnChange="HandleFileSelected" accept=".pdf,.docx,.txt,.md" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5 col-sm-5 col-5">
|
<div class="col-md-5 col-sm-5 col-5">
|
||||||
@@ -105,7 +105,6 @@ else
|
|||||||
private Button deleteButton = default!;
|
private Button deleteButton = default!;
|
||||||
|
|
||||||
private IList<SelectableDocument> documents = [];
|
private IList<SelectableDocument> documents = [];
|
||||||
private List<ToastMessage> messages = [];
|
|
||||||
|
|
||||||
private UploadDocumentRequest uploadDocumentRequest = new();
|
private UploadDocumentRequest uploadDocumentRequest = new();
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
@@ -161,9 +160,10 @@ else
|
|||||||
|
|
||||||
uploadButton.ShowLoading();
|
uploadButton.ShowLoading();
|
||||||
|
|
||||||
|
var fileName = uploadDocumentRequest.File.Name;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileName = uploadDocumentRequest.File.Name;
|
|
||||||
await using var inputStream = uploadDocumentRequest.File.OpenReadStream(20 * 1024 * 1024); // 20 MB
|
await using var inputStream = uploadDocumentRequest.File.OpenReadStream(20 * 1024 * 1024); // 20 MB
|
||||||
await using var stream = await inputStream.GetMemoryStreamAsync();
|
await using var stream = await inputStream.GetMemoryStreamAsync();
|
||||||
|
|
||||||
@@ -177,6 +177,10 @@ else
|
|||||||
|
|
||||||
await LoadDocumentsAsync(scope.ServiceProvider);
|
await LoadDocumentsAsync(scope.ServiceProvider);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ToastService.Notify(await CreateToastMessageAsync(ToastType.Danger, "Upload error", $"There was an error while uploading the document {fileName}: {ex.Message}"));
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
uploadButton.HideLoading();
|
uploadButton.HideLoading();
|
||||||
@@ -218,6 +222,10 @@ else
|
|||||||
await LoadDocumentsAsync(scope.ServiceProvider);
|
await LoadDocumentsAsync(scope.ServiceProvider);
|
||||||
ToastService.Notify(await CreateToastMessageAsync(ToastType.Info, "Delete documents", "The selected documents have been successfully deleted."));
|
ToastService.Notify(await CreateToastMessageAsync(ToastType.Info, "Delete documents", "The selected documents have been successfully deleted."));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ToastService.Notify(await CreateToastMessageAsync(ToastType.Danger, "Delete error", $"There was an error while deleting the documents: {ex.Message}"));
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
deleteButton.HideLoading();
|
deleteButton.HideLoading();
|
||||||
|
|||||||
Reference in New Issue
Block a user