Add citation handling and styling in Ask.razor

Updated Ask.razor to include regex for citation extraction and display.
Introduced a new method to extract citations and updated the Message
class to store them. Added a Citation class for individual citation
representation. Enhanced app.css with styles for citation display.
This commit is contained in:
Marco Minerva
2025-06-04 12:34:02 +02:00
parent 0766103b9a
commit 1c24250a42
2 changed files with 73 additions and 0 deletions
@@ -1,4 +1,5 @@
@page "/ask" @page "/ask"
@using System.Text.RegularExpressions
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@@ -72,6 +73,23 @@
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@if (message.Citations is not null && message.Citations.Count() > 0)
{
<div class="mt-3 d-flex flex-wrap">
@foreach (var citation in message.Citations)
{
<div class="border rounded p-2 me-2 mb-2 citation-box small">
<div>
<strong>@citation.FileName</strong> @if (!string.IsNullOrEmpty(citation.PageNumber))
{
<span class="ms-2">pag. @citation.PageNumber</span>
}
</div>
<div class="text-secondary small mt-1">@citation.Quote</div>
</div>
}
</div>
}
} }
</div> </div>
} }
@@ -178,10 +196,17 @@
} }
else if (delta.StreamState == StreamState.Append) else if (delta.StreamState == StreamState.Append)
{ {
// Adds tokens to the assistant message as they are received
assistantMessage.Text += delta.Answer; assistantMessage.Text += delta.Answer;
} }
else if (delta.StreamState == StreamState.End) else if (delta.StreamState == StreamState.End)
{ {
// Extracts citations, if any.
var (cleanText, citations) = ExtractCitations(assistantMessage.Text);
assistantMessage.Text = cleanText;
assistantMessage.Citations = citations;
assistantMessage.IsCompleted = true; assistantMessage.IsCompleted = true;
assistantMessage.TokenUsage += FormatTokenUsage(delta.TokenUsage); assistantMessage.TokenUsage += FormatTokenUsage(delta.TokenUsage);
} }
@@ -269,6 +294,36 @@
await JSRuntime.InvokeVoidAsync("scrollTo", chat); await JSRuntime.InvokeVoidAsync("scrollTo", chat);
} }
private static (string, IEnumerable<Citation>) ExtractCitations(string? text)
{
var citations = new List<Citation>();
if (string.IsNullOrEmpty(text))
{
return (text ?? string.Empty, citations);
}
var pattern = "<citation\\s+filename='([^']*)'\\s+page_number='([^']*)'>(.*?)<\\/citation>";
var matches = Regex.Matches(text, pattern, RegexOptions.Singleline);
foreach (Match match in matches)
{
if (match.Success && match.Groups.Count == 4)
{
citations.Add(new Citation
{
FileName = match.Groups[1].Value,
PageNumber = match.Groups[2].Value,
Quote = match.Groups[3].Value
});
}
}
// Remove all <citation> tags from the text
var cleanText = Regex.Replace(text, pattern, string.Empty, RegexOptions.Singleline).TrimEnd();
return (cleanText, citations);
}
public class Message public class Message
{ {
public string? Text { get; set; } public string? Text { get; set; }
@@ -278,5 +333,17 @@
public bool IsCompleted { get; set; } public bool IsCompleted { get; set; }
public string? TokenUsage { get; set; } public string? TokenUsage { get; set; }
// List of citations extracted from the answer
public IEnumerable<Citation>? Citations { get; set; }
}
public class Citation
{
public string FileName { get; set; } = null!;
public string Quote { get; set; } = null!;
public string PageNumber { get; set; } = null!;
} }
} }
@@ -62,3 +62,9 @@ h1:focus {
.blazor-error-boundary::after { .blazor-error-boundary::after {
content: "An error has occurred." content: "An error has occurred."
} }
.citation-box {
width: fit-content;
max-width: 100%;
background-color: #f8f9fa;
}