mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 12:07:22 +00:00
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
201 lines
7.6 KiB
C#
201 lines
7.6 KiB
C#
using AIStudio.Tools.Databases;
|
|
using AIStudio.Tools.Databases.ProvenanceStore;
|
|
using AIStudio.Tools.RAG;
|
|
using AIStudio.Tools.Rust;
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
namespace AIStudio.Components;
|
|
|
|
/// <summary>
|
|
/// Shows one passage the AI received from a data source, in full and the way the AI read it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.<br/><br/>
|
|
///
|
|
/// 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.
|
|
/// </remarks>
|
|
public partial class SourcePassageView : MSGComponentBase
|
|
{
|
|
/// <summary>
|
|
/// What stands between the parts of the text, as Markdown: a horizontal rule.
|
|
/// </summary>
|
|
private const string PART_SEPARATOR = "\n\n---\n\n";
|
|
|
|
/// <summary>
|
|
/// The passage to show.
|
|
/// </summary>
|
|
[Parameter]
|
|
public SourcePassage Passage { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// The document the passage is from, which can be opened at its page, or null when there is no
|
|
/// document to open.
|
|
/// </summary>
|
|
[Parameter]
|
|
public SourceDocumentLocation? Document { get; set; }
|
|
|
|
/// <summary>
|
|
/// How high the text may grow before it scrolls, as a CSS length.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string MaxHeight { get; set; } = ContentViewer.DEFAULT_MAX_HEIGHT;
|
|
|
|
[Inject]
|
|
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private ILogger<SourcePassageView> Logger { get; init; } = null!;
|
|
|
|
private string heading = string.Empty;
|
|
|
|
private string text = string.Empty;
|
|
|
|
/// <summary>
|
|
/// What the bar above the text opens: the document, or the web page of the passage; empty when there is neither.
|
|
/// </summary>
|
|
private string openPath = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The page of the passage, which a document opens at, or null.
|
|
/// </summary>
|
|
private int? openPage;
|
|
|
|
/// <summary>
|
|
/// The passage whose texts are shown or being read, so that a render with the same passage does not read them again.
|
|
/// </summary>
|
|
private SourcePassage? shownPassage;
|
|
|
|
/// <summary>
|
|
/// Numbers the reads, so that a read which another passage overtook leaves the texts alone.
|
|
/// </summary>
|
|
private int readNumber;
|
|
|
|
private bool isReadingText;
|
|
|
|
/// <summary>
|
|
/// Whether the provenance store no longer holds the texts of the passage.
|
|
/// </summary>
|
|
private bool isTextMissing;
|
|
|
|
/// <summary>
|
|
/// Whether the provenance store could not be read, so that it is unknown whether it holds the texts of the passage.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Reads the texts of a passage: from the provenance store once they lie there, otherwise from the passage itself.
|
|
/// </summary>
|
|
/// <param name="passage">The passage.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the texts of the shown passage once more, after the provenance store could not be read.
|
|
/// </summary>
|
|
private Task ReadTextAgainAsync() => this.ReadTextAsync(this.Passage);
|
|
|
|
/// <summary>
|
|
/// Writes the texts of a passage as one text.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The surrounding content follows the matched text, the way the AI received both, so that all
|
|
/// of it reads in one place.
|
|
/// </remarks>
|
|
private string ComposeText(string matchedText, IReadOnlyList<string> surroundingContent) => surroundingContent.Count is 0
|
|
? matchedText
|
|
: $"{matchedText}{PART_SEPARATOR}**{T("Surrounding content")}**{PART_SEPARATOR}{string.Join(PART_SEPARATOR, surroundingContent)}";
|
|
|
|
/// <summary>
|
|
/// Says what the passage is a transcript of, or nothing when it is no transcript.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
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");
|
|
}
|
|
} |