using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
///
/// Shows the sources an answer rests on, grouped and numbered the way the export is.
///
///
/// This list used to be Markdown, which read correctly but could not be clicked where it mattered:
/// a Markdown renderer hands every link to the browser, and the browser refuses a file address on a
/// page it loaded over http. A source of the user's own documents therefore did nothing at all.
/// Written out as components, an entry can hand its document to the runtime instead, together with
/// the page the passage was found on.
///
public partial class SourcesList : MSGComponentBase
{
//
// The name is about the alignment the function uses, not about the page: it brings the element
// into view with its end at the bottom, which for a list at the end of an answer shows all of it.
//
private const string SCROLL_INTO_VIEW_FUNCTION = "scrollToBottom";
///
/// The sources to show.
///
[Parameter]
public IList Sources { get; set; } = [];
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private ILogger Logger { get; init; } = null!;
private readonly List groups = [];
private ElementReference listElement;
///
/// Brings this list into view.
///
///
/// The counter above an answer says how many sources it rests on; this is how it takes the
/// reader to them. The element stays here, where it is rendered, rather than being handed to
/// whoever wants to scroll to it.
///
public async Task ScrollIntoViewAsync() => await this.JsRuntime.TryInvokeVoidAsync(this.CircuitState, SCROLL_INTO_VIEW_FUNCTION, this.listElement);
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
this.RebuildGroups();
await base.OnParametersSetAsync();
}
#endregion
///
/// Reads the sources once per render instead of once per entry and render.
///
///
/// Where a source points is answered by looking at its link, and while an answer streams, this
/// runs again for every chunk. The previous Markdown list was rebuilt and parsed just as often,
/// so this is the cheaper of the two, but it is still worth doing once for the whole list.
///
private void RebuildGroups()
{
this.groups.Clear();
foreach (var group in this.Sources.GroupSources())
{
var entries = new List(group.Sources.Count);
foreach (var numberedSource in group.Sources)
{
var document = numberedSource.Source.TryGetDocumentLocation(out var location) ? location : (SourceDocumentLocation?)null;
entries.Add(new(numberedSource.Number, numberedSource.Source.Title, numberedSource.Source.URL, document));
}
this.groups.Add(new(group.Heading, entries));
}
}
///
/// Opens a document in the program the system uses for it.
///
///
/// Whether the program can be sent to a page is the runtime's business, and it says afterwards
/// whether it managed to. Nothing is shown about that here: the document is open, and the title
/// of the source names the page anyway.
///
/// The document to open, and the page to show.
private async Task OpenDocument(SourceDocumentLocation document)
{
OpenDocumentResponse response;
try
{
response = await this.RustService.TryOpenDocumentInSystemViewer(document.Path, document.PageNumber);
}
catch (Exception e)
{
this.Logger.LogWarning(e, "Could not open a source document.");
await this.MessageBus.SendError(new(Icons.Material.Filled.Description, T("Could not open the document.")));
return;
}
if (response.Success)
return;
var issue = string.IsNullOrWhiteSpace(response.Issue) ? T("Unknown error") : response.Issue;
await this.MessageBus.SendError(new(Icons.Material.Filled.Description, string.Format(T("Could not open the document: {0}"), issue)));
}
///
/// Opens the file browser of the system and selects the document in it.
///
///
/// The second way out of the list: a document which the system opens in the wrong program, or
/// which the user wants to move or send on instead of read, is reached from here without being
/// opened. This is the same way out the embeddings page offers for a file it could not read.
///
/// The document to show.
private async Task ShowInFileManager(SourceDocumentLocation document)
{
OpenPathResponse response;
try
{
response = await this.RustService.TryOpenPathInRuntimeFileManager(document.Path);
}
catch (Exception e)
{
this.Logger.LogWarning(e, "Could not show a source document in the file manager.");
await this.MessageBus.SendError(new(Icons.Material.Filled.FolderOpen, T("Could not open the file location.")));
return;
}
if (response.Success)
return;
var issue = string.IsNullOrWhiteSpace(response.Issue) ? T("Unknown error") : response.Issue;
await this.MessageBus.SendError(new(Icons.Material.Filled.FolderOpen, string.Format(T("Could not open the file location: {0}"), issue)));
}
///
/// One group of the list, prepared so that the markup only has to show it.
///
/// The heading above the group.
/// The entries of the group, in the order they are shown.
private readonly record struct SourceEntryGroup(string Heading, IReadOnlyList Entries);
///
/// One entry of the list, prepared so that the markup only has to show it.
///
/// The number the source is listed under.
/// The title of the source.
/// The address of the source, which a web source is opened by.
/// The document the source names, or null when it names none.
private readonly record struct SourceEntry(int Number, string Title, string Link, SourceDocumentLocation? Document);
}