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:
Marco Minerva
2025-02-20 11:56:48 +01:00
parent 402bf1e570
commit eba0d4c272
4 changed files with 104 additions and 20 deletions
@@ -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">
<Button Outline="false" @onclick="@(async () => await CopyToClipboard(message.Text))">
<Icon @ref="clipboardIcon" Name="IconName.Clipboard" /> <Icon @ref="clipboardIcon" Name="IconName.Clipboard" />
</Button> </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,8 +122,6 @@
{ {
isAsking = true; isAsking = true;
try
{
var userQuestion = new Question(conversationId, question!); var userQuestion = new Question(conversationId, question!);
var userMessage = new Message { Text = userQuestion.Text, Role = "user" }; var userMessage = new Message { Text = userQuestion.Text, Role = "user" };
messages.Add(userMessage); messages.Add(userMessage);
@@ -129,12 +132,18 @@
question = null; question = null;
await Task.Yield(); await Task.Yield();
try
{
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();