Enhance chat interface and update styles

Significantly updated `Ask.razor` to improve the chat interface with a new layout for user and assistant messages, added input area for questions, and buttons for submission and reset. Removed the previous count display and introduced asynchronous message handling and a new `Message` class.

Minor change in `Documents.razor` by removing a 2000 ms delay before loading documents.

Updated `Ask.razor.css` with new styles for tooltips, avatars, input fields, card bodies, and progress indicators.

Added or updated `assistant.png` and `user.png` for new avatar images in the chat interface.
This commit is contained in:
Marco Minerva
2025-02-19 16:48:02 +01:00
parent ca51b19ea3
commit 9f6ac67b26
5 changed files with 242 additions and 8 deletions
@@ -1,16 +1,151 @@
@page "/ask" @page "/ask"
@inject IServiceProvider ServiceProvider
@inject IJSRuntime JSRuntime
<PageTitle>Chat with your data</PageTitle> <PageTitle>Chat with your data</PageTitle>
<p role="status">Current count: @currentCount</p> <div class="card mx-auto">
<div class="card-body">
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @foreach (var message in messages)
{
if (message.Role == "user")
{
<div class="d-flex align-items-baseline text-end justify-content-end">
<div class="pe-2">
<div>
<div class="card card-text d-inline-block p-2 px-3 m-1">
<Markdown style="overflow-y:auto;">@message.Text</Markdown>
</div>
</div>
</div>
<div class="position-relative avatar">
<Image src="/images/user.png" class="img-fluid rounded-circle" alt="" />
</div>
</div>
}
else if (message.Role == "assistant")
{
<div class="d-flex align-items-baseline">
<div class="position-relative avatar">
<Image src="/images/assistant.png" class="img-fluid rounded-circle" alt="" />
</div>
<div class="pe-2">
<div>
@if (message.Text is null)
{
<div class="card card-text d-inline-block p-3 px-3 m-1">
<div class="progress-chat" role="progressbar" aria-label="I'm thinking" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar-chat">
<div class="progress-bar-indeterminate"></div>
</div>
</div>
</div>
}
else
{
<div class="card card-text d-inline-block p-2 px-3 m-1">
<div>
<Markdown style="overflow-y:auto;">@message.Text</Markdown>
</div>
@if (message.IsCompleted)
{
<div class="text-end bg-transparent border-0">
<Tooltip Title="Copy to Clipboard" Color="TooltipColor.Dark" Placement="TooltipPlacement.Bottom">
<button class="btn" @onclick="@(async () => await CopyToClipboard(message.Text))">
<Icon Name="IconName.Clipboard" />
</button>
</Tooltip>
</div>
}
</div>
}
</div>
</div>
</div>
}
}
</div>
@code { <div class="card-footer bg-white w-100 bottom-0 m-0 p-1">
private int currentCount = 0; <div class="input-group">
<span class="input-group-text bg-transparent border-0">
<Tooltip Title="Messages aren't stored in any way on either the client or the server." Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex text-body-secondary" Name="IconName.InfoCircle"></Icon>
</Tooltip>
</span>
<input @bind="@question" @bind:event="oninput" placeholder="Ask me anything..." class="form-control border-0" maxlength="2000" autofocus />
<div class="input-group-text bg-transparent border-0">
<Button @ref="askButton" Color="ButtonColor.Primary" Disabled="@(isAsking || string.IsNullOrWhiteSpace(question))" @onclick="AskQuestion">
<Icon Name="IconName.Send" />
</Button>
<Button @ref="resetButton" Class="ms-2" Color="ButtonColor.Secondary" Disabled="@isAsking" @onclick="Reset">
<Icon CustomIconName="bi bi-x-lg" />
</Button>
</div>
</div>
</div>
</div>
private void IncrementCount() @code
{
private Button askButton = default!;
private Button resetButton = default!;
private IList<Message> messages = [];
private string? question;
private Guid conversationId = Guid.NewGuid();
private bool isAsking = false;
private async Task AskQuestion()
{ {
currentCount++; isAsking = true;
try
{
var userMessage = new Message { Text = question, Role = "user" };
messages.Add(userMessage);
var assistantMessage = new Message { Role = "assistant" };
messages.Add(assistantMessage);
await using var scope = ServiceProvider.CreateAsyncScope();
var vectorSearchService = scope.ServiceProvider.GetRequiredService<VectorSearchService>();
var response = await vectorSearchService.AskQuestionAsync(new Question(conversationId, question!));
assistantMessage.Text = response.Answer;
assistantMessage.IsCompleted = true;
question = null;
}
finally
{
isAsking = false;
}
} }
}
private void Reset()
{
question = null;
conversationId = Guid.NewGuid();
messages.Clear();
}
private ValueTask CopyToClipboard(string text)
{
return JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
}
public class Message
{
public string? Text { get; set; }
public required string Role { get; set; }
public bool IsCompleted { get; set; }
}
}
@@ -0,0 +1,100 @@
.tooltip-inner {
text-align: left;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
border: 2px solid #ddd;
padding: 2px;
flex: none;
}
input:focus {
outline: 0px !important;
box-shadow: none !important;
}
input[type="checkbox"],
input[type="checkbox"] + label {
cursor: pointer;
}
.card-body {
overflow: auto;
height: 490px;
}
@media (min-width: 768px) {
.card-body {
height: 595px;
}
}
@media (min-width: 2560px) {
.card-body {
height: 950px;
}
}
.card-text {
border: 2px solid #ddd;
border-radius: 8px;
}
.progress-chat {
width: 200px;
height: 4px;
}
.progress-bar-chat {
height: 4px;
background-color: rgba(5, 114, 206, 0.2);
width: 100%;
overflow: hidden;
}
.progress-bar-indeterminate {
width: 100%;
height: 100%;
background-color: rgb(5, 114, 206);
animation: indeterminate-animation 1s infinite linear;
transform-origin: 0% 50%;
}
@keyframes indeterminate-animation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
.btn-clipboard {
line-height: 1;
color: var(--bs-body-color);
background-color: var(--bd-pre-bg);
border: 0;
border-radius: .25rem;
margin-right: -.4em
}
.btn-clipboard:hover {
color: var(--bs-link-hover-color)
}
.btn-clipboard:focus {
z-index: 3
}
.btn-clipboard {
position: relative;
z-index: 2;
}
@@ -124,7 +124,6 @@ else
return; return;
} }
await Task.Delay(2000);
await using var scope = ServiceProvider.CreateAsyncScope(); await using var scope = ServiceProvider.CreateAsyncScope();
await LoadDocumentsAsync(scope.ServiceProvider); await LoadDocumentsAsync(scope.ServiceProvider);
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B