mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-14 10:53:48 +00:00
Merge branch 'main' into improve_document_analysis
This commit is contained in:
79 files changed
+2881
-335
No files matched your search
@@ -1475,8 +1475,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// of it would tell a person their window is empty while their first message is not.
|
||||
//
|
||||
var thread = this.ChatThread ?? this.NewChatThread(string.Empty);
|
||||
var toolDefinitions = this.GetRunnableToolDefinitions();
|
||||
provider = this.Provider;
|
||||
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput());
|
||||
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions);
|
||||
});
|
||||
|
||||
var counted = await this.ConversationTokenCounter.CountAsync(provider, parts, token);
|
||||
@@ -1501,22 +1502,29 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// source is appended to it, the selected profile adds a paragraph, and the policy of the
|
||||
/// selected tools adds another. Switching a profile while writing therefore moves the number,
|
||||
/// which is the whole reason this is asked rather than read off the thread.
|
||||
///
|
||||
/// The tools are filtered for the provider the same way they are before sending, so that a tool
|
||||
/// the provider is not trusted enough to receive does not count either.
|
||||
/// </remarks>
|
||||
/// <param name="thread">The thread to build the prompt for.</param>
|
||||
/// <param name="toolDefinitions">The tools whose policy the prompt states.</param>
|
||||
/// <returns>The system prompt as it would be sent.</returns>
|
||||
private string BuildSystemPromptFor(ChatThread thread)
|
||||
{
|
||||
var toolDefinitions = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds)
|
||||
.Select(this.ToolRegistry.GetDefinition)
|
||||
.Where(definition => definition is not null)
|
||||
.Select(definition => definition!)
|
||||
.ToList();
|
||||
private string BuildSystemPromptFor(ChatThread thread, IReadOnlyList<ToolDefinition> toolDefinitions) => thread.BuildSystemPrompt(this.SettingsManager, toolDefinitions).Text;
|
||||
|
||||
return thread.BuildSystemPrompt(this.SettingsManager, toolDefinitions).Text;
|
||||
}
|
||||
/// <summary>
|
||||
/// The tools the next request would offer the model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Filtered for the provider the same way they are before sending, so that a tool the provider
|
||||
/// is not trusted enough to receive does not count either.
|
||||
///
|
||||
/// Asked for once and used twice: their policy goes into the system prompt, and their schemas
|
||||
/// travel next to it in the request body. Both cost tokens, and both change the moment somebody
|
||||
/// switches a tool on.
|
||||
/// </remarks>
|
||||
/// <returns>The definitions of the selected tools.</returns>
|
||||
private IReadOnlyList<ToolDefinition> GetRunnableToolDefinitions() => this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds)
|
||||
.Select(this.ToolRegistry.GetDefinition)
|
||||
.Where(definition => definition is not null)
|
||||
.Select(definition => definition!)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// The thread a new chat starts with, as the selections made so far decide it.
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<MudTooltip Text="@T("Select the data you want to use here.")" Placement="Placement.Top">
|
||||
@if (this.PopoverTriggerMode is PopoverTriggerMode.ICON)
|
||||
{
|
||||
<MudIconButton Icon="@AppIcons.DATABASE" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"/>
|
||||
<MudIconButton Icon="@AppIcons.DATABASE" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleDataSourceSelection"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" StartIcon="@AppIcons.DATABASE" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())">
|
||||
<MudButton Variant="Variant.Filled" StartIcon="@AppIcons.DATABASE" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleDataSourceSelection">
|
||||
@T("Select data")
|
||||
</MudButton>
|
||||
}
|
||||
@@ -19,13 +19,13 @@
|
||||
|
||||
<MudPopover Open="@this.showDataSourceSelection" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" DropShadow="@true" Class="border-solid border-4 rounded-lg">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudCardHeader Class="pa-2 pb-0">
|
||||
<CardHeaderContent>
|
||||
<PreviewBeta/>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">
|
||||
@T("Data Source Selection")
|
||||
</MudText>
|
||||
<PreviewBeta ChipClass=""/>
|
||||
<MudSpacer/>
|
||||
<MudTooltip Text="@T("Manage your data sources")" Placement="Placement.Top">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@this.OpenSettingsDialog"/>
|
||||
@@ -33,7 +33,7 @@
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
|
||||
<MudCardContent Class="pa-2" Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
|
||||
@if (this.waitingForDataSources)
|
||||
{
|
||||
<MudSkeleton Width="30%" Height="42px;"/>
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
else if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-2">
|
||||
@T("You haven't configured any data sources. To grant the AI access to your data, you need to add such a source. However, if you wish to use data from your device, you first have to set up a so-called embedding. This embedding is necessary so the AI can effectively search your data, find and retrieve the correct information required for each task. In addition to local data, you can also incorporate your company's data. To do so, your company must provide the data through an ERI (External Retrieval Interface).")
|
||||
</MudJustifiedText>
|
||||
|
||||
@@ -57,51 +57,51 @@
|
||||
}
|
||||
else if (this.showDataSourceSelection)
|
||||
{
|
||||
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged"/>
|
||||
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Dense="@true"/>
|
||||
@if (this.areDataSourcesEnabled)
|
||||
{
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged"/>
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Dense="@true"/>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||
{
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged"/>
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Dense="@true"/>
|
||||
}
|
||||
|
||||
@switch (this.aiBasedSourceSelection)
|
||||
{
|
||||
case true when this.availableDataSources.Count == 0:
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudText Typo="Typo.body2" Class="mb-2">
|
||||
@T("Your data sources cannot be used with the selected providers due to data privacy or confidence-level requirements, or they are currently unavailable.")
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case true when this.DataSourcesAISelected.Count == 0:
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudText Typo="Typo.body2" Class="mb-2">
|
||||
@T("The AI evaluates each of your inputs to determine whether and which data sources are necessary. Currently, the AI has not selected any source.")
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case false when this.availableDataSources.Count == 0:
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
<MudText Typo="Typo.body2" Class="mb-2">
|
||||
@T("Your data sources cannot be used with the selected providers due to data privacy or confidence-level requirements, or they are currently unavailable.")
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
case false:
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-2" Disabled="@this.aiBasedSourceSelection">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@this.SelectionChanged" Style="max-height: 14em;">
|
||||
@foreach (var source in this.availableDataSources)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Style="min-width: 0; width: 100%;">
|
||||
<MudText Typo="Typo.body1" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
<MudText Typo="Typo.body2" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
@source.Name
|
||||
</MudText>
|
||||
@if (source is IInternalDataSource internalSource)
|
||||
{
|
||||
<MudSpacer/>
|
||||
<MudTooltip Text="@internalSource.ConfidenceLevel.GetName()">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -113,20 +113,20 @@
|
||||
|
||||
case true:
|
||||
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="@T("Available Data Sources")">
|
||||
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" IconSize="Size.Small" HeaderTypo="Typo.subtitle1" HeaderClass="expansion-panel-header-compact" HeaderText="@T("Available Data Sources")">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
|
||||
@foreach (var source in this.availableDataSources)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Style="min-width: 0; width: 100%;">
|
||||
<MudText Typo="Typo.body1" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
<MudText Typo="Typo.body2" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
@source.Name
|
||||
</MudText>
|
||||
@if (source is IInternalDataSource internalSource)
|
||||
{
|
||||
<MudSpacer/>
|
||||
<MudTooltip Text="@internalSource.ConfidenceLevel.GetName()">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -134,21 +134,21 @@
|
||||
}
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="@T("AI-Selected Data Sources")">
|
||||
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" IconSize="Size.Small" HeaderTypo="Typo.subtitle1" HeaderClass="expansion-panel-header-compact" HeaderText="@T("AI-Selected Data Sources")">
|
||||
<MudList T="DataSourceAgentSelected" Dense="@true" Class="data-source-rows" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
|
||||
@foreach (var source in this.DataSourcesAISelected)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
<ChildContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Style="min-width: 0; width: 100%;">
|
||||
<MudText Typo="Typo.body1" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
<MudText Typo="Typo.body2" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
@source.DataSource.Name
|
||||
</MudText>
|
||||
@if (source.DataSource is IInternalDataSource internalSource)
|
||||
{
|
||||
<MudSpacer/>
|
||||
<MudTooltip Text="@internalSource.ConfidenceLevel.GetName()">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -165,11 +165,24 @@
|
||||
</MudExpansionPanels>
|
||||
break;
|
||||
}
|
||||
|
||||
@if (!this.aiBasedSourceSelection && this.GetUnavailablePreselectedDataSources().Count > 0)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2" Color="Color.Warning">
|
||||
@T("These data sources are preselected, but cannot be used right now, either due to data privacy or confidence-level requirements, or because they are unavailable:")
|
||||
</MudJustifiedText>
|
||||
<ul class="unavailable-data-sources mb-3 mt-1">
|
||||
@foreach (var source in this.GetUnavailablePreselectedDataSources())
|
||||
{
|
||||
<li>@source.Name</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideDataSourceSelection())">
|
||||
<MudButton Variant="Variant.Filled" OnClick="@this.HideDataSourceSelection">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
@@ -187,7 +200,7 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.ConfigurationHeaderMessage))
|
||||
{
|
||||
<MudText Typo="Typo.body1">
|
||||
<MudText Typo="Typo.body2">
|
||||
@this.ConfigurationHeaderMessage
|
||||
</MudText>
|
||||
}
|
||||
@@ -198,19 +211,19 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Disabled="@this.IsPreselectedDataSourcesAutomaticSelectionLocked()"/>
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Disabled="@this.IsPreselectedDataSourcesAutomaticValidationLocked()"/>
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-3" Disabled="@(this.aiBasedSourceSelection || this.IsPreselectedDataSourceIdsLocked())">
|
||||
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" ReadOnly="@this.IsPreselectedDataSourceIdsLocked()">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" ReadOnly="@this.IsPreselectedDataSourceIdsLocked()">
|
||||
@foreach (var source in this.availableDataSources)
|
||||
{
|
||||
<MudListItem Value="@source">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Style="min-width: 0; width: 100%;">
|
||||
<MudText Typo="Typo.body1" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
<MudText Typo="Typo.body2" Style="min-width: 0; white-space: normal; overflow-wrap: anywhere;">
|
||||
@source.Name
|
||||
</MudText>
|
||||
@if (source is IInternalDataSource internalSource)
|
||||
{
|
||||
<MudSpacer/>
|
||||
<MudTooltip Text="@internalSource.ConfidenceLevel.GetName()">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Small" Class="confidence-icon" Style="@this.GetConfidenceIconStyle(internalSource)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -220,4 +233,4 @@ else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
|
||||
</MudField>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,22 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
var preselectedDataSourceIds = this.DataSourceOptions.PreselectedDataSourceIds.ToHashSet(StringComparer.Ordinal);
|
||||
return this.GetConfiguredDataSourcesSnapshot().Where(ds => preselectedDataSourceIds.Contains(ds.Id)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects the preselected data sources which the filters removed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The list of available sources shows what survived the filters, while the preselection keeps
|
||||
/// what the user asked for. Without this, a preselected source which cannot be used right now
|
||||
/// is simply missing from that list, and nothing says so. Preselected ids without a configured
|
||||
/// source are left out: that source is gone, not unavailable.
|
||||
/// </remarks>
|
||||
/// <returns>The unusable preselected data sources, or an empty list when there are none.</returns>
|
||||
private IReadOnlyList<IDataSource> GetUnavailablePreselectedDataSources()
|
||||
{
|
||||
var availableDataSourceIds = this.availableDataSources.Select(ds => ds.Id).ToHashSet(StringComparer.Ordinal);
|
||||
return this.GetDataSourcesFromConfiguredIds().Where(ds => !availableDataSourceIds.Contains(ds.Id)).ToList();
|
||||
}
|
||||
|
||||
private async Task LoadAndApplyFilters()
|
||||
{
|
||||
@@ -200,8 +216,12 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
this.waitingForDataSources = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Load the data sources:
|
||||
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.DataSourceOptions, this.selectedDataSources);
|
||||
//
|
||||
// Load the data sources. We ask with the preselection rather than with the field below:
|
||||
// that field holds what was usable the last time we looked, so a source filtered out once
|
||||
// would never come back, while the RAG process keeps reading it from the preselection.
|
||||
//
|
||||
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.DataSourceOptions, this.GetDataSourcesFromConfiguredIds());
|
||||
if (generation != this.loadAndApplyFiltersGeneration)
|
||||
return;
|
||||
|
||||
@@ -242,7 +262,16 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
private async Task SelectionChanged(IReadOnlyCollection<IDataSource>? chosenDataSources)
|
||||
{
|
||||
this.selectedDataSources = chosenDataSources ?? [];
|
||||
this.DataSourceOptions.PreselectedDataSourceIds = this.selectedDataSources.Select(ds => ds.Id).ToList();
|
||||
|
||||
//
|
||||
// The list offers only the data sources which survived the filters, so what the user picks
|
||||
// there says nothing about the preselected ones it could not show. Those are kept: dropping
|
||||
// them would undo a choice the user never revisited, and it is these ids -- not this list --
|
||||
// which the RAG process reads when an answer is created. The query has to run before the
|
||||
// assignment, because it reads what we are about to replace.
|
||||
//
|
||||
var keptDataSourceIds = this.GetUnavailablePreselectedDataSources().Select(ds => ds.Id).ToList();
|
||||
this.DataSourceOptions.PreselectedDataSourceIds = [..keptDataSourceIds, ..this.selectedDataSources.Select(ds => ds.Id)];
|
||||
|
||||
await this.OptionsChanged();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* A plain list renders without markers and without indentation here: something in the global
|
||||
* styles takes both off. This is an enumeration of names and wants to read as one, so it states
|
||||
* marker, indentation and spacing itself. MudBlazor's Markdown styles fight the same fight for
|
||||
* their own lists, and need an !important on the display to win it -- hence the one below.
|
||||
*/
|
||||
.unavailable-data-sources {
|
||||
max-height: 10em;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
margin-top: 0;
|
||||
padding-left: 1.5em;
|
||||
list-style: disc outside;
|
||||
}
|
||||
|
||||
.unavailable-data-sources li {
|
||||
display: list-item !important;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<MudField Label="@this.Label" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.Disabled">
|
||||
<MudSwitch T="bool" Value="@this.Value" ValueChanged="@this.ValueChanged" Color="@this.Color" Validation="@this.Validation" Disabled="@this.Disabled">
|
||||
<MudField Label="@this.Label" Variant="Variant.Outlined" Class="@this.FieldClasses" Disabled="@this.Disabled">
|
||||
<MudSwitch T="bool" Size="@this.SwitchSize" Value="@this.Value" ValueChanged="@this.ValueChanged" Color="@this.Color" Validation="@this.Validation" Disabled="@this.Disabled">
|
||||
@(this.Value ? this.LabelOn : this.LabelOff)
|
||||
</MudSwitch>
|
||||
</MudField>
|
||||
@@ -27,4 +27,19 @@ public partial class MudTextSwitch : ComponentBase
|
||||
|
||||
[Parameter]
|
||||
public string LabelOff { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to render this switch in its compact form.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For places which stack several of these switches above other content, such as the data source
|
||||
/// selection the chat opens from its footer. The roomy form stays the default, so that nothing
|
||||
/// changes where this was never asked for.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool Dense { get; set; }
|
||||
|
||||
private string FieldClasses => this.Dense ? "mb-2 text-switch-dense" : "mb-3";
|
||||
|
||||
private Size SwitchSize => this.Dense ? Size.Small : Size.Medium;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
|
||||
<ChildContent>
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3">
|
||||
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="@this.ChipClass">
|
||||
@T("Beta")
|
||||
</MudChip>
|
||||
</ChildContent>
|
||||
|
||||
@@ -7,5 +7,16 @@ public partial class PreviewBeta : MSGComponentBase
|
||||
[Parameter]
|
||||
public bool ApplyInnerScrollingFix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional class names for the chip itself, separated by space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default is the margin every caller relied on before this parameter existed, because the
|
||||
/// chip usually sits on a line of its own above a heading. A header which puts it beside the
|
||||
/// heading instead passes an empty value.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public string ChipClass { get; set; } = "mb-3";
|
||||
|
||||
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@* The class is what the Markdown renderer wraps its own output in, so the headings and the list
|
||||
keep the look they had while this list was Markdown. *@
|
||||
<div @ref="this.listElement" class="mud-markdown-body">
|
||||
@foreach (var group in this.groups)
|
||||
{
|
||||
@* A level-two heading was shown as h5 while this list was Markdown, because that is what
|
||||
Markdown.DefaultConfig overrides it to. The heading keeps that size here. *@
|
||||
<MudText Typo="Typo.h5">
|
||||
@group.Heading
|
||||
</MudText>
|
||||
<ul>
|
||||
@foreach (var entry in group.Entries)
|
||||
{
|
||||
<li>
|
||||
@($"[{entry.Number}] ")
|
||||
@if (entry.Document is { } document)
|
||||
{
|
||||
<MudTooltip Text="@T("Opens this document in the program your system uses for it")" Placement="Placement.Top">
|
||||
<MudLink Typo="Typo.body1" OnClick="@(() => this.OpenDocument(document))">
|
||||
@entry.Title
|
||||
</MudLink>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Show this file in the file manager of your system")" Placement="Placement.Top">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FolderOpen" Size="Size.Small" OnClick="@(() => this.ShowInFileManager(document))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudLink Href="@entry.Link" Target="_blank" Typo="Typo.body1">
|
||||
@entry.Title
|
||||
</MudLink>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,164 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the sources an answer rests on, grouped and numbered the way the export is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// The sources to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IList<Source> Sources { get; set; } = [];
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<SourcesList> Logger { get; init; } = null!;
|
||||
|
||||
private readonly List<SourceEntryGroup> groups = [];
|
||||
|
||||
private ElementReference listElement;
|
||||
|
||||
/// <summary>
|
||||
/// Brings this list into view.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Reads the sources once per render instead of once per entry and render.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
private void RebuildGroups()
|
||||
{
|
||||
this.groups.Clear();
|
||||
foreach (var group in this.Sources.GroupSources())
|
||||
{
|
||||
var entries = new List<SourceEntry>(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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a document in the program the system uses for it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="document">The document to open, and the page to show.</param>
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the file browser of the system and selects the document in it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="document">The document to show.</param>
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One group of the list, prepared so that the markup only has to show it.
|
||||
/// </summary>
|
||||
/// <param name="Heading">The heading above the group.</param>
|
||||
/// <param name="Entries">The entries of the group, in the order they are shown.</param>
|
||||
private readonly record struct SourceEntryGroup(string Heading, IReadOnlyList<SourceEntry> Entries);
|
||||
|
||||
/// <summary>
|
||||
/// One entry of the list, prepared so that the markup only has to show it.
|
||||
/// </summary>
|
||||
/// <param name="Number">The number the source is listed under.</param>
|
||||
/// <param name="Title">The title of the source.</param>
|
||||
/// <param name="Link">The address of the source, which a web source is opened by.</param>
|
||||
/// <param name="Document">The document the source names, or null when it names none.</param>
|
||||
private readonly record struct SourceEntry(int Number, string Title, string Link, SourceDocumentLocation? Document);
|
||||
}
|
||||
@@ -58,11 +58,15 @@
|
||||
Disabled="@this.IsRowDisabled(item)" OnClick="@(async () => await this.ToggleToolFromRow(item))">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
@*
|
||||
The switch only shows the state; the surrounding button does the switching.
|
||||
A checkbox rather than a switch, because this row is one entry of a set the
|
||||
user picks from, not a setting of its own -- the same question the data source
|
||||
selection next to it asks, and it should not look like a different one.
|
||||
|
||||
The checkbox only shows the state; the surrounding button does the switching.
|
||||
It therefore takes no pointer events at all: its label reaches past the visible
|
||||
switch and would otherwise swallow the clicks landing in that strip.
|
||||
box and would otherwise swallow the clicks landing in that strip.
|
||||
*@
|
||||
<MudSwitch T="bool" Size="Size.Small" Color="Color.Primary" Value="@isSelected" ReadOnly="@true" Disabled="@this.IsRowDisabled(item)" Style="pointer-events: none;" />
|
||||
<MudCheckBox T="bool" Size="Size.Small" Dense="@true" Color="Color.Primary" Value="@isSelected" ReadOnly="@true" Disabled="@this.IsRowDisabled(item)" Style="pointer-events: none;" />
|
||||
<MudIcon Icon="@item.Implementation.Icon" Color="Color.Info" />
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user