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"); } }