mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-06 16:49:40 +00:00
Merge branch 'main' into chunk-data
Resolved 29 conflicting files. The notable decisions: Confidence: main's tool-calling gate (RequiredProviderConfidence) and this branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule on the same axis, so they are now one field. Both tool results and data sources raise it through RequireProviderConfidence(). The gate checks the level strictly and no longer exempts providers trusted by configuration: TrustedProviderIds is documented as applying to data-source security checks only, and organizations set confidence through DataConfidence .CustomConfidenceScheme instead. The security axis (DataSecurity, ERI, IsTrustedForDataSourceSecurityChecks) is unchanged. Provider creation: main's CreateProvider signature won (hfEndpointKind, capabilityOverrides, no model parameter); tokenizerPath was added to it and is set for every provider, including the new Hetzner, IONOS and LiteLLM. Provider and EmbeddingProvider combine the record parameters, Lua parsing and Lua serialization of both sides. File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the runtime already reads. Both sides had added a conflicting HTML filter; the reading family keeps the name, and the export path uses a narrow HTML_DOCUMENT, following the existing LATEX/TEX split. Runtime: main's file_data.rs is the base, including the prompt-injection sanitizer and the extraction routes. Token counting and chunk segmentation moved into take_released, so they act on the text the filter has released rather than on text it is still holding. A failed count is logged and left out instead of ending the extraction, because the app counts such a segment itself. Data sources: the participating-provider checks of this branch are kept, and main's GetAllowedDataSources overload now builds on them. DirectChatService resolves the launched chat's data source options before the check, so filter and chat see the same options. .NET and Rust both build clean; I18N regenerated to 4060 keys.
This commit is contained in:
640 files changed
+45059
-4952
No files matched your search
@@ -3,13 +3,6 @@
|
||||
<MudDialog DefaultFocus="DefaultFocus.None">
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
@if (!string.IsNullOrWhiteSpace(this.issue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true">
|
||||
@this.issue
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (this.isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
@@ -35,6 +28,12 @@
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@if (!string.IsNullOrWhiteSpace(this.issue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true" Class="mx-3">
|
||||
@this.issue
|
||||
</MudAlert>
|
||||
}
|
||||
<MudButton OnClick="@this.Cancel" Disabled="@this.isSaving" Size="Size.Small">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
|
||||
@@ -6,8 +6,6 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public sealed record AssistantPluginEditorDialogResult(Guid PluginId, string PluginName);
|
||||
|
||||
public partial class AssistantPluginEditorDialog : MSGComponentBase
|
||||
{
|
||||
[Inject]
|
||||
@@ -29,7 +27,7 @@ public partial class AssistantPluginEditorDialog : MSGComponentBase
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
||||
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public Guid PluginId { get; set; }
|
||||
@@ -72,6 +70,14 @@ public partial class AssistantPluginEditorDialog : MSGComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
// An assistant an organization rolled out must keep the content its enterprise approval
|
||||
// was granted for, so only its IT department may change it:
|
||||
if (this.plugin.IsManagedByConfigServer)
|
||||
{
|
||||
this.issue = T("Only locally managed assistant plugins can be edited.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.pluginFile = Path.Join(this.plugin.LocalPath, PLUGIN_FILE_NAME);
|
||||
if (!File.Exists(this.pluginFile))
|
||||
{
|
||||
@@ -105,7 +111,7 @@ public partial class AssistantPluginEditorDialog : MSGComponentBase
|
||||
try
|
||||
{
|
||||
var editedLua = await this.codeEditor.GetCodeAsync();
|
||||
var result = await this.AssistantPluginInstallService.UpdateInstalledAssistantAsync(this.plugin, editedLua, CancellationToken.None);
|
||||
var result = await this.PluginInstallService.UpdateInstalledAssistantAsync(this.plugin, editedLua, CancellationToken.None);
|
||||
if (!result.Success)
|
||||
{
|
||||
LOGGER.LogError($"Failed to update assistant plugin '{result.PluginName}' ({result.PluginId}) in '{result.PluginDirectory}' with issue '{result.Issue}'.");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public sealed record AssistantPluginEditorDialogResult(Guid PluginId, string PluginName);
|
||||
@@ -9,8 +9,6 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public sealed record AssistantPluginRevisionDialogResult(Guid PluginId, string PluginName, PluginAssistantAudit? Audit);
|
||||
|
||||
public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
||||
{
|
||||
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
||||
@@ -23,7 +21,7 @@ public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
||||
private AssistantPluginGenerationService AssistantPluginGenerationService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
||||
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||
@@ -144,7 +142,7 @@ public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
||||
if (this.availablePlugin is null)
|
||||
return;
|
||||
|
||||
this.revisionCheckResult = await this.AssistantPluginInstallService.CheckInstalledAssistantUpdateAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
||||
this.revisionCheckResult = await this.PluginInstallService.CheckInstalledAssistantUpdateAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
||||
if (this.revisionCheckResult.Success)
|
||||
return;
|
||||
|
||||
@@ -168,7 +166,7 @@ public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
||||
|
||||
try
|
||||
{
|
||||
var result = await this.AssistantPluginInstallService.UpdateInstalledAssistantAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
||||
var result = await this.PluginInstallService.UpdateInstalledAssistantAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
||||
if (!result.Success)
|
||||
{
|
||||
LOGGER.LogError($"Failed to revise assistant plugin '{result.PluginName}' ({result.PluginId}) in '{result.PluginDirectory}' with issue '{result.Issue}'.");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public sealed record AssistantPluginRevisionDialogResult(Guid PluginId, string PluginName, PluginAssistantAudit? Audit);
|
||||
@@ -0,0 +1,34 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("There is already a log of a previous batch run in the output folder.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@(string.Format(T("{0} document(s) were processed successfully. {1} document(s) are missing or failed."), this.NumCompletedFiles, this.NumRemainingFiles))
|
||||
</MudJustifiedText>
|
||||
|
||||
@if (this.NumMissingResults > 0)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@(string.Format(T("Please note: the log lists {0} more document(s) as successfully processed, but their results no longer exist. They count as missing and are processed again when you continue the run."), this.NumMissingResults))
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@T("Would you like to continue the previous run and process only the missing and failed documents? Or would you like to start a completely new run, which processes all documents again?")
|
||||
</MudJustifiedText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Restart" Variant="Variant.Filled" Color="Color.Error">
|
||||
@T("Start a new run")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Continue" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@T("Continue the previous run")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,41 @@
|
||||
using AIStudio.Assistants.BatchProcessing;
|
||||
using AIStudio.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Asks the user whether a previous batch run should be continued or started from scratch.
|
||||
/// </summary>
|
||||
public partial class BatchProcessingResumeDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The number of documents which were processed successfully during the previous run.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int NumCompletedFiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of documents which still need to be processed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int NumRemainingFiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of documents which the log lists as successfully processed,
|
||||
/// but whose results no longer exist. They count as remaining and are
|
||||
/// processed again when the run is continued.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int NumMissingResults { get; set; }
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Continue() => this.MudDialog.Close(DialogResult.Ok(BatchProcessingResumeDecision.CONTINUE));
|
||||
|
||||
private void Restart() => this.MudDialog.Close(DialogResult.Ok(BatchProcessingResumeDecision.RESTART));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@(string.Format(T("Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files."), this.PluginName))
|
||||
</MudJustifiedText>
|
||||
|
||||
@if (this.Consequences.Count > 0)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-1">
|
||||
@T("This also removes everything the configuration plugin had set up:")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudList T="string" Class="mb-3">
|
||||
@foreach (var consequence in this.Consequences)
|
||||
{
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.RemoveCircleOutline" IconColor="Color.Error">
|
||||
@consequence
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@T("You can install the plugin again later, but any changes you made to its settings are lost.")
|
||||
</MudJustifiedText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("No")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Error">
|
||||
@T("Yes, delete it")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,69 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Asks the user whether a local configuration plugin may be deleted, and shows what the deletion
|
||||
/// takes with it.
|
||||
/// </summary>
|
||||
public partial class ConfigurationPluginDeleteDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the configuration plugin about to be deleted.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string PluginName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// What the deletion removes besides the plugin directory.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public ConfigurationPluginDeleteSummary Summary { get; set; } = ConfigurationPluginDeleteSummary.EMPTY;
|
||||
|
||||
private List<string> Consequences => this.BuildConsequences();
|
||||
|
||||
/// <summary>
|
||||
/// Turns the summary into the lines shown to the user. Only what is actually affected is listed,
|
||||
/// so the dialog stays short for a configuration plugin that just locks a single setting.
|
||||
/// </summary>
|
||||
private List<string> BuildConsequences()
|
||||
{
|
||||
var consequences = new List<string>();
|
||||
var summary = this.Summary;
|
||||
|
||||
Add(summary.LlmProviders, this.T("{0} LLM provider"), this.T("{0} LLM providers"));
|
||||
Add(summary.TranscriptionProviders, this.T("{0} transcription provider"), this.T("{0} transcription providers"));
|
||||
Add(summary.EmbeddingProviders, this.T("{0} embedding provider"), this.T("{0} embedding providers"));
|
||||
Add(summary.ChatTemplates, this.T("{0} chat template"), this.T("{0} chat templates"));
|
||||
Add(summary.Profiles, this.T("{0} profile"), this.T("{0} profiles"));
|
||||
Add(summary.DocumentAnalysisPolicies, this.T("{0} document analysis policy"), this.T("{0} document analysis policies"));
|
||||
Add(summary.MandatoryInfos, this.T("{0} mandatory information"), this.T("{0} mandatory informations"));
|
||||
Add(summary.Introductions, this.T("{0} introduction on the welcome page"), this.T("{0} introductions on the welcome page"));
|
||||
|
||||
// Data sources are called out separately: removing them also deletes their credentials from
|
||||
// the operating system's keychain, which the user cannot undo by reinstalling the plugin.
|
||||
Add(summary.DataSources,
|
||||
this.T("{0} data source, including its credentials in your operating system's keychain"),
|
||||
this.T("{0} data sources, including their credentials in your operating system's keychain"));
|
||||
|
||||
Add(summary.LockedSettings, this.T("{0} setting returns to its default value"), this.T("{0} settings return to their default values"));
|
||||
|
||||
return consequences;
|
||||
|
||||
void Add(int count, string singular, string plural)
|
||||
{
|
||||
if (count > 0)
|
||||
consequences.Add(string.Format(count == 1 ? singular : plural, count));
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
@@ -15,7 +15,7 @@ using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDisposable, ISecretId
|
||||
public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, ISecretId
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
@@ -186,9 +186,9 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
protected override async ValueTask DisposeResourcesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -78,7 +78,14 @@
|
||||
@foreach (var embedding in this.AvailableEmbeddings)
|
||||
{
|
||||
<MudSelectItem Value="@embedding.Value">
|
||||
@embedding.Name
|
||||
@if (this.GetEmbeddingProvider(embedding.Value) is { } provider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@provider.UsedLLMProvider" CustomIconDataUrl="@provider.CustomIconDataUrl" Text="@embedding.Name" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@embedding.Name
|
||||
}
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@@ -108,6 +108,12 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private EmbeddingProvider? GetEmbeddingProvider(string providerId)
|
||||
{
|
||||
var provider = this.SettingsManager.GetEmbeddingProviderById(providerId);
|
||||
return provider == EmbeddingProvider.NONE ? null : provider;
|
||||
}
|
||||
|
||||
private EmbeddingProvider? SelectedEmbedding => this.SettingsManager.ConfigurationData.EmbeddingProviders
|
||||
.FirstOrDefault(x => x.Id == this.dataEmbeddingId);
|
||||
|
||||
@@ -10,7 +10,7 @@ using Timer = System.Timers.Timer;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase, IAsyncDisposable
|
||||
public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
@@ -89,9 +89,9 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase, IAsy
|
||||
this.MudDialog.Close();
|
||||
}
|
||||
|
||||
#region Implementation of IDisposable
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
protected override async ValueTask DisposeResourcesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -78,7 +78,14 @@
|
||||
@foreach (var embedding in this.AvailableEmbeddings)
|
||||
{
|
||||
<MudSelectItem Value="@embedding.Value">
|
||||
@embedding.Name
|
||||
@if (this.GetEmbeddingProvider(embedding.Value) is { } provider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@provider.UsedLLMProvider" CustomIconDataUrl="@provider.CustomIconDataUrl" Text="@embedding.Name" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@embedding.Name
|
||||
}
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@@ -108,6 +108,12 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private EmbeddingProvider? GetEmbeddingProvider(string providerId)
|
||||
{
|
||||
var provider = this.SettingsManager.GetEmbeddingProviderById(providerId);
|
||||
return provider == EmbeddingProvider.NONE ? null : provider;
|
||||
}
|
||||
|
||||
private EmbeddingProvider? SelectedEmbedding => this.SettingsManager.ConfigurationData.EmbeddingProviders
|
||||
.FirstOrDefault(x => x.Id == this.dataEmbeddingId);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog DefaultFocus="DefaultFocus.None">
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
@if (!string.IsNullOrWhiteSpace(this.issue))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="@true">
|
||||
@this.issue
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (this.isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="@true" Color="Color.Primary"/>
|
||||
}
|
||||
else if (this.assistantPlugin is not null && this.canEdit)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
@T("This tile opens a chat directly, so there is nothing to prompt for: pick what the chat should start with. AI Studio rewrites the plugin itself, without asking a model.")
|
||||
</MudText>
|
||||
|
||||
<MudForm @ref="@this.form">
|
||||
@* The dashed frame shows that these fields belong together: they describe one
|
||||
chat the launcher tile opens. *@
|
||||
<MudPaper Class="pa-3 border-dashed border rounded-lg">
|
||||
<MudTextField T="string" @bind-Text="@this.pluginName" Validation="@this.ValidatePluginName" AdornmentIcon="@Icons.Material.Filled.Extension" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Plugin name")" HelperText="@T("The name shown on the plugins page.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" Disabled="@this.IsBusy"/>
|
||||
<MudTextField T="string" @bind-Text="@this.title" Validation="@this.ValidateTitle" AdornmentIcon="@Icons.Material.Filled.Assistant" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Tile title")" HelperText="@T("The title shown on the tile.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" Disabled="@this.IsBusy"/>
|
||||
<MudTextField T="string" @bind-Text="@this.description" Validation="@this.ValidateDescription" AdornmentIcon="@Icons.Material.Filled.Notes" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Description")" HelperText="@T("Shown on the tile and on the plugins page.")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="2" AutoGrow="@true" MaxLines="6" Class="mb-3" Disabled="@this.IsBusy"/>
|
||||
<DirectChatLauncherForm @bind-WorkspaceName="@this.workspaceName"
|
||||
@bind-ProviderId="@this.providerId"
|
||||
@bind-ProfileId="@this.profileId"
|
||||
@bind-ChatTemplateId="@this.chatTemplateId"
|
||||
@bind-DataSourceIds="@this.dataSourceIds"
|
||||
@bind-ToolIds="@this.toolIds"
|
||||
ValidateWorkspaceName="@this.ValidateWorkspaceName"/>
|
||||
</MudPaper>
|
||||
</MudForm>
|
||||
|
||||
@* The panel content is only built while it is open, so the plugin is written just
|
||||
for users who want to look at it. *@
|
||||
<MudExpansionPanels Dense="@true" Elevation="0">
|
||||
<MudExpansionPanel Dense="@true" Class="border-solid border rounded pt-n4" Style="border-color: #BDBDBD">
|
||||
<TitleContent>
|
||||
<div class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Code" Class="mr-3" Color="Color.Primary"/>
|
||||
<MudText Typo="Typo.button">
|
||||
@T("Resulting Lua plugin")
|
||||
</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudTextField T="string" Text="@this.BuildLua()" ReadOnly="@true" Variant="Variant.Outlined" Lines="18" Class="mt-2" Style="font-family: monospace"/>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
|
||||
@if (this.IsBusy)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="@true" Color="Color.Primary"/>
|
||||
<MudText Typo="Typo.body2">
|
||||
@(this.isAuditing ? T("Running security audit...") : T("Saving the tile..."))
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Disabled="@this.IsBusy" Size="Size.Small">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@(async () => await this.SaveAsync())"
|
||||
Disabled="@(!this.CanSave)"
|
||||
Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
StartIcon="@Icons.Material.Filled.Save"
|
||||
Size="Size.Small">
|
||||
@T("Save tile")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,280 @@
|
||||
using System.Text;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Changes the settings of an installed direct chat launcher without asking a model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A launcher has no prompt and no form, so every change a user can make here is a different pick
|
||||
/// from a drop-down. The dialog therefore writes the plugin itself through
|
||||
/// DirectChatLauncherLuaWriter and reuses the regular assistant update path for validating,
|
||||
/// writing, and rolling back.
|
||||
/// </remarks>
|
||||
public partial class DirectChatLauncherSettingsDialog : MSGComponentBase
|
||||
{
|
||||
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(DirectChatLauncherSettingsDialog));
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public Guid PluginId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string PluginLocalPath { get; set; } = string.Empty;
|
||||
|
||||
private IAvailablePlugin? availablePlugin;
|
||||
private PluginAssistants? assistantPlugin;
|
||||
private MudForm? form;
|
||||
private string pluginName = string.Empty;
|
||||
private string title = string.Empty;
|
||||
private string description = string.Empty;
|
||||
private string workspaceName = string.Empty;
|
||||
private string providerId = string.Empty;
|
||||
private string profileId = string.Empty;
|
||||
private string chatTemplateId = string.Empty;
|
||||
private IEnumerable<string> dataSourceIds = [];
|
||||
private HashSet<string> toolIds = [];
|
||||
private string issue = string.Empty;
|
||||
private bool canEdit;
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
private bool isAuditing;
|
||||
|
||||
private bool IsBusy => this.isSaving || this.isAuditing;
|
||||
|
||||
private bool CanSave => this.canEdit && this.assistantPlugin is not null && this.availablePlugin is not null && !this.isLoading && !this.IsBusy;
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.availablePlugin = PluginFactory.AvailablePlugins
|
||||
.OfType<IAvailablePlugin>()
|
||||
.FirstOrDefault(x => x.Id == this.PluginId && AreSamePath(x.LocalPath, this.PluginLocalPath));
|
||||
|
||||
this.assistantPlugin = PluginFactory.RunningPlugins
|
||||
.OfType<PluginAssistants>()
|
||||
.FirstOrDefault(x => x.Id == this.PluginId && AreSamePath(x.PluginPath, this.PluginLocalPath));
|
||||
|
||||
if (this.availablePlugin is null || this.assistantPlugin is null)
|
||||
{
|
||||
this.issue = T("The assistant plugin could not be resolved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DirectChatLauncherLuaWriter.CanRewrite(this.assistantPlugin) || this.assistantPlugin.ChatLaunchConfiguration is not { } launch)
|
||||
{
|
||||
this.issue = T("Only locally managed direct chat launchers can be edited here.");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Saving replaces the whole plugin.lua. Anything the file carries beyond the canonical
|
||||
// launcher shape would be lost, so those plugins keep the code editor and the AI
|
||||
// revision instead of this dialog:
|
||||
//
|
||||
var pluginFile = Path.Join(this.availablePlugin.LocalPath, PLUGIN_FILE_NAME);
|
||||
if (!File.Exists(pluginFile))
|
||||
{
|
||||
this.issue = T("The plugin.lua file could not be found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentLua = await File.ReadAllTextAsync(pluginFile, Encoding.UTF8);
|
||||
if (DirectChatLauncherLuaWriter.HasCompanionLuaFiles(this.assistantPlugin) || !DirectChatLauncherLuaWriter.IsCanonicalSource(currentLua))
|
||||
{
|
||||
this.issue = T("This launcher contains its own icon or additional Lua code. Please edit it with the plugin code editor, so nothing of it gets lost.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.pluginName = this.assistantPlugin.Name;
|
||||
this.title = this.assistantPlugin.AssistantTitle;
|
||||
this.description = string.IsNullOrWhiteSpace(this.assistantPlugin.Description)
|
||||
? this.assistantPlugin.AssistantDescription
|
||||
: this.assistantPlugin.Description;
|
||||
|
||||
this.workspaceName = launch.WorkspaceName;
|
||||
this.providerId = launch.ProviderId?.ToString() ?? string.Empty;
|
||||
this.profileId = launch.ProfileId?.ToString() ?? string.Empty;
|
||||
this.chatTemplateId = launch.ChatTemplateId?.ToString() ?? string.Empty;
|
||||
this.dataSourceIds = launch.DataSourceIds?.Select(id => id.ToString()).ToArray() ?? [];
|
||||
this.toolIds = launch.ToolIds is null ? [] : [..launch.ToolIds];
|
||||
this.canEdit = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.issue = string.Format(T("The assistant plugin could not be loaded: {0}"), e.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string BuildLua() => this.assistantPlugin is null
|
||||
? string.Empty
|
||||
: DirectChatLauncherLuaWriter.Write(this.assistantPlugin, this.BuildDefinition());
|
||||
|
||||
private DirectChatLauncherDefinition BuildDefinition() => new(
|
||||
this.pluginName.Trim(),
|
||||
this.title.Trim(),
|
||||
this.description.Trim(),
|
||||
this.BuildLaunchConfiguration());
|
||||
|
||||
private AssistantChatLaunchConfiguration BuildLaunchConfiguration()
|
||||
{
|
||||
var selectedDataSourceIds = this.dataSourceIds
|
||||
.Select(id => Guid.TryParse(id, out var parsed) ? parsed : Guid.Empty)
|
||||
.Where(id => id != Guid.Empty)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
//
|
||||
// An empty selection means "use the chat defaults" and is left out of the plugin, whereas
|
||||
// the empty GUID explicitly selects no profile or no chat template:
|
||||
//
|
||||
return new(
|
||||
this.workspaceName.Trim(),
|
||||
ParseOptionalGuid(this.providerId),
|
||||
ParseOptionalGuid(this.profileId),
|
||||
ParseOptionalGuid(this.chatTemplateId),
|
||||
selectedDataSourceIds.Length == 0 ? null : selectedDataSourceIds,
|
||||
this.toolIds.Count == 0 ? null : this.toolIds.Order(StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (!this.CanSave || this.assistantPlugin is null || this.availablePlugin is null || this.form is null)
|
||||
return;
|
||||
|
||||
await this.form.Validate();
|
||||
if (!this.form.IsValid)
|
||||
return;
|
||||
|
||||
this.isSaving = true;
|
||||
this.issue = string.Empty;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var lua = DirectChatLauncherLuaWriter.Write(this.assistantPlugin, this.BuildDefinition());
|
||||
|
||||
//
|
||||
// The writer produces the plugin deterministically, but the update path is still the
|
||||
// authority: it validates the Lua, writes it atomically with a backup, and restores the
|
||||
// previous file when the reload fails.
|
||||
//
|
||||
var checkResult = await this.PluginInstallService.CheckInstalledAssistantUpdateAsync(this.availablePlugin, lua, CancellationToken.None);
|
||||
if (!checkResult.Success)
|
||||
{
|
||||
LOGGER.LogError($"The rewritten chat launcher '{this.pluginName}' ({this.PluginId}) is not valid. Issue: {checkResult.Issue}");
|
||||
this.issue = checkResult.Issue;
|
||||
return;
|
||||
}
|
||||
|
||||
var updateResult = await this.PluginInstallService.UpdateInstalledAssistantAsync(this.availablePlugin, lua, CancellationToken.None);
|
||||
if (!updateResult.Success)
|
||||
{
|
||||
LOGGER.LogError($"Failed to save the chat launcher '{updateResult.PluginName}' ({updateResult.PluginId}) in '{updateResult.PluginDirectory}'. Issue: {updateResult.Issue}");
|
||||
this.issue = updateResult.Issue;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Writing the file changes the audit hash, so a stored audit no longer applies:
|
||||
//
|
||||
PluginAssistantAudit? audit = null;
|
||||
if (this.SettingsManager.ConfigurationData.AssistantPluginAudit.AutomaticallyAuditAssistants)
|
||||
audit = await this.TryRunAuditAsync(updateResult.PluginId);
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(new DirectChatLauncherSettingsDialogResult(updateResult.PluginId, updateResult.PluginName, audit)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isSaving = false;
|
||||
if (!string.IsNullOrWhiteSpace(this.issue))
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<PluginAssistantAudit?> TryRunAuditAsync(Guid pluginId)
|
||||
{
|
||||
var updatedPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == pluginId);
|
||||
if (updatedPlugin is null)
|
||||
return null;
|
||||
|
||||
this.isAuditing = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
try
|
||||
{
|
||||
var audit = await this.AssistantPluginAuditService.RunAuditAsync(updatedPlugin);
|
||||
if (audit.Level is AssistantAuditLevel.UNKNOWN)
|
||||
return audit;
|
||||
|
||||
UpsertAudit(this.SettingsManager.ConfigurationData.AssistantPluginAudits, audit);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
return audit;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isAuditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string? ValidatePluginName(string value) => string.IsNullOrWhiteSpace(value) ? T("Please provide a name for this plugin.") : null;
|
||||
|
||||
private string? ValidateTitle(string value) => string.IsNullOrWhiteSpace(value) ? T("Please provide a title for this tile.") : null;
|
||||
|
||||
private string? ValidateDescription(string value) => string.IsNullOrWhiteSpace(value) ? T("Please provide a description for this tile.") : null;
|
||||
|
||||
private string? ValidateWorkspaceName(string value) => string.IsNullOrWhiteSpace(value) ? T("Please select or enter a workspace name for this tile.") : null;
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private static Guid? ParseOptionalGuid(string value) => Guid.TryParse(value, out var parsed) ? parsed : null;
|
||||
|
||||
private static void UpsertAudit(IList<PluginAssistantAudit> audits, PluginAssistantAudit audit)
|
||||
{
|
||||
var existingIndex = audits.ToList().FindIndex(x => x.PluginId == audit.PluginId);
|
||||
if (existingIndex >= 0)
|
||||
audits[existingIndex] = audit;
|
||||
else
|
||||
audits.Add(audit);
|
||||
}
|
||||
|
||||
private static bool AreSamePath(string left, string right)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
|
||||
return false;
|
||||
|
||||
var comparison = OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
return string.Equals(
|
||||
Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||
Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||
comparison);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public sealed record DirectChatLauncherSettingsDialogResult(Guid PluginId, string PluginName, PluginAssistantAudit? Audit);
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
@if (this.Document is null)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load file")" @bind-FileContent="@this.FileContent" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent Text="@T("Load file")" FileContent="@this.FileContent" FileContentChanged="@this.ApplyLoadedFileContent" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -33,8 +33,31 @@
|
||||
@T("The specified file could not be found. The file have been moved, deleted, renamed, or is otherwise inaccessible.")
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.isLoadingContent)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="my-3">
|
||||
@T("Please wait while we load the content of your file. Depending on the file type and size, this may take a moment.")
|
||||
</MudJustifiedText>
|
||||
<MudSkeleton Width="30%" Height="42px"/>
|
||||
<MudSkeleton Width="80%"/>
|
||||
<MudSkeleton Width="100%"/>
|
||||
<MudSkeleton Width="90%"/>
|
||||
}
|
||||
else if (this.loadFailureMessage is not null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled" Class="my-2">
|
||||
@this.loadFailureMessage
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (this.previewCutOffCharacters > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="my-2">
|
||||
@string.Format(T("Your document is large, so we show you only its beginning. We hide the remaining {0:N0} characters here. Rest assured: the AI still receives your entire document."), this.previewCutOffCharacters)
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" Outlined="true" PanelClass="pa-2" Class="mb-2">
|
||||
@if (this.Document?.IsImage ?? false)
|
||||
{
|
||||
@@ -54,14 +77,14 @@
|
||||
Class="ma-2 pe-4"
|
||||
HelperText="@T("This is the content we loaded from your file — including headings, lists, and formatting. Use this to verify your file loads as expected.")">
|
||||
<div style="max-height: 40vh; overflow-y: auto;">
|
||||
<MudMarkdown Value="@this.FileContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
<MudMarkdown Value="@this.previewContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</div>
|
||||
</MudField>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("Simple View")" Icon="@Icons.Material.Filled.Terminal">
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.FileContent"
|
||||
Text="@this.previewContent"
|
||||
AdornmentIcon="@Icons.Material.Filled.Article"
|
||||
Adornment="Adornment.Start"
|
||||
Immediate="@true"
|
||||
|
||||
@@ -20,40 +20,169 @@ public partial class DocumentCheckDialog : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public string FileContent { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// How many characters we show at most. Rendering a huge document costs us a large Markdown
|
||||
/// syntax tree and an equally large render tree. This dialog answers the question of how we
|
||||
/// read the file, though — the beginning of the document is enough for that, and the AI still
|
||||
/// receives the entire content.
|
||||
/// </summary>
|
||||
private const int PREVIEW_CHARACTER_LIMIT = 200_000;
|
||||
|
||||
/// <summary>
|
||||
/// Set when reading the file failed, so the dialog shows the reason instead of empty content.
|
||||
/// </summary>
|
||||
private string? loadFailureMessage;
|
||||
|
||||
/// <summary>
|
||||
/// What we show to the user: either the entire file content, or its beginning. We keep this in
|
||||
/// its own field so that we cut the content only once, instead of on every render.
|
||||
/// </summary>
|
||||
private string previewContent = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// How many characters we cut off from the preview. Zero when we show the entire content.
|
||||
/// </summary>
|
||||
private int previewCutOffCharacters;
|
||||
|
||||
/// <summary>
|
||||
/// Ends the extraction when this dialog is gone before the file was read completely.
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource extractionCancellation = new();
|
||||
|
||||
/// <summary>
|
||||
/// True once this dialog was disposed. The extraction runs across awaits, so it may return
|
||||
/// long after the user closed the dialog — it must not touch this component afterwards.
|
||||
/// </summary>
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// True while we extract the file content. Reading happens after the first render, so the
|
||||
/// dialog can tell the user that it is working instead of showing an empty document.
|
||||
/// </summary>
|
||||
private bool isLoadingContent;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
|
||||
[Inject]
|
||||
private ILogger<DocumentCheckDialog> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PandocAvailabilityService PandocAvailability { get; init; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
//
|
||||
// Decide before the first render whether we have to read the file at all. Images are shown
|
||||
// as they are, a missing file shows its own message, and content a caller already handed
|
||||
// us is reused instead of being extracted a second time:
|
||||
//
|
||||
this.isLoadingContent =
|
||||
this.Document is not null &&
|
||||
!this.Document.IsImage &&
|
||||
this.Document.Exists &&
|
||||
string.IsNullOrWhiteSpace(this.FileContent);
|
||||
|
||||
this.UpdatePreview();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && this.Document is not null)
|
||||
{
|
||||
if (!this.isLoadingContent)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!this.Document.IsImage)
|
||||
{
|
||||
var fileContent = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.DialogService);
|
||||
this.FileContent = fileContent;
|
||||
}
|
||||
var extraction = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.PandocAvailability, this.extractionCancellation.Token);
|
||||
if (this.isDisposed)
|
||||
return;
|
||||
|
||||
this.FileContent = extraction.Content;
|
||||
|
||||
//
|
||||
// This dialog exists so the user can check what we hand to the AI. Showing an
|
||||
// empty document when reading the file failed would answer that question wrong.
|
||||
//
|
||||
if (!extraction.HasUsableContent)
|
||||
this.loadFailureMessage = extraction.ToUserMessage(this.Document.FileName);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// The user closed this dialog while we were reading the file. Nothing left to do.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Failed to load file content from '{FilePath}'", this.Document);
|
||||
this.FileContent = string.Empty;
|
||||
this.loadFailureMessage = FileExtractionErrorCode.INTERNAL.ToUserMessage(this.Document.FileName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!this.isDisposed)
|
||||
{
|
||||
this.isLoadingContent = false;
|
||||
this.UpdatePreview();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
else if (firstRender)
|
||||
this.Logger.LogWarning("Document check dialog opened without a valid file path.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the user loads a file through this dialog. We don't use a two-way binding here,
|
||||
/// since we have to refresh the preview whenever the content changes.
|
||||
/// </summary>
|
||||
/// <param name="fileContent">The content of the file the user has loaded.</param>
|
||||
private void ApplyLoadedFileContent(string fileContent)
|
||||
{
|
||||
this.FileContent = fileContent;
|
||||
this.UpdatePreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines what part of the file content we show to the user.
|
||||
/// </summary>
|
||||
private void UpdatePreview()
|
||||
{
|
||||
if (this.FileContent.Length <= PREVIEW_CHARACTER_LIMIT)
|
||||
{
|
||||
this.previewContent = this.FileContent;
|
||||
this.previewCutOffCharacters = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// We cut at the last line break before our limit. Otherwise, we might tear apart a Markdown
|
||||
// construct like a table row or a code fence in the middle of a line:
|
||||
//
|
||||
var cutIndex = this.FileContent.LastIndexOf('\n', PREVIEW_CHARACTER_LIMIT - 1) + 1;
|
||||
if (cutIndex < 1)
|
||||
cutIndex = PREVIEW_CHARACTER_LIMIT;
|
||||
|
||||
this.previewContent = this.FileContent[..cutIndex];
|
||||
this.previewCutOffCharacters = this.FileContent.Length - cutIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a running extraction. Without this, reading a large document would continue after the
|
||||
/// user closed this dialog and would keep this component, the extracted content, and the
|
||||
/// response stream alive until the runtime is done.
|
||||
/// </summary>
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.isDisposed = true;
|
||||
this.extractionCancellation.Cancel();
|
||||
this.extractionCancellation.Dispose();
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
|
||||
|
||||
private MudMarkdownStyling MarkdownStyling => new()
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Provider.HuggingFace
|
||||
@using AIStudio.Provider.SelfHosted
|
||||
@using AIStudio.Tools.Rust
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@if (this.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-4">
|
||||
@T("This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
|
||||
</MudAlert>
|
||||
}
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
|
||||
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
|
||||
{
|
||||
if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE)
|
||||
{
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
<ProviderLabel ProviderType="@provider" CustomIconDataUrl="@(provider == this.DataLLMProvider ? this.DataCustomIconDataUrl : string.Empty)" Text="@provider.ToName()" />
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
@@ -39,13 +46,14 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.IsEmbeddingSupported())
|
||||
@@ -58,6 +66,24 @@
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProviderForEmbeddings">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsEmbeddings())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Hugging Face offers embeddings through a few of its inference providers only, which is why this list is shorter than the one for chatting.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
@if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost))
|
||||
@@ -70,13 +96,14 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateManuallyModel"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load")
|
||||
</MudButton>
|
||||
@if (this.availableModels.Count is 0)
|
||||
@@ -87,7 +114,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info" Adornment="Adornment.Start"
|
||||
Validation="@this.providerValidation.ValidatingModel">
|
||||
@@ -121,6 +148,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
@if (this.DataLLMProvider != LLMProviders.NONE)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -57,12 +58,24 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataCustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The embedding model to use.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Model DataModel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Hugging Face inference provider to use.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public HFInferenceProvider HFInferenceProviderId { get; set; } = HFInferenceProvider.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Should the dialog be in editing mode?
|
||||
@@ -78,7 +91,14 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
[Parameter]
|
||||
public int DataEmbeddingBatchSize { get; set; } = EmbeddingProvider.DEFAULT_EMBEDDING_BATCH_SIZE;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this embedding provider is managed by an enterprise configuration plugin. When true,
|
||||
/// every field except the API key is locked, matching Settings.EmbeddingProvider.IsEnterpriseConfiguration.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEnterpriseConfiguration { get; set; }
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
@@ -95,6 +115,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataAPIKey = string.Empty;
|
||||
private bool dataHadStoredAPIKeyOnLoad;
|
||||
private string dataManuallyModel = string.Empty;
|
||||
private string dataAPIKeyStorageIssue = string.Empty;
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
@@ -152,11 +173,13 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
Host = this.DataHost,
|
||||
IsEnterpriseConfiguration = false,
|
||||
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
TokenizerPath = this.dataFilePath,
|
||||
EmbeddingBatchSize = this.DataEmbeddingBatchSize,
|
||||
TokenLimit = this.DataTokenLimit,
|
||||
CustomIconDataUrl = this.DataCustomIconDataUrl,
|
||||
HFInferenceProvider = this.HFInferenceProviderId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -199,11 +222,17 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
// Load the API key:
|
||||
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
|
||||
if (requestedSecret.Success)
|
||||
{
|
||||
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.dataAPIKey = string.Empty;
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
|
||||
|
||||
// For an enterprise-managed provider, having no key yet is the expected first-run
|
||||
// state, not a storage failure -- the user is just about to set their own key:
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
|
||||
{
|
||||
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
|
||||
await this.form.Validate();
|
||||
@@ -228,8 +257,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||
|
||||
// Must mirror Settings.EmbeddingProvider.SecretId exactly: when editing an enterprise-managed
|
||||
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
|
||||
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
|
||||
// silently end up in the wrong keyring row and never be found again.
|
||||
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
|
||||
|
||||
public string SecretName => this.DataName;
|
||||
|
||||
#endregion
|
||||
@@ -276,6 +309,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHadStoredAPIKeyOnLoad = true;
|
||||
}
|
||||
else if (this.dataHadStoredAPIKeyOnLoad)
|
||||
{
|
||||
// The user cleared a previously stored key. Without this, the old key would simply
|
||||
// stay in the OS keyring untouched and keep being used:
|
||||
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||
if (!deleteResponse.Success)
|
||||
{
|
||||
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHadStoredAPIKeyOnLoad = false;
|
||||
}
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
|
||||
@@ -411,6 +460,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the model selection when the user picks another Hugging Face inference provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each inference provider offers embedding models of its own, so the models loaded for the
|
||||
/// previous one say nothing about the new one.
|
||||
/// </remarks>
|
||||
/// <param name="selectedInferenceProvider">The inference provider the user chose.</param>
|
||||
private void OnHFInferenceProviderChanged(HFInferenceProvider selectedInferenceProvider)
|
||||
{
|
||||
this.HFInferenceProviderId = selectedInferenceProvider;
|
||||
this.DataModel = default;
|
||||
this.availableModels.Clear();
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
}
|
||||
|
||||
private async Task ReloadModels()
|
||||
{
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Start" Spacing="3">
|
||||
<MudIcon Icon="@this.Icon" Color="@this.IconColor" Size="Size.Large"/>
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@this.Message
|
||||
</MudJustifiedText>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,35 @@
|
||||
using AIStudio.Components;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// A dialog that informs the user about something without asking for a decision. Use it when a
|
||||
/// message must not be missed, e.g., when an action was refused.
|
||||
/// </summary>
|
||||
public partial class InformationDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The message shown to the user.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The icon shown next to the message.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.Info;
|
||||
|
||||
/// <summary>
|
||||
/// The color of the icon.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Color IconColor { get; set; } = Color.Info;
|
||||
|
||||
private void Close() => this.MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@this.IntroductionText @T("Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudPaper Class="pa-3 mb-3 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.h6">
|
||||
@this.Preview.Plugin.Name
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-2">
|
||||
@this.Preview.Plugin.Description
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Type"): <strong>@this.TypeLabel</strong>
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Version"): <strong>@this.Preview.Plugin.Version</strong>
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Authors"): <strong>@this.AuthorsLabel</strong>
|
||||
</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.SourceURL))
|
||||
{
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Source"): <strong>@this.Preview.Plugin.SourceURL</strong>
|
||||
</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.SupportContact))
|
||||
{
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Support contact"): <strong>@this.Preview.Plugin.SupportContact</strong>
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@if (this.Preview.ConfigurationSummary is { HasAnyContent: true } configurationSummary)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-3">
|
||||
@T("A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:")
|
||||
</MudAlert>
|
||||
|
||||
@if (configurationSummary.Destinations.Count > 0)
|
||||
{
|
||||
<MudSimpleTable Dense="@true" Striped="@true" Class="mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>@T("Sends data to")</th>
|
||||
<th>@T("Name")</th>
|
||||
<th>@T("Destination")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var destination in configurationSummary.Destinations)
|
||||
{
|
||||
<tr>
|
||||
<td>@this.DestinationTypeLabel(destination.Type)</td>
|
||||
<td>@destination.Name</td>
|
||||
<td><strong>@destination.Endpoint</strong></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
}
|
||||
|
||||
@if (this.ConfigurationContents.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mb-1">
|
||||
@T("It also brings:")
|
||||
</MudText>
|
||||
<MudList T="string" Class="mb-3">
|
||||
@foreach (var content in this.ConfigurationContents)
|
||||
{
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.AddCircleOutline">
|
||||
@content
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.DeprecationMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-3">
|
||||
@string.Format(T("The authors marked this plugin as deprecated: {0}"), this.Preview.Plugin.DeprecationMessage)
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (this.Preview.ExistingPlugin is { } existingPlugin)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-3">
|
||||
@string.Format(T("This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}."), existingPlugin.Name, existingPlugin.Version, this.Preview.Plugin.Version)
|
||||
</MudAlert>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Warning">
|
||||
@(this.Preview.ReplacesExisting ? T("Replace plugin") : T("Install plugin"))
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,89 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Asks the user whether a plugin archive may be installed. It shows the metadata the archive
|
||||
/// declares about itself, so the user can judge the plugin before its code runs.
|
||||
/// </summary>
|
||||
public partial class PluginImportDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The metadata of the plugin archive about to be installed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public PluginImportPreview Preview { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Names the kind of plugin the user is about to install. Each plugin type gets its own
|
||||
/// sentence instead of a placeholder because articles and word order differ between languages.
|
||||
/// </summary>
|
||||
private string IntroductionText => this.Preview.Plugin.Type switch
|
||||
{
|
||||
PluginType.LANGUAGE => this.T("You are about to install a language plugin from a file."),
|
||||
PluginType.ASSISTANT => this.T("You are about to install an assistant plugin from a file."),
|
||||
PluginType.CONFIGURATION => this.T("You are about to install a configuration plugin from a file."),
|
||||
PluginType.THEME => this.T("You are about to install a theme plugin from a file."),
|
||||
|
||||
_ => this.T("You are about to install a plugin from a file."),
|
||||
};
|
||||
|
||||
private string TypeLabel => this.Preview.Plugin.Type.GetName();
|
||||
|
||||
private string AuthorsLabel => this.Preview.Plugin.Authors.Length > 0
|
||||
? string.Join(", ", this.Preview.Plugin.Authors)
|
||||
: this.T("Unknown");
|
||||
|
||||
/// <summary>
|
||||
/// Names the kind of a destination a configuration plugin brings.
|
||||
/// </summary>
|
||||
private string DestinationTypeLabel(PluginConfigurationObjectType objectType) => objectType switch
|
||||
{
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => this.T("LLM provider"),
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => this.T("Embedding provider"),
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => this.T("Transcription provider"),
|
||||
PluginConfigurationObjectType.DATA_SOURCE => this.T("Data source"),
|
||||
|
||||
_ => this.T("Unknown"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Everything a configuration plugin brings besides its providers and data sources. Only what is
|
||||
/// actually there gets listed, so the dialog stays short for a small configuration.
|
||||
/// </summary>
|
||||
private List<string> ConfigurationContents
|
||||
{
|
||||
get
|
||||
{
|
||||
var contents = new List<string>();
|
||||
if (this.Preview.ConfigurationSummary is not { } summary)
|
||||
return contents;
|
||||
|
||||
Add(summary.DeclaredSettings, this.T("{0} setting it takes control of"), this.T("{0} settings it takes control of"));
|
||||
Add(summary.ChatTemplates, this.T("{0} chat template"), this.T("{0} chat templates"));
|
||||
Add(summary.Profiles, this.T("{0} profile"), this.T("{0} profiles"));
|
||||
Add(summary.DocumentAnalysisPolicies, this.T("{0} document analysis policy"), this.T("{0} document analysis policies"));
|
||||
Add(summary.MandatoryInfos, this.T("{0} mandatory information you have to accept before using AI Studio"), this.T("{0} mandatory information you have to accept before using AI Studio"));
|
||||
Add(summary.Introductions, this.T("{0} introduction on the welcome page"), this.T("{0} introductions on the welcome page"));
|
||||
|
||||
return contents;
|
||||
|
||||
void Add(int count, string singular, string plural)
|
||||
{
|
||||
if (count > 0)
|
||||
contents.Add(string.Format(count == 1 ? singular : plural, count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
@using AIStudio.Tools.Security
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudPaper Class="pa-6 mb-4" Elevation="0" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
|
||||
<MudAvatar Size="Size.Large" Color="Color.Warning" Variant="Variant.Filled">
|
||||
<MudIcon Icon="@Icons.Material.Filled.GppMaybe"/>
|
||||
</MudAvatar>
|
||||
|
||||
<MudStack Spacing="0">
|
||||
<MudJustifiedText Typo="Typo.h5">
|
||||
@T("Suspicious content was removed")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@T("AI Studio found instructions aimed at the AI inside your content and removed them. Everything around them was kept, so you can continue working with the content. Please review what was removed below.")
|
||||
</MudJustifiedText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mt-4">
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@T("Typical attacks on AI systems (e.g. prompt injection) hide instructions within untrusted content to trick an AI model into ignoring its intended rules or performing unintended actions.")
|
||||
</MudJustifiedText>
|
||||
</MudAlert>
|
||||
</MudPaper>
|
||||
|
||||
<MudDivider />
|
||||
<MudStack Row="true" Justify="Justify.Center" Class="my-2">
|
||||
<MudButton Variant="Variant.Text"
|
||||
EndIcon="@(this.showPromptInjectionInformation ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
OnClick="@this.TogglePromptInjectionInformation">
|
||||
|
||||
@(this.showPromptInjectionInformation ? T("Hide more information") : T("More information"))
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudCollapse Expanded="@this.showPromptInjectionInformation">
|
||||
<MudPaper Outlined="true"
|
||||
Class="pa-4 mb-4">
|
||||
@foreach (var result in this.Alert.Results)
|
||||
{
|
||||
<MudGrid Class="mb-2">
|
||||
<MudItem xs="12" md="3">
|
||||
<MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Source" />
|
||||
<MudJustifiedText Typo="Typo.subtitle2">
|
||||
@T("Source type")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@result.Source.Kind.GetDisplayName()
|
||||
</MudJustifiedText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="4">
|
||||
<MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Description"/>
|
||||
<MudJustifiedText Typo="Typo.subtitle2">
|
||||
@T("Content source")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Style="word-break:break-all;">
|
||||
@result.Source.Label
|
||||
</MudJustifiedText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="5">
|
||||
<MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Outlined.WarningAmber"/>
|
||||
<MudJustifiedText Typo="Typo.subtitle2">
|
||||
@T("Removed content")
|
||||
</MudJustifiedText>
|
||||
|
||||
@foreach (var finding in result.Findings)
|
||||
{
|
||||
<MudStack Spacing="0">
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
<b>
|
||||
@finding.Category.GetDisplayName()
|
||||
</b>
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Class="ml-4 mt-1">
|
||||
@finding.Snippet
|
||||
</MudJustifiedText>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
@* The runtime caps how many passages it describes, while it removes every one of them. *@
|
||||
@if (result.RedactedCount > result.Findings.Count)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mt-1">
|
||||
@string.Format(T("And {0} more passages of the same kind."), result.RedactedCount - result.Findings.Count)
|
||||
</MudJustifiedText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-4 mt-2" Outlined="true">
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("Prompt injection is a method used to manipulate AI systems such as chatbots. An attacker places misleading instructions in content so that the AI treats them as legitimate. This can cause the AI to ignore safeguards, expose private information, or generate harmful content.")
|
||||
</MudJustifiedText>
|
||||
<MudLink Href="@PromptInjectionGuardService.WIKI_URL" Target="_blank">
|
||||
@PromptInjectionGuardService.WIKI_URL
|
||||
</MudLink>
|
||||
</MudPaper>
|
||||
</MudCollapse>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@if (CanDisableFutureAlerts)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="@this.CloseAndDisableFutureAlertsAsync">
|
||||
@T("Close and don't show again")
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@this.Close">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,39 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Security;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class PromptInjectionAlertDialog : MSGComponentBase
|
||||
{
|
||||
private bool showPromptInjectionInformation;
|
||||
|
||||
private static bool CanDisableFutureAlerts => !ManagedConfiguration.TryGet(x => x.App, x => x.ShowPromptInjectionAlert, out var meta) || !meta.IsLocked;
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// What was filtered during the user action that triggered this dialog.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Carries every affected source, because one action may involve many documents and the
|
||||
/// user should acknowledge them together rather than one dialog at a time.
|
||||
/// </remarks>
|
||||
[Parameter, EditorRequired]
|
||||
public PromptInjectionAlertMessage Alert { get; set; } = null!;
|
||||
|
||||
private void Close() => this.MudDialog.Close();
|
||||
|
||||
private async Task CloseAndDisableFutureAlertsAsync()
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.App.ShowPromptInjectionAlert = false;
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
this.MudDialog.Close();
|
||||
}
|
||||
|
||||
private void TogglePromptInjectionInformation() => this.showPromptInjectionInformation = !this.showPromptInjectionInformation;
|
||||
}
|
||||
@@ -5,6 +5,12 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@if (this.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-4">
|
||||
@T("This provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
|
||||
</MudAlert>
|
||||
}
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
@@ -13,14 +19,12 @@
|
||||
ValueChanged="@this.OnProviderChanged"
|
||||
Label="@T("Provider")"
|
||||
Class="mb-3"
|
||||
OpenIcon="@Icons.Material.Filled.AccountBalance"
|
||||
AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingProvider">
|
||||
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
|
||||
{
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
<ProviderLabel ProviderType="@provider" CustomIconDataUrl="@(provider == this.DataLLMProvider ? this.DataCustomIconDataUrl : string.Empty)" Text="@provider.ToName()" />
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@@ -28,7 +32,7 @@
|
||||
@T("Create account")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
|
||||
@if (this.DataLLMProvider.IsAPIKeyNeeded(this.DataHost))
|
||||
{
|
||||
<SecretInputField Secret="@this.dataAPIKey" SecretChanged="@this.OnAPIKeyChanged" Label="@this.APIKeyText" Validation="@this.providerValidation.ValidatingAPIKey"/>
|
||||
@@ -44,13 +48,14 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
@if (host.IsChatSupported())
|
||||
@@ -65,19 +70,20 @@
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect @bind-Value="@this.HFInferenceProviderId" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProvider">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
@if (inferenceProvider.SupportsChat())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
@* ReSharper disable Asp.Entity *@
|
||||
<MudJustifiedText Class="mb-3">
|
||||
Please double-check if your model name matches the curl specifications provided by the inference provider. If it doesn't, you might get a <b>Not Found</b> error when trying to use the model. Here's a <MudLink Href="https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct?inference_api=true&inference_provider=novita&language=sh" Target="_blank">curl example</MudLink>.
|
||||
@T("Choose which inference provider should answer your requests. When you pick one of the automatic options instead, Hugging Face selects a provider for you and switches to another one when your choice is unavailable.")
|
||||
</MudJustifiedText>
|
||||
@* ReSharper restore Asp.Entity *@
|
||||
}
|
||||
|
||||
@if (!this.IsLLMModelSelectionHidden)
|
||||
@@ -86,7 +92,7 @@
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" StretchItems="StretchItems.End">
|
||||
@if (this.DataLLMProvider.IsLLMModelProvidedManually())
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.DataLLMProvider.GetModelsOverviewURL(this.HFInferenceProviderId)" Target="_blank">
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.ModelsOverviewURL" Target="_blank">
|
||||
@T("Show available models")
|
||||
</MudButton>
|
||||
<MudTextField
|
||||
@@ -96,6 +102,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateManuallyModel"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually.")"
|
||||
@@ -103,7 +110,13 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@if (!string.IsNullOrWhiteSpace(this.ModelsOverviewURL))
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.ModelsOverviewURL" Target="_blank">
|
||||
@T("Show available models")
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load models")
|
||||
</MudButton>
|
||||
@if(this.availableModels.Count is 0)
|
||||
@@ -118,7 +131,7 @@
|
||||
Value="@this.DataModel"
|
||||
ValueChanged="@(async model => await this.OnModelChanged(model))"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingModel">
|
||||
Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingModel">
|
||||
@foreach (var model in this.availableModels)
|
||||
{
|
||||
<MudSelectItem Value="@model">
|
||||
@@ -158,6 +171,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
@@ -221,12 +235,13 @@
|
||||
<MudSwitch T="bool"
|
||||
Value="@this.IsCapabilityEnabled(capability)"
|
||||
ValueChanged="@(value => this.OnCapabilitySwitchChanged(capability, value))"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Color="Color.Primary" />
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.RestartAlt"
|
||||
Disabled="@(!this.HasCapabilityOverride(capability))"
|
||||
Disabled="@(this.IsEnterpriseConfiguration || !this.HasCapabilityOverride(capability))"
|
||||
OnClick="@(() => this.ResetCapabilityOverride(capability))">
|
||||
@T("Reset")
|
||||
</MudButton>
|
||||
@@ -244,7 +259,8 @@
|
||||
Margin="Margin.Dense"
|
||||
OpenIcon="@Icons.Material.Filled.Psychology"
|
||||
AdornmentColor="Color.Info"
|
||||
Adornment="Adornment.Start">
|
||||
Adornment="Adornment.Start"
|
||||
Disabled="@this.IsEnterpriseConfiguration">
|
||||
@foreach (var mode in REASONING_OVERRIDE_MODES)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@@ -257,7 +273,7 @@
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-4">
|
||||
@string.Format(T("The current model uses the {0}."), this.GetCurrentModelApiLabel())
|
||||
</MudJustifiedText>
|
||||
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
|
||||
<MudTextField T="string" Label=@T("Additional API parameters") Variant="Variant.Outlined" Lines="4" AutoGrow="true" MaxLines="10" HelperText=@T("""Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""") Placeholder="@GetPlaceholderExpertSettings" @bind-Value="@this.AdditionalJsonApiParameters" Immediate="true" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateAdditionalJsonApiParameters" OnBlur="@this.OnInputChangeExpertSettings"/>
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
|
||||
@@ -80,6 +80,12 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataCustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The LLM model to use, e.g., GPT-4o.
|
||||
@@ -92,6 +98,13 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEditing { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this provider is managed by an enterprise configuration plugin. When true, every
|
||||
/// field except the API key is locked, matching Settings.Provider.IsEnterpriseConfiguration.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEnterpriseConfiguration { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string AdditionalJsonApiParameters { get; set; } = string.Empty;
|
||||
@@ -112,6 +125,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
private static readonly IReadOnlyList<Capability> SWITCH_CAPABILITY_OVERRIDES =
|
||||
[
|
||||
Capability.AUDIO_INPUT,
|
||||
Capability.FUNCTION_CALLING,
|
||||
Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.SPEECH_INPUT,
|
||||
Capability.VIDEO_INPUT
|
||||
@@ -134,6 +148,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataAPIKey = string.Empty;
|
||||
private bool dataHadStoredAPIKeyOnLoad;
|
||||
private string dataManuallyModel = string.Empty;
|
||||
private string dataAPIKeyStorageIssue = string.Empty;
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
@@ -182,13 +197,14 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
UsedLLMProvider = this.DataLLMProvider,
|
||||
Model = this.GetSelectedModel(),
|
||||
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
|
||||
IsEnterpriseConfiguration = false,
|
||||
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
Host = this.DataHost,
|
||||
HFInferenceProvider = this.HFInferenceProviderId,
|
||||
AdditionalJsonApiParameters = this.AdditionalJsonApiParameters,
|
||||
TokenizerPath = this.dataFilePath,
|
||||
CapabilityOverrides = this.capabilityOverrides.HasOverrides ? this.capabilityOverrides : null,
|
||||
CustomIconDataUrl = this.DataCustomIconDataUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,9 +230,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
// Load the used instance names:
|
||||
#pragma warning disable MWAIS0001
|
||||
this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
|
||||
#pragma warning restore MWAIS0001
|
||||
this.UsedInstanceNames = this.SettingsManager.GetAllProviders().Select(x => x.InstanceName.ToLowerInvariant()).ToList();
|
||||
|
||||
this.capabilityOverrides = this.DataCapabilityOverrides ?? new();
|
||||
this.showExpertSettings = !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters) || this.capabilityOverrides.HasOverrides;
|
||||
@@ -227,7 +241,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
|
||||
this.dataFilePath = this.DataTokenizerPath;
|
||||
|
||||
// When using Fireworks or Hugging Face, we must copy the model name:
|
||||
// When using Fireworks, we must copy the model name:
|
||||
if (this.DataLLMProvider.IsLLMModelProvidedManually())
|
||||
this.dataManuallyModel = this.DataModel.Id;
|
||||
|
||||
@@ -244,11 +258,17 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
// Load the API key:
|
||||
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.LLM_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
|
||||
if (requestedSecret.Success)
|
||||
{
|
||||
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.dataAPIKey = string.Empty;
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
|
||||
|
||||
// For an enterprise-managed provider, having no key yet is the expected first-run
|
||||
// state, not a storage failure -- the user is just about to set their own key:
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
|
||||
{
|
||||
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
|
||||
await this.form.Validate();
|
||||
@@ -273,8 +293,12 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||
|
||||
// Must mirror Settings.Provider.SecretId exactly: when editing an enterprise-managed
|
||||
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
|
||||
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
|
||||
// silently end up in the wrong keyring row and never be found again.
|
||||
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
|
||||
|
||||
public string SecretName => this.DataInstanceName;
|
||||
|
||||
#endregion
|
||||
@@ -322,6 +346,22 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHadStoredAPIKeyOnLoad = true;
|
||||
}
|
||||
else if (this.dataHadStoredAPIKeyOnLoad)
|
||||
{
|
||||
// The user cleared a previously stored key. Without this, the old key would simply
|
||||
// stay in the OS keyring untouched and keep being used:
|
||||
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||
if (!deleteResponse.Success)
|
||||
{
|
||||
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHadStoredAPIKeyOnLoad = false;
|
||||
}
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
|
||||
@@ -442,6 +482,24 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
this.usesLegacySystemModelFallback = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the model selection when the user picks another Hugging Face inference provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Which models are on offer depends on the inference provider, so the models loaded for the
|
||||
/// previous one say nothing about the new one. Keeping them would let the user pick a model
|
||||
/// their provider does not serve, which the router answers with an error.
|
||||
/// </remarks>
|
||||
/// <param name="selectedInferenceProvider">The inference provider the user chose.</param>
|
||||
private void OnHFInferenceProviderChanged(HFInferenceProvider selectedInferenceProvider)
|
||||
{
|
||||
this.HFInferenceProviderId = selectedInferenceProvider;
|
||||
this.DataModel = default;
|
||||
this.capabilityOverrides = new();
|
||||
this.availableModels.Clear();
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
}
|
||||
|
||||
private void OnHostChanged(Host selectedHost)
|
||||
{
|
||||
// When the host changes, reset the model selection state:
|
||||
@@ -500,6 +558,11 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
this.DataHost is Host.LLAMA_CPP &&
|
||||
this.usesLegacySystemModelFallback;
|
||||
|
||||
/// <summary>
|
||||
/// The catalog of the provider, where the user can read up on the models before choosing one.
|
||||
/// </summary>
|
||||
private string ModelsOverviewURL => this.DataLLMProvider.GetModelsOverviewURL(this.HFInferenceProviderId);
|
||||
|
||||
private void UpdateModelSelectionAfterLoading()
|
||||
{
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED || this.DataHost is not Host.LLAMA_CPP)
|
||||
@@ -668,6 +731,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
private string GetCapabilityOverrideLabel(Capability capability) => capability switch
|
||||
{
|
||||
Capability.AUDIO_INPUT => T("Audio input"),
|
||||
Capability.FUNCTION_CALLING => T("Tool calling"),
|
||||
Capability.MULTIPLE_IMAGE_INPUT => T("Multiple image input"),
|
||||
Capability.SPEECH_INPUT => T("Speech input"),
|
||||
Capability.VIDEO_INPUT => T("Video input"),
|
||||
@@ -701,7 +765,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.");
|
||||
return T("""Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,7 +898,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
if (objectStack.Count != 0)
|
||||
{
|
||||
errorMessage = T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.");
|
||||
errorMessage = T("""Invalid JSON: Add the parameters in proper JSON formatting, e.g., "temperature": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though.""");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.AGENDA_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</MudField>
|
||||
<ToolDefaultsConfiguration Component="Components.BIAS_DAY_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -40,18 +38,17 @@ public abstract class SettingsDialogBase : MSGComponentBase
|
||||
|
||||
protected void Close() => this.MudDialog.Cancel();
|
||||
|
||||
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||
private void UpdateProviders()
|
||||
{
|
||||
this.AvailableLLMProviders.Clear();
|
||||
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
|
||||
foreach (var provider in this.SettingsManager.GetAllProviders())
|
||||
this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id));
|
||||
}
|
||||
|
||||
private void UpdateEmbeddingProviders()
|
||||
{
|
||||
this.AvailableEmbeddingProviders.Clear();
|
||||
foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders)
|
||||
foreach (var provider in this.SettingsManager.GetAllEmbeddingProviders())
|
||||
this.AvailableEmbeddingProviders.Add(new (provider.Name, provider.Id));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
@using AIStudio.Assistants.BatchProcessing
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
@using AIStudio.Tools.Rust
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.BatchPrediction" Class="mr-2"/>
|
||||
@T("Assistant: Batch Processing defaults")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="@T("Preselect batch processing options?")" LabelOn="@T("Batch processing options are preselected")" LabelOff="@T("No batch processing options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectOptions)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectOptions = value)" OptionHelp="@T("When enabled, new batch runs start with the defaults configured below.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3">@T("Input")</MudText>
|
||||
<ConfigurationDirectory OptionDescription="@T("Default input folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default input folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory = value)" OptionHelp="@T("Leave empty when an input folder should be selected for every batch run.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.InputDirectory, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationText OptionDescription="@T("Default file patterns")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAlt" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns = value)" ResetValue="@(() => DataBatchProcessing.DEFAULT_FILE_PATTERNS)" ResetButtonText="@T("Restore default patterns")" OptionHelp="@T("Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FilePatterns, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationOption OptionDescription="@T("Include subfolders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Subfolders are included")" LabelOff="@T("Only the selected folder is processed")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeSubdirectories, out var meta) && meta.IsLocked"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("Instructions")</MudText>
|
||||
<ConfigurationSelect OptionDescription="@T("Default source of the instructions")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource)" Data="@this.PromptSourceData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptSource, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FREE_PROMPT)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load default prompt from file")" FileContent="@this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt" FileContentChanged="@this.UpdateFreePromptFromFileAsync" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true" Disabled="@this.FreePromptImportDisabled()"/>
|
||||
<ConfigurationText OptionDescription="@T("Default prompt")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.EditNote" NumLines="5" MaxLines="26" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt = value)" OptionHelp="@T("These instructions are applied to every document of a new batch run.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FreePrompt, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
else if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FILE_IMPORT)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load default Markdown instructions file")" Filter="@([FileTypes.MARKDOWN])" FilePathLoaded="@this.UpdatePromptFilePathAsync" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true" Disabled="@this.PromptFileImportDisabled()"/>
|
||||
<ConfigurationFile OptionDescription="@T("Default Markdown instructions file")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Description" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath = value)" FileDialogTitle="@T("Select the default Markdown instructions file")" Filter="@([FileTypes.MARKDOWN])" OptionHelp="@T("The current content of this Markdown file is loaded whenever the defaults are applied.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptFilePath, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Default document analysis policy")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedPolicyId)" Data="@this.PolicyData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedPolicyId = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PreselectedPolicyId, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SelectedPolicyMissing)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Class="mb-6">@T("The configured default policy no longer exists. Select another policy before starting a policy-based batch run.")</MudAlert>
|
||||
}
|
||||
}
|
||||
|
||||
@* The tools belong to the instructions, which is why they sit here rather than at the end of the dialog. *@
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.POLICY)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("A policy brings its own tools, so there is nothing to preselect here. You configure them with the policy in the Document Analysis Assistant.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ToolDefaultsConfiguration Component="Components.BATCH_PROCESSING_ASSISTANT" IncludeVisibilityToggle="@false" />
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("Output")</MudText>
|
||||
<ConfigurationSelect OptionDescription="@T("Default output mode")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode)" Data="@this.OutputModeData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.OutputMode, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES)
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Default file format")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ResultFileFormat)" Data="@ResultFileFormatData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ResultFileFormat = value)" OptionHelp="@T("Choose the format of new result files. Everything except Markdown is converted by Pandoc.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ResultFileFormat, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ConfigurationText OptionDescription="@T("Default results table name")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Description" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.CsvFileName)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.CsvFileName = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.CsvFileName, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationText OptionDescription="@T("Default result column header")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.TableChart" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ResultColumnHeader)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ResultColumnHeader = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ResultColumnHeader, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Default column separator")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.CsvSeparator)" Data="@this.CsvSeparatorData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.CsvSeparator = value)" OptionHelp="@T("Choose which character separates the columns of new results tables.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.CsvSeparator, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.CsvSeparator is BatchProcessingCsvSeparator.CUSTOM)
|
||||
{
|
||||
<ConfigurationText OptionDescription="@T("Default custom column separator")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Edit" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.CustomCsvSeparator)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.CustomCsvSeparator = value)" Validation="@this.ValidateCustomCsvSeparator" OptionHelp="@T("Enter one punctuation or symbol character.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.CustomCsvSeparator, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
}
|
||||
<ConfigurationDirectory OptionDescription="@T("Default output folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default output folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.OutputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.OutputDirectory = value)" OptionHelp="@T("Leave empty to use the ai-results subfolder of the input folder.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.OutputDirectory, out var meta) && meta.IsLocked"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("Processing pace")</MudText>
|
||||
@if (this.MinimumDelayIsManaged)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-6">@(string.Format(T("Your organization requires a pause of at least {0} seconds between files. Users can configure only the upper limit."), this.ManagedMinimumDelaySeconds))</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Default minimum pause between files")" Min="@DataBatchProcessing.MIN_DELAY_SECONDS" Max="@DataBatchProcessing.MAX_DELAY_SECONDS" Step="1" Unit="@T("seconds")" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds = value)" OptionHelp="@T("The lower end of the random pause interval. AI Studio never allows less than 6 seconds.")"/>
|
||||
}
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Default maximum pause between files")" Min="@this.EffectiveMinimumDelaySeconds" Max="@DataBatchProcessing.MAX_DELAY_SECONDS" Step="1" Unit="@T("seconds")" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MaximumDelaySeconds)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MaximumDelaySeconds = value)" OptionHelp="@T("The upper end of the random pause interval. The app-wide maximum is 300 seconds (5 minutes).")"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("AI selection")</MudText>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@this.DefaultsDisabled" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationProviderSelection Component="Components.BATCH_PROCESSING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,109 @@
|
||||
using AIStudio.Assistants.BatchProcessing;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class SettingsDialogBatchProcessing : SettingsDialogBase
|
||||
{
|
||||
private bool DefaultsDisabled() => !this.SettingsManager.ConfigurationData.BatchProcessing.PreselectOptions;
|
||||
|
||||
private bool MinimumDelayIsManaged => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MinimumDelaySeconds, out var meta)
|
||||
&& meta.ManagedMode is not null;
|
||||
|
||||
private int ManagedMinimumDelaySeconds => Math.Clamp(
|
||||
this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds,
|
||||
DataBatchProcessing.MIN_DELAY_SECONDS,
|
||||
DataBatchProcessing.MAX_DELAY_SECONDS);
|
||||
|
||||
private int EffectiveMinimumDelaySeconds => this.MinimumDelayIsManaged
|
||||
? this.ManagedMinimumDelaySeconds
|
||||
: Math.Clamp(
|
||||
this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds,
|
||||
DataBatchProcessing.MIN_DELAY_SECONDS,
|
||||
DataBatchProcessing.MAX_DELAY_SECONDS);
|
||||
|
||||
private bool FreePromptImportDisabled() => this.DefaultsDisabled()
|
||||
|| ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FreePrompt, out var meta) && meta.IsLocked;
|
||||
|
||||
private bool PromptFileImportDisabled() => this.DefaultsDisabled()
|
||||
|| ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptFilePath, out var meta) && meta.IsLocked;
|
||||
|
||||
private async Task UpdateFreePromptFromFileAsync(string content)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt = content;
|
||||
await this.StoreImportedDefaultAsync();
|
||||
}
|
||||
|
||||
private async Task UpdatePromptFilePathAsync(string path)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath = path;
|
||||
await this.StoreImportedDefaultAsync();
|
||||
}
|
||||
|
||||
private async Task StoreImportedDefaultAsync()
|
||||
{
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ConfigurationSelectData<BatchProcessingPromptSource>> PromptSourceData =>
|
||||
[
|
||||
.. Enum
|
||||
.GetValues<BatchProcessingPromptSource>()
|
||||
.Select(value => new ConfigurationSelectData<BatchProcessingPromptSource>(value.Name(), value))
|
||||
];
|
||||
|
||||
private IReadOnlyList<ConfigurationSelectData<BatchProcessingOutputMode>> OutputModeData =>
|
||||
[
|
||||
.. Enum
|
||||
.GetValues<BatchProcessingOutputMode>()
|
||||
.Select(value => new ConfigurationSelectData<BatchProcessingOutputMode>(value.Name(), value))
|
||||
];
|
||||
|
||||
private static IReadOnlyList<ConfigurationSelectData<FileExportFormat>> ResultFileFormatData =>
|
||||
[
|
||||
.. FileExportFormatExtensions.ANSWER_FORMATS
|
||||
.Select(value => new ConfigurationSelectData<FileExportFormat>(value.ToName(), value))
|
||||
];
|
||||
|
||||
private IReadOnlyList<ConfigurationSelectData<BatchProcessingCsvSeparator>> CsvSeparatorData =>
|
||||
[
|
||||
.. Enum
|
||||
.GetValues<BatchProcessingCsvSeparator>()
|
||||
.Select(value => new ConfigurationSelectData<BatchProcessingCsvSeparator>(value.Name(), value))
|
||||
];
|
||||
|
||||
private string? ValidateCustomCsvSeparator(string separator)
|
||||
{
|
||||
if (!BatchProcessingCsvSeparatorExtensions.IsValidCustomSeparator(separator))
|
||||
return T("Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ConfigurationSelectData<string>> PolicyData
|
||||
{
|
||||
get
|
||||
{
|
||||
var selectedPolicyId = this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedPolicyId;
|
||||
var policies = this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies
|
||||
.Select(policy => new ConfigurationSelectData<string>(policy.PolicyName, policy.Id))
|
||||
.ToList();
|
||||
|
||||
if (this.SelectedPolicyMissing)
|
||||
policies.Add(new(string.Format(T("Missing policy ({0})"), selectedPolicyId), selectedPolicyId));
|
||||
|
||||
return policies;
|
||||
}
|
||||
}
|
||||
|
||||
private bool SelectedPolicyMissing
|
||||
{
|
||||
get
|
||||
{
|
||||
var selectedPolicyId = this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedPolicyId;
|
||||
return !string.IsNullOrWhiteSpace(selectedPolicyId) && this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.All(policy => policy.Id != selectedPolicyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
<ConfigurationOption OptionDescription="@T("Show the latest message after loading?")" LabelOn="@T("Latest message is shown, after loading a chat")" LabelOff="@T("First (oldest) message is shown, after loading a chat")" State="@(() => this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading = updatedState)" OptionHelp="@T("When enabled, the latest message is shown after loading a chat. When disabled, the first (oldest) message is shown.")"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Provider selection when creating new chats")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)" Data="@ConfigurationSelectDataFactory.GetAddChatProviderBehavior()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior = selectedValue)" OptionHelp="@T("Control how the LLM provider for added chats is selected.")"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Provider selection when loading a chat and sending assistant results to chat")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)" Data="@ConfigurationSelectDataFactory.GetLoadingChatProviderBehavior()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior = selectedValue)" OptionHelp="@T("Control how the LLM provider for loaded chats is selected and when assistant results are sent to chat.")"/>
|
||||
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||
@@ -22,6 +21,8 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedChatTemplate, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
|
||||
<ToolDefaultsConfiguration Component="Components.CHAT" IncludeVisibilityToggle="@false" />
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<DataSourceSelection SelectionMode="DataSourceSelectionMode.CONFIGURATION_MODE" AutoSaveAppSettings="@true" @bind-DataSourceOptions="@this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions" ConfigurationHeaderMessage="@T("You can set default data sources and options for new chats. You can change these settings later for each individual chat.")"/>
|
||||
|
||||
@@ -48,27 +48,22 @@
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
@if (context.FileAttachments.Count == 0)
|
||||
{
|
||||
@if (context.FileAttachments.Count == 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||
@T("Use shared attachment paths")
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" Disabled="@this.isPluginDirectoryDialogOpen" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||
@T("Copy attachments into plugin")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
<AdminExportButton OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))" />
|
||||
}
|
||||
else if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||
@T("Use shared attachment paths")
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" Disabled="@this.isPluginDirectoryDialogOpen" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||
@T("Copy attachments into plugin")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Coding.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.CODING_ASSISTANT" IncludeVisibilityToggle="@false" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
|
||||
|
||||
@@ -81,11 +81,9 @@
|
||||
<MudTooltip Text="@T("Refresh")">
|
||||
<MudIconButton Color="Color.Primary" Icon="@Icons.Material.Filled.Sync" Disabled="@(!this.CanRefreshDataSource(context))" OnClick="() => this.RefreshDataSource(context)"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1)
|
||||
@if (context is DataSourceERI_V1)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/>
|
||||
</MudTooltip>
|
||||
<AdminExportButton OnClick="@(() => this.ExportDataSource(context))" />
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)"/>
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.GRAMMAR_SPELLING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.GRAMMAR_SPELLING_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -19,10 +19,11 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId = selectedValue)" OptionHelp="@T("Select the language plugin used for comparision.")"/>
|
||||
<ConfigurationProviderSelection Component="Components.I18N_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.I18N.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.I18N.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.I18N.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.I18N_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -15,10 +15,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.ICON_FINDER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.ICON_FINDER_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -26,10 +26,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.JOB_POSTING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.JOB_POSTING_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -17,6 +17,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.LEGAL_CHECK_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.LEGAL_CHECK_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.MY_TASKS_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("View")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="() => this.ViewProfile(context)"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="@(() => this.ViewProfile(context))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -45,16 +45,11 @@
|
||||
{
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditProfile(context))"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportProfile(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<AdminExportButton OnClick="@(() => this.ExportProfile(context))" />
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteProfile(context))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.REWRITE_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.REWRITE_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -25,6 +25,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.SLIDE_BUILDER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.SLIDE_BUILDER_ASSISTANT" IncludeVisibilityToggle="@false" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.SYNONYMS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.SYNONYMS_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -29,10 +29,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TEXT_SUMMARIZER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.TEXT_SUMMARIZER_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -23,10 +23,11 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.TRANSLATION_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.TRANSLATION_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
@T("Close")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -23,6 +23,7 @@
|
||||
<ConfigurationProviderSelection Component="Components.EMAIL_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.EMail.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
|
||||
</MudPaper>
|
||||
<ToolDefaultsConfiguration Component="Components.EMAIL_ASSISTANT" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@this.implementation?.Icon" Class="mr-2" />
|
||||
@(this.implementation?.GetDisplayName() ?? T("Tool Settings"))
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (this.toolDefinition is null)
|
||||
{
|
||||
<MudText Typo="Typo.body1">@T("The selected tool could not be loaded.")</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-4">
|
||||
@this.implementation?.GetDescription()
|
||||
</MudJustifiedText>
|
||||
|
||||
@if (!this.SettingsManager.IsToolActive(this.toolDefinition.Id))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">@T("This tool has been disabled by your organization.")</MudAlert>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.validationMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4">@this.validationMessage</MudAlert>
|
||||
}
|
||||
|
||||
@foreach (var warning in this.GetSettingsWarnings())
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">@warning</MudAlert>
|
||||
}
|
||||
|
||||
@foreach (var group in this.BuildVisibleFieldGroups())
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
@if (this.ShowsGroupHeader(group))
|
||||
{
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap" Class="mb-3">
|
||||
<MudText Typo="Typo.subtitle2">@this.GetGroupLabel(group.Key)</MudText>
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
|
||||
@foreach (var link in this.GetGroupLinks(group.Key))
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@link.Icon" Href="@link.Url" Target="_blank">
|
||||
@link.Label
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
}
|
||||
@foreach (var property in group.Fields)
|
||||
{
|
||||
var fieldName = property.Key;
|
||||
var field = property.Value;
|
||||
var fieldOptions = field.GetOptions();
|
||||
if (fieldOptions.Count > 0)
|
||||
{
|
||||
<MudSelect T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" Class="mb-3" Disabled="@this.IsFieldDisabled(fieldName)">
|
||||
@if (!this.toolDefinition.SettingsSchema.Required.Contains(fieldName))
|
||||
{
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Not set")</MudSelectItem>
|
||||
}
|
||||
@foreach (var option in fieldOptions)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@option.Value">@option.Label</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" InputType="@(field.Secret ? InputType.Password : InputType.Text)" Disabled="@this.IsFieldDisabled(fieldName)" />
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Text">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Save" Variant="Variant.Filled" Disabled="@(this.toolDefinition is null)">
|
||||
@T("Save")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,183 @@
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class ToolSettingsDialog : SettingsDialogBase
|
||||
{
|
||||
[Parameter]
|
||||
public string ToolId { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ToolSettingsService ToolSettingsService { get; init; } = null!;
|
||||
|
||||
private ToolDefinition? toolDefinition;
|
||||
private IToolImplementation? implementation;
|
||||
private Dictionary<string, string> values = new(StringComparer.Ordinal);
|
||||
private IReadOnlyList<FieldGroup> fieldGroups = [];
|
||||
private string validationMessage = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
this.toolDefinition = this.ToolRegistry.GetDefinition(this.ToolId);
|
||||
if (this.toolDefinition is not null)
|
||||
{
|
||||
this.implementation = this.ToolRegistry.GetImplementation(this.toolDefinition.ImplementationKey);
|
||||
this.values = await this.ToolSettingsService.GetSettingsAsync(this.toolDefinition);
|
||||
this.fieldGroups = BuildFieldGroups(this.toolDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetValue(string fieldName) => this.values.GetValueOrDefault(fieldName, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Splits the tool's settings fields into the groups the tool declared for them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Groups appear in the order in which their first field appears in the schema, and the
|
||||
/// fields keep the order the tool wrote them in. That is the order the fields have always
|
||||
/// been rendered in, so a tool without groups looks exactly as it did before: one group
|
||||
/// with an empty name, holding everything.<br/><br/>
|
||||
/// A schema does not change while the dialog is open, so this runs once rather than on
|
||||
/// every render.
|
||||
/// </remarks>
|
||||
private static IReadOnlyList<FieldGroup> BuildFieldGroups(ToolDefinition definition)
|
||||
{
|
||||
var groups = new List<FieldGroup>();
|
||||
var groupIndexByKey = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var property in definition.SettingsSchema.Properties)
|
||||
{
|
||||
if (!groupIndexByKey.TryGetValue(property.Value.Group, out var groupIndex))
|
||||
{
|
||||
groupIndex = groups.Count;
|
||||
groupIndexByKey[property.Value.Group] = groupIndex;
|
||||
groups.Add(new FieldGroup(property.Value.Group, []));
|
||||
}
|
||||
|
||||
groups[groupIndex].Fields.Add(property);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The groups as they are rendered right now, without the fields the tool is hiding.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Which fields make sense can depend on what is filled in, so this is built on every
|
||||
/// render rather than once: a field the tool starts to offer has to appear as soon as the
|
||||
/// value it depends on changes. A group whose every field is hidden is left out entirely,
|
||||
/// so no empty box is rendered.<br/><br/>
|
||||
/// Cheap enough to be called more than once per render: a tool has a handful of settings,
|
||||
/// and asking the tool about one of them costs a dictionary lookup or two.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<FieldGroup> BuildVisibleFieldGroups()
|
||||
{
|
||||
if (this.implementation is null)
|
||||
return this.fieldGroups;
|
||||
|
||||
var visibleGroups = new List<FieldGroup>();
|
||||
foreach (var group in this.fieldGroups)
|
||||
{
|
||||
var visibleFields = group.Fields.Where(field => this.implementation.IsSettingsFieldVisible(field.Key, this.values)).ToList();
|
||||
if (visibleFields.Count > 0)
|
||||
visibleGroups.Add(new FieldGroup(group.Key, visibleFields));
|
||||
}
|
||||
|
||||
return visibleGroups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether one group shows a heading above its fields.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A tool that declares no groups has a single nameless group holding everything, and a
|
||||
/// heading above the only box would say nothing the dialog's title does not say already.
|
||||
/// As soon as there is a second box, each of them has to state which one it is — the box
|
||||
/// holding the fields that belong to no group in particular included.<br/><br/>
|
||||
/// It counts the boxes that are actually rendered, so a group the tool hides entirely does
|
||||
/// not leave the remaining box with a heading it does not need.
|
||||
/// </remarks>
|
||||
private bool ShowsGroupHeader(FieldGroup group) => this.BuildVisibleFieldGroups().Count > 1 || !string.IsNullOrEmpty(group.Key);
|
||||
|
||||
/// <remarks>
|
||||
/// The ungrouped fields have no name of their own, so the label hook hands back their
|
||||
/// empty group name. A tool may still name them through that same hook; when it does not,
|
||||
/// they are simply what is left over next to the named groups.
|
||||
/// </remarks>
|
||||
private string GetGroupLabel(string groupKey)
|
||||
{
|
||||
var label = this.implementation?.GetSettingsGroupLabel(groupKey) ?? groupKey;
|
||||
return string.IsNullOrEmpty(label) ? T("General") : label;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ToolSettingsGroupLink> GetGroupLinks(string groupKey) => this.implementation?.GetSettingsGroupLinks(groupKey) ?? [];
|
||||
|
||||
/// <summary>
|
||||
/// What the tool wants to say about the settings as they stand right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Asked on every render, so a warning follows the value it is about instead of waiting for
|
||||
/// the next save. These are not errors: they describe settings that are allowed and do
|
||||
/// something other than what they look like, and the dialog saves them either way.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<string> GetSettingsWarnings() => this.implementation?.GetSettingsWarnings(this.values) ?? [];
|
||||
|
||||
private string GetFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.implementation?.GetSettingsFieldLabel(fieldName, fieldDefinition) ?? fieldDefinition.Title;
|
||||
|
||||
private string GetFieldDescription(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.GetFieldDescriptionWithDefault(fieldName, fieldDefinition);
|
||||
|
||||
private string GetFieldDefaultValue(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
this.implementation?.GetSettingsFieldDefaultValue(fieldName, fieldDefinition) ?? string.Empty;
|
||||
|
||||
private string GetFieldDescriptionWithDefault(string fieldName, ToolSettingsFieldDefinition fieldDefinition)
|
||||
{
|
||||
var description = this.implementation?.GetSettingsFieldDescription(fieldName, fieldDefinition) ?? fieldDefinition.Description;
|
||||
var defaultValue = this.GetFieldDefaultValue(fieldName, fieldDefinition);
|
||||
if (string.IsNullOrWhiteSpace(defaultValue))
|
||||
return description;
|
||||
|
||||
return string.Format(T("{0} Default: {1}"), description, defaultValue);
|
||||
}
|
||||
|
||||
private bool IsFieldDisabled(string fieldName) =>
|
||||
this.toolDefinition is not null && this.ToolSettingsService.IsFieldLocked(this.toolDefinition, fieldName);
|
||||
|
||||
private string GetFieldPlaceholder(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
|
||||
string.IsNullOrWhiteSpace(this.GetValue(fieldName)) ? this.GetFieldDefaultValue(fieldName, fieldDefinition) : string.Empty;
|
||||
|
||||
private void UpdateValue(string fieldName, string? value)
|
||||
{
|
||||
this.values[fieldName] = value ?? string.Empty;
|
||||
this.validationMessage = string.Empty;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (this.toolDefinition is null)
|
||||
return;
|
||||
|
||||
var validationState = await this.ToolSettingsService.ValidateSettingsAsync(this.toolDefinition, this.values, this.implementation);
|
||||
if (!validationState.IsConfigured)
|
||||
{
|
||||
this.validationMessage = !string.IsNullOrWhiteSpace(validationState.Message)
|
||||
? validationState.Message
|
||||
: string.Format(T("Please configure the required settings: {0}"), string.Join(", ", validationState.MissingRequiredFields));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ToolSettingsService.SaveSettingsAsync(this.toolDefinition, this.values);
|
||||
this.MudDialog.Close();
|
||||
}
|
||||
|
||||
/// <param name="Key">The group's name from the schema, or empty for the ungrouped fields.</param>
|
||||
/// <param name="Fields">The fields of this group, in the order the tool declared them.</param>
|
||||
private sealed record FieldGroup(string Key, List<KeyValuePair<string, ToolSettingsFieldDefinition>> Fields);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@using AIStudio.Tools.PluginSystem
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Dataset" Class="mr-2" />
|
||||
@T("Export tool configuration")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (this.IsAdmin)
|
||||
{
|
||||
@if (!string.IsNullOrWhiteSpace(this.message))
|
||||
{
|
||||
<MudAlert Severity="@this.messageSeverity" Class="mb-4">@this.message</MudAlert>
|
||||
}
|
||||
|
||||
@if (this.isLoading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="@true" Size="Size.Small" />
|
||||
<MudText>@T("Loading tool configuration...")</MudText>
|
||||
}
|
||||
else if (this.toolDefinition is null || this.implementation is null)
|
||||
{
|
||||
@if (string.IsNullOrWhiteSpace(this.message))
|
||||
{
|
||||
<MudText>@T("The selected tool could not be loaded.")</MudText>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.subtitle1" Class="mb-2">@this.implementation.GetDisplayName()</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-4">
|
||||
@T("Export saved settings as Lua code for your configuration plugin. You can combine exports and adapt the code before deploying it.")
|
||||
</MudText>
|
||||
|
||||
@if (this.areas.Count > 0)
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.subtitle2">@T("Settings to include")</MudText>
|
||||
@if (this.areas.Count > 1)
|
||||
{
|
||||
<MudCheckBox T="bool" Label="@T("Select all")" Value="@this.AllAreasSelected" ValueChanged="@this.SelectAllAreas" Disabled="@this.isExporting" />
|
||||
}
|
||||
@foreach (var area in this.areas)
|
||||
{
|
||||
<MudCheckBox @key="area.Id" T="bool" Label="@area.Label" Value="@this.selectedAreaIds.Contains(area.Id)" ValueChanged="@(selected => this.SelectArea(area.Id, selected))" Disabled="@this.isExporting" />
|
||||
}
|
||||
<MudText Typo="Typo.caption">@T("Each area is independent. Select general settings separately if you want to include them.")</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudSelect T="ToolSettingsExportMode" Label="@T("Export mode")" @bind-Value="this.mode" Variant="Variant.Outlined" Class="mb-4" Disabled="@this.isExporting" HelperText="@T("This choice applies to settings other than secrets and the minimum provider confidence.")">
|
||||
<MudSelectItem T="ToolSettingsExportMode" Value="@ToolSettingsExportMode.LOCKED">@T("Locked settings")</MudSelectItem>
|
||||
<MudSelectItem T="ToolSettingsExportMode" Value="@ToolSettingsExportMode.DEFAULT">@T("Editable defaults")</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
@if (this.WarnAboutEmptyLockedSettings)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">
|
||||
@string.Format(T("{0} of the selected settings are empty and are exported as empty locked values. Users cannot change a locked setting, so an empty required one leaves the tool unusable. Deselect the areas you have not configured, or export them as editable defaults."), this.EmptySelectedFieldCount)
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
<MudCheckBox T="bool" Label="@T("Include encrypted API keys and other secrets")" @bind-Value="this.includeSecrets" Disabled="@(this.isExporting || !this.CanIncludeSecrets)" />
|
||||
@if (PluginFactory.EnterpriseEncryption?.IsAvailable is not true)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("No enterprise encryption secret is configured. API keys and other secrets cannot be exported.")</MudText>
|
||||
}
|
||||
else if (!this.HasSelectedSecrets)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("The selected areas contain no configured API keys or other secrets.")</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("Secrets are always exported as locked settings. Recipients need the same enterprise encryption secret to use them.")</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
<MudCheckBox T="bool" Label="@T("Include minimum provider confidence")" @bind-Value="this.includeMinimumProviderConfidence" Disabled="@this.isExporting" />
|
||||
<MudText Typo="Typo.body2" Class="mb-2">@string.Format(T("Current requirement: {0}"), this.GetMinimumProviderConfidenceName())</MudText>
|
||||
<MudText Typo="Typo.body2">@T("This setting is always locked and applies to the entire tool. The configuration plugin locks minimum provider confidence levels together for all tools in its confidence table.")</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Text">@T("Cancel")</MudButton>
|
||||
<MudButton OnClick="@this.Export" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" Disabled="@(!this.CanExport)">
|
||||
@T("Export to clipboard")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,204 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class ToolSettingsExportDialog : SettingsDialogBase
|
||||
{
|
||||
[Parameter]
|
||||
public string ToolId { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ToolSettingsService ToolSettingsService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ToolSettingsExportDialog> Logger { get; init; } = null!;
|
||||
|
||||
private ToolDefinition? toolDefinition;
|
||||
private IToolImplementation? implementation;
|
||||
private IReadOnlyList<ExportableSettings> areas = [];
|
||||
private HashSet<string> selectedAreaIds = new(StringComparer.Ordinal);
|
||||
private HashSet<string> configuredSecretFields = new(StringComparer.Ordinal);
|
||||
private HashSet<string> emptyFieldNames = new(StringComparer.Ordinal);
|
||||
private ToolSettingsExportMode mode = ToolSettingsExportMode.LOCKED;
|
||||
private bool includeSecrets;
|
||||
private bool includeMinimumProviderConfidence = true;
|
||||
private bool isLoading = true;
|
||||
private bool isExporting;
|
||||
private bool isDisposed;
|
||||
private string message = string.Empty;
|
||||
private Severity messageSeverity = Severity.Error;
|
||||
|
||||
private bool IsAdmin => this.SettingsManager.ConfigurationData.App.ShowAdminSettings;
|
||||
|
||||
private bool AllAreasSelected => this.areas.Count > 0 && this.areas.All(area => this.selectedAreaIds.Contains(area.Id));
|
||||
|
||||
private bool HasSelectedSecrets => this.areas.Any(area => this.selectedAreaIds.Contains(area.Id) && area.FieldNames.Any(this.configuredSecretFields.Contains));
|
||||
|
||||
private bool CanIncludeSecrets => this.HasSelectedSecrets && PluginFactory.EnterpriseEncryption?.IsAvailable is true;
|
||||
|
||||
/// <summary>
|
||||
/// How many of the selected settings hold no value, counting a field shared by two areas once.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Saving a tool's settings writes every field of its schema, empty ones included, so an area
|
||||
/// the administrator never filled in still exports. Locked, those empty values are what the
|
||||
/// recipient is left with and cannot change, which is worth saying before the export.
|
||||
/// </remarks>
|
||||
private int EmptySelectedFieldCount => this.areas
|
||||
.Where(area => this.selectedAreaIds.Contains(area.Id))
|
||||
.SelectMany(area => area.FieldNames)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count(this.emptyFieldNames.Contains);
|
||||
|
||||
private bool WarnAboutEmptyLockedSettings => this.mode is ToolSettingsExportMode.LOCKED && this.EmptySelectedFieldCount > 0;
|
||||
|
||||
private bool CanExport => this.IsAdmin && !this.isLoading && !this.isExporting && !this.isDisposed &&
|
||||
this.toolDefinition is not null && this.implementation is not null && (this.selectedAreaIds.Count > 0 || this.includeMinimumProviderConfidence);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
if (!this.IsAdmin)
|
||||
{
|
||||
this.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.toolDefinition = this.ToolRegistry.GetDefinition(this.ToolId);
|
||||
if (this.toolDefinition is null)
|
||||
return;
|
||||
|
||||
this.implementation = this.ToolRegistry.GetImplementation(this.toolDefinition.ImplementationKey);
|
||||
if (this.implementation is null)
|
||||
return;
|
||||
|
||||
this.areas = this.implementation.GetExportableSettings(this.toolDefinition);
|
||||
this.selectedAreaIds = this.areas.Select(area => area.Id).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
// Retain only field names, never the values themselves, so no plaintext secret lives
|
||||
// in this component. ExportAsync reads effective settings again when the
|
||||
// administrator exports.
|
||||
var values = await this.ToolSettingsService.GetSettingsAsync(this.toolDefinition);
|
||||
this.configuredSecretFields = this.toolDefinition.SettingsSchema.Properties
|
||||
.Where(property => property.Value.Secret && values.TryGetValue(property.Key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
.Select(property => property.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
// A field the export writes as an empty value: it has to be present, because a
|
||||
// missing one is skipped rather than exported, and it has to be a non-secret,
|
||||
// because an empty secret is skipped as well.
|
||||
this.emptyFieldNames = this.toolDefinition.SettingsSchema.Properties
|
||||
.Where(property => !property.Value.Secret && values.TryGetValue(property.Key, out var value) && string.IsNullOrWhiteSpace(value))
|
||||
.Select(property => property.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// A runtime error may contain secret data, so it goes to the log for diagnosis but
|
||||
// never into the dialog:
|
||||
this.Logger.LogError(e, "Failed to load the configuration of the tool '{ToolId}' for export.", this.ToolId);
|
||||
this.toolDefinition = null;
|
||||
this.message = T("The tool configuration could not be loaded. Please close this dialog and try again.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectArea(string areaId, bool selected)
|
||||
{
|
||||
if (selected)
|
||||
this.selectedAreaIds.Add(areaId);
|
||||
else
|
||||
this.selectedAreaIds.Remove(areaId);
|
||||
|
||||
this.SelectionChanged();
|
||||
}
|
||||
|
||||
private void SelectAllAreas(bool selected)
|
||||
{
|
||||
this.selectedAreaIds = selected ? this.areas.Select(area => area.Id).ToHashSet(StringComparer.Ordinal) : new(StringComparer.Ordinal);
|
||||
this.SelectionChanged();
|
||||
}
|
||||
|
||||
private void SelectionChanged()
|
||||
{
|
||||
// A new selection must not keep an invisible opt-in to secrets it no longer contains.
|
||||
if (!this.CanIncludeSecrets)
|
||||
this.includeSecrets = false;
|
||||
|
||||
this.message = string.Empty;
|
||||
}
|
||||
|
||||
private string GetMinimumProviderConfidenceName()
|
||||
{
|
||||
var confidence = this.toolDefinition is null ? ConfidenceLevel.NONE : this.ToolRegistry.GetMinimumProviderConfidence(this.toolDefinition);
|
||||
return confidence is ConfidenceLevel.NONE ? T("No minimum confidence level chosen") : confidence.GetName();
|
||||
}
|
||||
|
||||
private async Task Export()
|
||||
{
|
||||
if (!this.CanExport || this.toolDefinition is null || this.implementation is null)
|
||||
return;
|
||||
|
||||
this.isExporting = true;
|
||||
this.message = string.Empty;
|
||||
this.messageSeverity = Severity.Error;
|
||||
try
|
||||
{
|
||||
var options = new ToolSettingsExportOptions
|
||||
{
|
||||
SelectedAreaIds = new HashSet<string>(this.selectedAreaIds, StringComparer.Ordinal),
|
||||
Mode = this.mode,
|
||||
IncludeSecrets = this.includeSecrets,
|
||||
IncludeMinimumProviderConfidence = this.includeMinimumProviderConfidence,
|
||||
};
|
||||
|
||||
var result = await this.ToolSettingsService.ExportAsync(this.toolDefinition, this.implementation, options);
|
||||
if (this.isDisposed || !this.IsAdmin)
|
||||
return;
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
this.message = result.ErrorMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.LuaCode))
|
||||
{
|
||||
this.messageSeverity = Severity.Info;
|
||||
this.message = T("The selected areas contain no settings to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The runtime reports clipboard success or failure. Keep the dialog open so that
|
||||
// administrators can retry or export another selection from the same tool.
|
||||
await this.RustService.CopyText2Clipboard(result.LuaCode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.Logger.LogError(e, "Failed to export the configuration of the tool '{ToolId}'.", this.ToolId);
|
||||
this.message = T("The tool configuration could not be exported. Please try again.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isExporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.isDisposed = true;
|
||||
base.DisposeResources();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Provider.HuggingFace
|
||||
@using AIStudio.Provider.SelfHosted
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@if (this.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-4">
|
||||
@T("This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
|
||||
</MudAlert>
|
||||
}
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
|
||||
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
|
||||
{
|
||||
if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE)
|
||||
{
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
<ProviderLabel ProviderType="@provider" CustomIconDataUrl="@(provider == this.DataLLMProvider ? this.DataCustomIconDataUrl : string.Empty)" Text="@provider.ToName()" />
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
@@ -38,13 +45,14 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingHostname"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHostNeeded())
|
||||
{
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
|
||||
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
|
||||
@foreach (Host host in Enum.GetValues(typeof(Host)))
|
||||
{
|
||||
if (host.IsTranscriptionSupported())
|
||||
@@ -57,6 +65,24 @@
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.DataLLMProvider.IsHFInstanceProviderNeeded())
|
||||
{
|
||||
<MudSelect T="HFInferenceProvider" Value="@this.HFInferenceProviderId" ValueChanged="@this.OnHFInferenceProviderChanged" Label="@T("Hugging Face Inference Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHFInstanceProviderForTranscription">
|
||||
@foreach (HFInferenceProvider inferenceProvider in Enum.GetValues(typeof(HFInferenceProvider)))
|
||||
{
|
||||
@if (inferenceProvider.SupportsTranscription())
|
||||
{
|
||||
<MudSelectItem Value="@inferenceProvider">
|
||||
@inferenceProvider.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
<MudJustifiedText Class="mb-3">
|
||||
@T("Hugging Face transcribes audio through a few of its inference providers only, which is why this list is shorter than the one for chatting.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
@if (!this.DataLLMProvider.IsTranscriptionModelSelectionHidden(this.DataHost))
|
||||
{
|
||||
<MudField FullWidth="true" Label="@T("Model selection")" Variant="Variant.Outlined" Class="mb-3">
|
||||
@@ -71,6 +97,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.ValidateManuallyModel"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
HelperText="@T("Currently, we cannot query the transcription models for the selected provider and/or host. Therefore, please enter the model name manually.")"
|
||||
@@ -78,7 +105,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
|
||||
@T("Load")
|
||||
</MudButton>
|
||||
@if(this.availableModels.Count is 0)
|
||||
@@ -89,7 +116,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
|
||||
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
|
||||
AdornmentColor="Color.Info" Adornment="Adornment.Start"
|
||||
Validation="@this.providerValidation.ValidatingModel">
|
||||
@@ -132,6 +159,7 @@
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
Disabled="@this.IsEnterpriseConfiguration"
|
||||
Validation="@this.providerValidation.ValidatingInstanceName"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
@@ -154,4 +182,4 @@
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -1,5 +1,6 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.Validation;
|
||||
@@ -56,19 +57,38 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataCustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The transcription model to use.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Model DataModel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Hugging Face inference provider to use.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public HFInferenceProvider HFInferenceProviderId { get; set; } = HFInferenceProvider.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// Should the dialog be in editing mode?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEditing { get; init; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this transcription provider is managed by an enterprise configuration plugin. When
|
||||
/// true, every field except the API key is locked, matching Settings.TranscriptionProvider.IsEnterpriseConfiguration.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsEnterpriseConfiguration { get; set; }
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
@@ -85,6 +105,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataAPIKey = string.Empty;
|
||||
private bool dataHadStoredAPIKeyOnLoad;
|
||||
private string dataManuallyModel = string.Empty;
|
||||
private string dataAPIKeyStorageIssue = string.Empty;
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
@@ -149,8 +170,10 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
Host = this.DataHost,
|
||||
IsEnterpriseConfiguration = false,
|
||||
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
|
||||
EnterpriseConfigurationPluginId = Guid.Empty,
|
||||
CustomIconDataUrl = this.DataCustomIconDataUrl,
|
||||
HFInferenceProvider = this.HFInferenceProviderId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,11 +212,17 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
// Load the API key:
|
||||
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
|
||||
if (requestedSecret.Success)
|
||||
{
|
||||
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.dataAPIKey = string.Empty;
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
|
||||
|
||||
// For an enterprise-managed provider, having no key yet is the expected first-run
|
||||
// state, not a storage failure -- the user is just about to set their own key:
|
||||
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
|
||||
{
|
||||
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
|
||||
await this.form.Validate();
|
||||
@@ -218,8 +247,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.DataLLMProvider.ToSecretId();
|
||||
|
||||
// Must mirror Settings.TranscriptionProvider.SecretId exactly: when editing an enterprise-managed
|
||||
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
|
||||
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
|
||||
// silently end up in the wrong keyring row and never be found again.
|
||||
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
|
||||
|
||||
public string SecretName => this.DataName;
|
||||
|
||||
#endregion
|
||||
@@ -255,6 +288,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHadStoredAPIKeyOnLoad = true;
|
||||
}
|
||||
else if (this.dataHadStoredAPIKeyOnLoad)
|
||||
{
|
||||
// The user cleared a previously stored key. Without this, the old key would simply
|
||||
// stay in the OS keyring untouched and keep being used:
|
||||
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
|
||||
if (!deleteResponse.Success)
|
||||
{
|
||||
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataHadStoredAPIKeyOnLoad = false;
|
||||
}
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));
|
||||
@@ -290,6 +339,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the model selection when the user picks another Hugging Face inference provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each inference provider offers transcription models of its own, so the models loaded for the
|
||||
/// previous one say nothing about the new one.
|
||||
/// </remarks>
|
||||
/// <param name="selectedInferenceProvider">The inference provider the user chose.</param>
|
||||
private void OnHFInferenceProviderChanged(HFInferenceProvider selectedInferenceProvider)
|
||||
{
|
||||
this.HFInferenceProviderId = selectedInferenceProvider;
|
||||
this.DataModel = default;
|
||||
this.availableModels.Clear();
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
}
|
||||
|
||||
private async Task ReloadModels()
|
||||
{
|
||||
this.dataLoadingModelsIssue = string.Empty;
|
||||
@@ -324,4 +389,4 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
};
|
||||
|
||||
private bool IsNoneProvider => this.DataLLMProvider is LLMProviders.NONE;
|
||||
}
|
||||
}
|
||||
@@ -179,17 +179,23 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
/// <summary>
|
||||
/// Removes the escape key handler from the browser before this dialog goes away.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base class runs this before DisposeResources, which is what lets us await the call. The
|
||||
/// previous attempt discarded it inside a try/catch: a failing JS call reports itself on the task,
|
||||
/// not to the caller, so that catch never ran and the fault ended up as an unobserved task
|
||||
/// exception whenever the circuit was already gone.
|
||||
/// </remarks>
|
||||
protected override async ValueTask DisposeResourcesAsync()
|
||||
{
|
||||
await this.JsRuntime.TryInvokeVoidAsync(this.CircuitState, "unregisterEscapeHandler", this.escapeHandlerId);
|
||||
await base.DisposeResourcesAsync();
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = this.JsRuntime.InvokeVoidAsync("unregisterEscapeHandler", this.escapeHandlerId).AsTask();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore JS cleanup errors while the dialog is being disposed.
|
||||
}
|
||||
|
||||
this.dotNetReference?.Dispose();
|
||||
this.dotNetReference = null;
|
||||
|
||||
|
||||
Reference in new issue
Block a user