using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.ProvenanceStore;
using AIStudio.Tools.RAG;
using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
///
/// Shows one passage the AI received from a data source, in full and the way the AI read it.
///
///
/// The content is the filtered one, see SourcePassage. The bar above it says where in the document
/// the passage stands, and whether it is the transcript of a recording, since the AI then read the
/// words of the recording rather than the recording itself. An image the AI was only told about is
/// said to be one, rather than shown as if the AI had seen it.
///
/// The texts of a passage lie in the provenance store once its answer is complete, and are read
/// from there. Should the store no longer hold them, the view says so instead of showing nothing.
/// A store which cannot be read, e.g., while it is still starting, is no proof of that: the view
/// then offers to read the texts again.
///
public partial class SourcePassageView : MSGComponentBase
{
///
/// What stands between the parts of the text, as Markdown: a horizontal rule.
///
private const string PART_SEPARATOR = "\n\n---\n\n";
///
/// The passage to show.
///
[Parameter]
public SourcePassage Passage { get; set; } = new();
///
/// The document the passage is from, which can be opened at its page, or null when there is no
/// document to open.
///
[Parameter]
public SourceDocumentLocation? Document { get; set; }
///
/// How high the text may grow before it scrolls, as a CSS length.
///
[Parameter]
public string MaxHeight { get; set; } = ContentViewer.DEFAULT_MAX_HEIGHT;
[Inject]
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
[Inject]
private ILogger Logger { get; init; } = null!;
private string heading = string.Empty;
private string text = string.Empty;
///
/// What the bar above the text opens: the document, or the web page of the passage; empty when there is neither.
///
private string openPath = string.Empty;
///
/// The page of the passage, which a document opens at, or null.
///
private int? openPage;
///
/// The passage whose texts are shown or being read, so that a render with the same passage does not read them again.
///
private SourcePassage? shownPassage;
///
/// Numbers the reads, so that a read which another passage overtook leaves the texts alone.
///
private int readNumber;
private bool isReadingText;
///
/// Whether the provenance store no longer holds the texts of the passage.
///
private bool isTextMissing;
///
/// Whether the provenance store could not be read, so that it is unknown whether it holds the texts of the passage.
///
private bool isStoreUnavailable;
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
var parts = new[] { this.Passage.GetLocationLabel(), this.GetTranscriptLabel() }.Where(part => part.Length > 0).ToList();
this.heading = parts.Count > 0 ? string.Join(" ยท ", parts) : T("Text the AI received");
//
// A document opens at the page of the passage. One without pages, such as a CSV file, opens
// at its beginning: no program can be sent to a part, but the user may still want to look.
// Without a document, the passage of a web page opens its page; any other passage, such as
// that of a mail, has nothing to open, see FileActions:
//
var page = this.Passage.PageNumber is > 0 ? this.Passage.PageNumber : null;
this.openPath = this.Document?.Path ?? this.Passage.Path;
this.openPage = this.Document is null ? null : page;
if (!ReferenceEquals(this.shownPassage, this.Passage))
{
this.shownPassage = this.Passage;
await this.ReadTextAsync(this.Passage);
}
await base.OnParametersSetAsync();
}
#endregion
///
/// Reads the texts of a passage: from the provenance store once they lie there, otherwise from the passage itself.
///
/// The passage.
private async Task ReadTextAsync(SourcePassage passage)
{
var currentRead = ++this.readNumber;
this.isTextMissing = false;
this.isStoreUnavailable = false;
if (passage.ProvenanceId is not { } provenanceId)
{
this.isReadingText = false;
this.text = this.ComposeText(passage.Text, passage.SurroundingContent);
return;
}
this.isReadingText = true;
this.text = string.Empty;
ProvenanceEntry? entry = null;
bool storeCannotBeRead;
try
{
// A store which is not available answers every read with nothing, which would read as a deleted text:
var store = await this.DatabaseClientProvider.GetProvenanceStoreAsync();
storeCannotBeRead = !store.IsAvailable;
if (!storeCannotBeRead)
entry = (await store.GetEntriesAsync([provenanceId], CancellationToken.None)).GetValueOrDefault(provenanceId);
}
catch (Exception exception)
{
this.Logger.LogError(exception, "Failed to read the passage '{ProvenanceId}' from the provenance store.", provenanceId);
storeCannotBeRead = true;
}
// The user went on to another passage meanwhile, which shows its own texts:
if (currentRead != this.readNumber)
return;
this.isReadingText = false;
this.isStoreUnavailable = storeCannotBeRead;
this.isTextMissing = !storeCannotBeRead && entry is null;
this.text = entry is null ? string.Empty : this.ComposeText(entry.Content, entry.SurroundingContent);
}
///
/// Reads the texts of the shown passage once more, after the provenance store could not be read.
///
private Task ReadTextAgainAsync() => this.ReadTextAsync(this.Passage);
///
/// Writes the texts of a passage as one text.
///
///
/// The surrounding content follows the matched text, the way the AI received both, so that all
/// of it reads in one place.
///
private string ComposeText(string matchedText, IReadOnlyList surroundingContent) => surroundingContent.Count is 0
? matchedText
: $"{matchedText}{PART_SEPARATOR}**{T("Surrounding content")}**{PART_SEPARATOR}{string.Join(PART_SEPARATOR, surroundingContent)}";
///
/// Says what the passage is a transcript of, or nothing when it is no transcript.
///
///
/// Asked by the extension of the path only: the file may be gone by now, and reading it would
/// tell nothing more about what the AI received.
///
private string GetTranscriptLabel()
{
if (!this.Passage.IsTranscript())
return string.Empty;
var extension = Path.GetExtension(this.Passage.Path);
if (FileTypes.IsAllowedExtension(extension, FileTypes.VIDEO))
return T("Transcript of a video file");
return FileTypes.IsAllowedExtension(extension, FileTypes.AUDIO)
? T("Transcript of an audio file")
: T("Transcript");
}
}