Improve UI/UX for chat, documents, and home pages

- Chat page: Add empty state, clarify tooltips/placeholders, and enhance error/copy messages.
- Documents page: Add chunking/embedding info, clarify file support, show document count badge, and display empty state. Refactor table/button markup and improve upload toasts.
- Home page: Redesign with hero section, icons, workflow cards, and styled features list. Clarify README link.
- Add Home.razor.css for new visual styles.
This commit is contained in:
Marco Minerva
2026-07-28 14:57:57 +02:00
parent 9f0d4f6402
commit dc26a3d2e9
4 changed files with 195 additions and 83 deletions
@@ -9,6 +9,15 @@
<div class="card mx-auto mt-2"> <div class="card mx-auto mt-2">
<div class="card-body"> <div class="card-body">
@if (messages.Count == 0)
{
<div class="h-100 d-flex flex-column justify-content-center align-items-center text-body-secondary">
<Icon Name="IconName.ChatSquareQuoteFill" Color="IconColor.Primary" Size="IconSize.x4" />
<p class="mt-3 mb-1 fw-semibold">Chat with your documents</p>
<p class="small mb-0">Ask a question about the documents you have uploaded. Press the up arrow key to recall your previous question.</p>
</div>
}
@foreach (var message in messages) @foreach (var message in messages)
{ {
if (message.Role == "user") if (message.Role == "user")
@@ -98,18 +107,20 @@
<div class="card-footer bg-white w-100 bottom-0 m-0 p-1"> <div class="card-footer bg-white w-100 bottom-0 m-0 p-1">
<div class="input-group"> <div class="input-group">
<span class="input-group-text bg-transparent border-0"> <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"> <Tooltip Title="Messages aren't stored in any way, either on the client or on the server." Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex" Color="IconColor.Success" Name="IconName.ShieldLockFill"></Icon> <Icon Class="d-flex" Color="IconColor.Success" Name="IconName.ShieldLockFill"></Icon>
</Tooltip> </Tooltip>
</span> </span>
<input @ref="askInput" type="text" @bind="@question" @bind:event="oninput" placeholder="Ask me anything..." class="form-control border-0" maxlength="2000" @onkeydown="HandleKeyDown" /> <input @ref="askInput" type="text" @bind="@question" @bind:event="oninput" placeholder="Ask a question about your documents..." class="form-control border-0" maxlength="2000" @onkeydown="HandleKeyDown" />
<div class="input-group-text bg-transparent border-0"> <div class="input-group-text bg-transparent border-0">
<Button Type="ButtonType.Submit" @ref="askButton" Color="ButtonColor.Primary" Disabled="@(isAsking || string.IsNullOrWhiteSpace(question))" @onclick="AskQuestion"> <Button Type="ButtonType.Submit" @ref="askButton" Color="ButtonColor.Primary" Disabled="@(isAsking || string.IsNullOrWhiteSpace(question))" @onclick="AskQuestion">
<Icon Name="IconName.Send" /> <Icon Name="IconName.Send" />
</Button> </Button>
<Button Type="ButtonType.Reset" @ref="resetButton" Class="ms-2" Color="ButtonColor.Secondary" Disabled="@isAsking" @onclick="Reset"> <Tooltip Title="Start a new conversation" Color="TooltipColor.Secondary" Placement="TooltipPlacement.Bottom">
<Icon CustomIconName="bi bi-x-lg" /> <Button Type="ButtonType.Reset" @ref="resetButton" Class="ms-2" Color="ButtonColor.Secondary" Disabled="@isAsking" @onclick="Reset">
</Button> <Icon CustomIconName="bi bi-x-lg" />
</Button>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
@@ -129,7 +140,7 @@
private bool isAsking = false; private bool isAsking = false;
private bool showCopyConfirmation = false; private bool showCopyConfirmation = false;
private string toolTipText = "Copy to Clipboard"; private string toolTipText = "Copy to clipboard";
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@@ -206,7 +217,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
assistantMessage.Text = $"There was an error while processing the question: {ex.Message}"; assistantMessage.Text = $"There was an error while processing your question: {ex.Message}";
assistantMessage.Status = MessageStatus.Completed; assistantMessage.Status = MessageStatus.Completed;
} }
finally finally
@@ -237,7 +248,7 @@
await Task.Delay(3000); // Shows the checkmark for 3 seconds await Task.Delay(3000); // Shows the checkmark for 3 seconds
toolTipText = "Copy to Clipboard"; toolTipText = "Copy to clipboard";
showCopyConfirmation = false; showCopyConfirmation = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -8,10 +8,11 @@
<PageTitle>Documents</PageTitle> <PageTitle>Documents</PageTitle>
<h4 class="mb-4"> <h4 class="mb-1">
<Icon Name="IconName.CloudArrowUpFill" Color="IconColor.Primary" class="me-2" /> <Icon Name="IconName.CloudArrowUpFill" Color="IconColor.Primary" class="me-2" />
Upload new document Upload a new document
</h4> </h4>
<p class="text-body-secondary small mb-4">The document is split into chunks, and an embedding is generated and stored for each chunk.</p>
<EditForm Model="Model" Enhance OnValidSubmit="UploadFile"> <EditForm Model="Model" Enhance OnValidSubmit="UploadFile">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
@@ -20,7 +21,7 @@
<div class="col-md-5 col-sm-4 col-5"> <div class="col-md-5 col-sm-4 col-5">
<div class="input-group"> <div class="input-group">
<span class="input-group-text"> <span class="input-group-text">
<Tooltip Title="PDF, DOCX, TXT and MD files are supported" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom"> <Tooltip Title="PDF, DOCX, TXT and MD files are supported, up to 20 MB" Color="TooltipColor.Primary" Placement="TooltipPlacement.Bottom">
<Icon Class="d-flex" Color="IconColor.Info" Name="IconName.InfoCircle"></Icon> <Icon Class="d-flex" Color="IconColor.Info" Name="IconName.InfoCircle"></Icon>
</Tooltip> </Tooltip>
</span> </span>
@@ -58,53 +59,64 @@ else
<h4 class="mt-4 mb-4"> <h4 class="mt-4 mb-4">
<Icon Name="IconName.FileEarmarkTextFill" Color="IconColor.Info" class="me-2" /> <Icon Name="IconName.FileEarmarkTextFill" Color="IconColor.Info" class="me-2" />
Available documents Available documents
<span class="badge bg-secondary-subtle text-secondary-emphasis ms-2 align-middle">@documents.Count</span>
</h4> </h4>
<div class="table-responsive"> @if (documents.Count == 0)
<table class="table table-hover align-middle mb-0 border rounded overflow-hidden"> {
<thead class="table-light sticky-top"> <div class="text-center border rounded py-5 text-body-secondary">
<tr> <Icon Name="IconName.Inbox" Color="IconColor.Secondary" Size="IconSize.x3" />
<th style="width:40px;"></th> <p class="mt-3 mb-0">No documents have been indexed yet. Upload one to get started.</p>
<th class="text-secondary">ID</th> </div>
<th class="text-secondary">Name</th> }
<th class="text-secondary">Content type</th> else
<th class="text-secondary text-center">Chunks</th> {
<th class="text-secondary">Created</th> <div class="table-responsive">
</tr> <table class="table table-hover align-middle mb-0 border rounded overflow-hidden">
</thead> <thead class="table-light sticky-top">
<tbody> <tr>
@foreach (var document in documents) <th style="width:40px;"></th>
{ <th class="text-secondary">ID</th>
<tr class="@((document.IsSelected ? "table-primary" : null))"> <th class="text-secondary">Name</th>
<td> <th class="text-secondary">Content type</th>
<div class="d-flex justify-content-center align-items-center"> <th class="text-secondary text-center">Chunks</th>
<CheckboxInput @bind-Value="document.IsSelected" /> <th class="text-secondary">Created</th>
</div>
</td>
<td class="text-break small">@document.Id</td>
<td class="fw-medium">@document.Name</td>
<td>
<span class="badge content-type-badge px-2 py-1 rounded-pill small">
@document.ContentType
</span>
</td>
<td class="text-center">@document.ChunkCount</td>
<td class="small text-secondary">@document.LocalCreationDateString</td>
</tr> </tr>
} </thead>
</tbody> <tbody>
</table> @foreach (var document in documents)
</div> {
<div class="my-4"></div> <tr class="@((document.IsSelected ? "table-primary" : null))">
<div class="row"> <td>
<div class="col-md-2 col-sm-3 col-2"> <div class="d-flex justify-content-center align-items-center">
<div class="d-grid gap-2"> <CheckboxInput @bind-Value="document.IsSelected" />
<Button @ref="deleteButton" Color="ButtonColor.Danger" Disabled="@(!documents.Any(d => d.IsSelected))" @onclick="DeleteSelectedDocuments" Class="w-100 py-2 fw-semibold shadow-sm"> </div>
<Icon Name="IconName.Trash" /><span class="d-none d-lg-inline ps-3">Delete</span> </td>
</Button> <td class="text-break small">@document.Id</td>
<td class="fw-medium">@document.Name</td>
<td>
<span class="badge content-type-badge px-2 py-1 rounded-pill small">
@document.ContentType
</span>
</td>
<td class="text-center">@document.ChunkCount</td>
<td class="small text-secondary">@document.LocalCreationDateString</td>
</tr>
}
</tbody>
</table>
</div>
<div class="my-4"></div>
<div class="row">
<div class="col-md-2 col-sm-3 col-2">
<div class="d-grid gap-2">
<Button @ref="deleteButton" Color="ButtonColor.Danger" Disabled="@(!documents.Any(d => d.IsSelected))" @onclick="DeleteSelectedDocuments" Class="w-100 py-2 fw-semibold shadow-sm">
<Icon Name="IconName.Trash" /><span class="d-none d-lg-inline ps-3">Delete</span>
</Button>
</div>
</div> </div>
</div> </div>
</div> }
} }
@code { @code {
@@ -179,7 +191,7 @@ else
var documentId = string.IsNullOrWhiteSpace(Model.DocumentId) ? null : (Guid?)Guid.Parse(Model.DocumentId); var documentId = string.IsNullOrWhiteSpace(Model.DocumentId) ? null : (Guid?)Guid.Parse(Model.DocumentId);
await vectorSearchService.ImportAsync(EmbeddingRequest.Create(stream, fileName, documentId)); await vectorSearchService.ImportAsync(EmbeddingRequest.Create(stream, fileName, documentId));
ToastService.Notify(await CreateToastMessageAsync(ToastType.Success, "Upload document", $"The document {fileName} has been successfully uploaded and indexed.")); ToastService.Notify(await CreateToastMessageAsync(ToastType.Success, "Upload document", $"The document '{fileName}' has been successfully uploaded and indexed."));
Model = new UploadDocument(); Model = new UploadDocument();
await JSRuntime.InvokeVoidAsync("resetFileInput", "fileInput"); await JSRuntime.InvokeVoidAsync("resetFileInput", "fileInput");
@@ -188,7 +200,7 @@ else
} }
catch (Exception ex) catch (Exception ex)
{ {
ToastService.Notify(await CreateToastMessageAsync(ToastType.Danger, "Upload error", $"There was an error while uploading the document {fileName}: {ex.Message}")); ToastService.Notify(await CreateToastMessageAsync(ToastType.Danger, "Upload error", $"There was an error while uploading the document '{fileName}': {ex.Message}"));
} }
finally finally
{ {
@@ -3,37 +3,112 @@
<PageTitle>SQL Database Vector Search</PageTitle> <PageTitle>SQL Database Vector Search</PageTitle>
<h1>SQL Database Vector Search</h1> <div class="hero p-4 p-md-5 mb-4 rounded-3 shadow-sm">
<h1 class="display-6 fw-semibold mb-3">
<Icon Name="IconName.Search" Color="IconColor.Primary" Class="me-2" />
SQL Database Vector Search
</h1>
<p class="lead mb-4">
A Blazor Web App and Minimal API for Retrieval Augmented Generation (RAG) and vector search using the native
<code>VECTOR</code> type in
<img src="/images/sqldatabase.svg" class="inline-logo" alt="Azure SQL Database" /> Azure SQL Database, with
<img src="/images/openai.svg" class="inline-logo" alt="Azure OpenAI" /> Azure OpenAI and
<a href="https://github.com/microsoft/agent-framework" target="_blank" rel="noopener">Microsoft Agent Framework</a>.
</p>
<div class="d-flex flex-wrap gap-2">
<a href="/documents" class="btn btn-primary px-4 py-2 fw-semibold shadow-sm">
<Icon Name="IconName.CloudArrowUpFill" Class="me-2" />Upload a document
</a>
<a href="/ask" class="btn btn-outline-primary px-4 py-2 fw-semibold">
<Icon Name="IconName.ChatDotsFill" Class="me-2" />Ask a question
</a>
</div>
</div>
<p class="mt-3 p-3 rounded bg-light text-dark shadow-sm"> <h2 class="h4 mb-3">
A Blazor Web App and Minimal API for Retrieval Augmented Generation (RAG) and vector search using the native VECTOR type in <img src="/images/sqldatabase.svg" style="height:1.5em;vertical-align:middle;" /> Azure SQL Database with <img src="/images/openai.svg" style="height:1.5em;vertical-align:middle;" /> Azure OpenAI and <a href="https://github.com/microsoft/agent-framework" target="_blank">Microsoft Agent Framework</a>. <Icon Name="IconName.Diagram3Fill" Color="IconColor.Secondary" Class="me-2" />
</p> How it works
</h2>
<p> <div class="row g-3 mb-4">
This application allows you to: <div class="col-md-4">
<ul> <div class="card h-100 border-0 shadow-sm">
<li>Load documents (PDF, DOCX, TXT, MD)</li> <div class="card-body">
<li>Generate embeddings and save them as vectors in Azure SQL Database</li> <Icon Name="IconName.FileEarmarkArrowUpFill" Color="IconColor.Primary" Size="IconSize.x3" />
<li>Perform semantic search and RAG using Azure OpenAI agents</li> <h3 class="h6 mt-3 mb-2">1. Import</h3>
<li>Interact via a Blazor Web App or programmatically via Minimal API</li> <p class="text-body-secondary mb-0 small">
</ul> Upload a PDF, DOCX, TXT or MD file. Its content is split into chunks, and each chunk is turned into
Document import, question reformulation, and RAG are orchestrated with <a href="https://github.com/microsoft/agent-framework" target="_blank">Microsoft Agent Framework</a>. an embedding by an Agent Framework workflow.
</p> </p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<Icon Name="IconName.DatabaseFillCheck" Color="IconColor.Success" Size="IconSize.x3" />
<h3 class="h6 mt-3 mb-2">2. Store</h3>
<p class="text-body-secondary mb-0 small">
Embeddings are persisted in Azure SQL Database using the native <code>VECTOR</code> type, so no
external vector store is required.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<Icon Name="IconName.ChatSquareQuoteFill" Color="IconColor.Info" Size="IconSize.x3" />
<h3 class="h6 mt-3 mb-2">3. Ask</h3>
<p class="text-body-secondary mb-0 small">
Your question is reformulated with the conversation context, matched against the stored vectors, and
answered with citations.
</p>
</div>
</div>
</div>
</div>
<h3>Supported Features</h3> <h2 class="h4 mb-3">
<ul> <Icon Name="IconName.ListStars" Color="IconColor.Warning" Class="me-2" />
<li><strong>Microsoft Agent Framework orchestration</strong>: Import documents through an embedding workflow and answer questions with dedicated reformulation and RAG agents.</li> Supported features
<li><strong>Conversation history with question reformulation</strong>: Rewrite follow-up questions with the current conversation context before vector search.</li> </h2>
<li><strong>SQL vector-search context</strong>: Retrieve relevant chunks from Azure SQL Database through native VECTOR search.</li>
<li><strong>Information about token usage</strong>: Access token usage for reformulation and final answer generation.</li> <ul class="list-group list-group-flush mb-4 shadow-sm rounded overflow-hidden border">
<li><strong>Response streaming</strong>: Receive answer tokens in real time in the chat page and streaming API.</li> <li class="list-group-item">
<li><strong>Markdown source citations</strong>: Get citations directly in the answer text with source name, page number when available, and a short supporting excerpt.</li> <Icon Name="IconName.Diagram3Fill" Color="IconColor.Primary" Class="me-2" />
<strong>Microsoft Agent Framework orchestration</strong>: documents are imported through an embedding workflow,
and questions are answered by dedicated reformulation and RAG agents.
</li>
<li class="list-group-item">
<Icon Name="IconName.ArrowRepeat" Color="IconColor.Secondary" Class="me-2" />
<strong>Conversation history with question reformulation</strong>: follow-up questions are rewritten with the
current conversation context before the vector search is performed.
</li>
<li class="list-group-item">
<Icon Name="IconName.DatabaseFillCheck" Color="IconColor.Success" Class="me-2" />
<strong>SQL vector search</strong>: the most relevant chunks are retrieved from Azure SQL Database through
native <code>VECTOR</code> cosine-distance search.
</li>
<li class="list-group-item">
<Icon Name="IconName.CashCoin" Color="IconColor.Warning" Class="me-2" />
<strong>Token usage details</strong>: input, output and total tokens are reported for both question
reformulation and answer generation.
</li>
<li class="list-group-item">
<Icon Name="IconName.LightningChargeFill" Color="IconColor.Danger" Class="me-2" />
<strong>Response streaming</strong>: answers are streamed token by token in the chat page and through the
Server-Sent Events API.
</li>
<li class="list-group-item">
<Icon Name="IconName.ChatQuoteFill" Color="IconColor.Info" Class="me-2" />
<strong>Markdown source citations</strong>: citations are embedded in the answer with the source name, the page
number when available, and a short supporting excerpt.
</li>
</ul> </ul>
<p class="mt-3 p-3 rounded bg-light text-dark shadow-sm"> <p class="text-body-secondary small">
Try <a href="/documents">uploading a document</a> or <a href="/ask">ask a question</a> to get started! <Icon Name="IconName.InfoCircleFill" Color="IconColor.Info" Class="me-2" />
</p> For API usage and more details, see the
<a href="https://github.com/marcominerva/SqlDatabaseVectorSearch#how-to-use" target="_blank" rel="noopener">README</a>.
<p class="mt-4">
<em>For API usage and more details, see the <a href="https://github.com/marcominerva/SqlDatabaseVectorSearch#how-to-use" target="_blank">README</a>.</em>
</p> </p>
@@ -0,0 +1,14 @@
.hero {
background: linear-gradient(135deg, #eef4ff 0%, #f8f9fa 100%);
border: 1px solid #e3e8f0;
}
.inline-logo {
height: 1.5em;
vertical-align: middle;
}
.card:hover {
transform: translateY(-2px);
transition: transform 0.15s ease-in-out;
}