@this.ChildContent
-
@if (this.FooterContent is not null)
{
-
+
@this.FooterContent
}
diff --git a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs
index 126317cd..29f4847b 100644
--- a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs
+++ b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs
@@ -9,14 +9,6 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter]
public bool FillEntireHorizontalSpace { get; set; }
- ///
- /// Set the height of anything above the scrolling content; usually a header.
- /// What we do is calc(100vh - HeaderHeight). Means, you can use multiple measures like
- /// 230px - 3em. Default is 3em.
- ///
- [Parameter]
- public string HeaderHeight { get; set; } = "3em";
-
[Parameter]
public RenderFragment? HeaderContent { get; set; }
@@ -34,6 +26,9 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter]
public string? MinWidth { get; set; }
+
+ [Parameter]
+ public string Style { get; set; } = string.Empty;
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
@@ -55,6 +50,8 @@ public partial class InnerScrolling : MSGComponentBase
#region Overrides of MSGComponentBase
+ public override string ComponentName => nameof(InnerScrolling);
+
public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
@@ -74,12 +71,14 @@ public partial class InnerScrolling : MSGComponentBase
#endregion
- private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth};";
+ private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
+
+ private string TerminatedStyles => string.IsNullOrWhiteSpace(this.Style) ? string.Empty : $"{this.Style}; ";
- private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0; {this.MinWidthStyle}" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0; {this.MinWidthStyle}";
-
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
+ private string Styles => $"flex-grow: 1; overflow: hidden; {this.TerminatedStyles}{this.MinWidthStyle}";
+
public async Task ScrollToBottom()
{
await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime);
diff --git a/app/MindWork AI Studio/Components/MSGComponentBase.cs b/app/MindWork AI Studio/Components/MSGComponentBase.cs
index 940ec78e..4dddb57d 100644
--- a/app/MindWork AI Studio/Components/MSGComponentBase.cs
+++ b/app/MindWork AI Studio/Components/MSGComponentBase.cs
@@ -24,6 +24,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
#region Implementation of IMessageBusReceiver
+ public abstract string ComponentName { get; }
+
public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
diff --git a/app/MindWork AI Studio/Components/MudTextList.razor.cs b/app/MindWork AI Studio/Components/MudTextList.razor.cs
index 551878b0..46cde417 100644
--- a/app/MindWork AI Studio/Components/MudTextList.razor.cs
+++ b/app/MindWork AI Studio/Components/MudTextList.razor.cs
@@ -14,7 +14,7 @@ public partial class MudTextList : ComponentBase
public string Icon { get; set; } = Icons.Material.Filled.CheckCircle;
[Parameter]
- public string Class { get; set; } = "";
+ public string Class { get; set; } = string.Empty;
private string Classes => $"mud-text-list {this.Class}";
}
diff --git a/app/MindWork AI Studio/Components/ConfidenceInfoMode.cs b/app/MindWork AI Studio/Components/PopoverTriggerMode.cs
similarity index 63%
rename from app/MindWork AI Studio/Components/ConfidenceInfoMode.cs
rename to app/MindWork AI Studio/Components/PopoverTriggerMode.cs
index d7e63da6..c122e409 100644
--- a/app/MindWork AI Studio/Components/ConfidenceInfoMode.cs
+++ b/app/MindWork AI Studio/Components/PopoverTriggerMode.cs
@@ -1,6 +1,6 @@
namespace AIStudio.Components;
-public enum ConfidenceInfoMode
+public enum PopoverTriggerMode
{
BUTTON,
ICON,
diff --git a/app/MindWork AI Studio/Components/PreviewAlpha.razor b/app/MindWork AI Studio/Components/PreviewAlpha.razor
index 99f9d844..b1b629d8 100644
--- a/app/MindWork AI Studio/Components/PreviewAlpha.razor
+++ b/app/MindWork AI Studio/Components/PreviewAlpha.razor
@@ -1,4 +1,4 @@
-
+
Alpha
diff --git a/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs b/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs
index 62466852..deb962c4 100644
--- a/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs
+++ b/app/MindWork AI Studio/Components/PreviewAlpha.razor.cs
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
-public partial class PreviewAlpha : ComponentBase;
\ No newline at end of file
+public partial class PreviewAlpha : ComponentBase
+{
+ [Parameter]
+ public bool ApplyInnerScrollingFix { get; set; }
+
+ private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/PreviewBeta.razor b/app/MindWork AI Studio/Components/PreviewBeta.razor
index cd6b3c65..dd54225e 100644
--- a/app/MindWork AI Studio/Components/PreviewBeta.razor
+++ b/app/MindWork AI Studio/Components/PreviewBeta.razor
@@ -1,4 +1,4 @@
-
+
Beta
diff --git a/app/MindWork AI Studio/Components/PreviewBeta.razor.cs b/app/MindWork AI Studio/Components/PreviewBeta.razor.cs
index a5064b60..d8fee758 100644
--- a/app/MindWork AI Studio/Components/PreviewBeta.razor.cs
+++ b/app/MindWork AI Studio/Components/PreviewBeta.razor.cs
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
-public partial class PreviewBeta : ComponentBase;
\ No newline at end of file
+public partial class PreviewBeta : ComponentBase
+{
+ [Parameter]
+ public bool ApplyInnerScrollingFix { get; set; }
+
+ private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/PreviewExperimental.razor b/app/MindWork AI Studio/Components/PreviewExperimental.razor
index 59e6651a..bedc9e4f 100644
--- a/app/MindWork AI Studio/Components/PreviewExperimental.razor
+++ b/app/MindWork AI Studio/Components/PreviewExperimental.razor
@@ -1,4 +1,4 @@
-
+
Experimental
diff --git a/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs b/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs
index c66fa730..0588d489 100644
--- a/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs
+++ b/app/MindWork AI Studio/Components/PreviewExperimental.razor.cs
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
-public partial class PreviewExperimental : ComponentBase;
\ No newline at end of file
+public partial class PreviewExperimental : ComponentBase
+{
+ [Parameter]
+ public bool ApplyInnerScrollingFix { get; set; }
+
+ private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/PreviewPrototype.razor b/app/MindWork AI Studio/Components/PreviewPrototype.razor
index f645e0ca..9aa8bbc0 100644
--- a/app/MindWork AI Studio/Components/PreviewPrototype.razor
+++ b/app/MindWork AI Studio/Components/PreviewPrototype.razor
@@ -1,4 +1,4 @@
-
+
Prototype
diff --git a/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs b/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs
index 573e2fd0..3ceab4d1 100644
--- a/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs
+++ b/app/MindWork AI Studio/Components/PreviewPrototype.razor.cs
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
-public partial class PreviewPrototype : ComponentBase;
\ No newline at end of file
+public partial class PreviewPrototype : ComponentBase
+{
+ [Parameter]
+ public bool ApplyInnerScrollingFix { get; set; }
+
+ private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor
index 44b51084..86954ddd 100644
--- a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor
+++ b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor
@@ -1,4 +1,4 @@
-
+
Release Candidate
diff --git a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs
index 1d22d17e..249f1f35 100644
--- a/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs
+++ b/app/MindWork AI Studio/Components/PreviewReleaseCandidate.razor.cs
@@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
-public partial class PreviewReleaseCandidate : ComponentBase;
\ No newline at end of file
+public partial class PreviewReleaseCandidate : ComponentBase
+{
+ [Parameter]
+ public bool ApplyInnerScrollingFix { get; set; }
+
+ private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor b/app/MindWork AI Studio/Components/ProfileSelection.razor
index a832ec60..f4acf35a 100644
--- a/app/MindWork AI Studio/Components/ProfileSelection.razor
+++ b/app/MindWork AI Studio/Components/ProfileSelection.razor
@@ -1,5 +1,5 @@
-
-
+
+
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{
diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs
index 55f2fa99..d2b41a57 100644
--- a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs
+++ b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs
@@ -14,11 +14,14 @@ public partial class ProfileSelection : ComponentBase
[Parameter]
public string MarginLeft { get; set; } = "ml-3";
+
+ [Parameter]
+ public string MarginRight { get; set; } = string.Empty;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
- private string MarginClass => $"{this.MarginLeft}";
+ private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
private async Task SelectionChanged(Profile profile)
{
diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor b/app/MindWork AI Studio/Components/ProviderSelection.razor
index 3ac64b96..9082f016 100644
--- a/app/MindWork AI Studio/Components/ProviderSelection.razor
+++ b/app/MindWork AI Studio/Components/ProviderSelection.razor
@@ -1,6 +1,6 @@
@using AIStudio.Settings
-
+
@foreach (var provider in this.GetAvailableProviders())
{
diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs
index 927c1d62..66158211 100644
--- a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs
+++ b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
using AIStudio.Assistants;
using AIStudio.Provider;
using AIStudio.Settings;
@@ -9,7 +11,7 @@ namespace AIStudio.Components;
public partial class ProviderSelection : ComponentBase
{
[CascadingParameter]
- public AssistantBase? AssistantBase { get; set; }
+ public AssistantBase? AssistantBase { get; set; }
[Parameter]
public AIStudio.Settings.Provider ProviderSettings { get; set; }
@@ -29,6 +31,7 @@ public partial class ProviderSelection : ComponentBase
await this.ProviderSettingsChanged.InvokeAsync(provider);
}
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable GetAvailableProviders()
{
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE);
diff --git a/app/MindWork AI Studio/Components/ReadWebContent.razor b/app/MindWork AI Studio/Components/ReadWebContent.razor
index 1d83309c..9cb451b2 100644
--- a/app/MindWork AI Studio/Components/ReadWebContent.razor
+++ b/app/MindWork AI Studio/Components/ReadWebContent.razor
@@ -2,7 +2,7 @@
@if (this.showWebContentReader)
{
-
+
diff --git a/app/MindWork AI Studio/Components/ReadWebContent.razor.cs b/app/MindWork AI Studio/Components/ReadWebContent.razor.cs
index aebe7ef4..6cf40701 100644
--- a/app/MindWork AI Studio/Components/ReadWebContent.razor.cs
+++ b/app/MindWork AI Studio/Components/ReadWebContent.razor.cs
@@ -59,11 +59,7 @@ public partial class ReadWebContent : ComponentBase
if(this.PreselectContentCleanerAgent)
this.useContentCleanerAgent = true;
- if (this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)
- this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider);
- else
- this.providerSettings = this.ProviderSettings;
-
+ this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_TEXT_CONTENT_CLEANER, this.ProviderSettings.Id, true);
await base.OnInitializedAsync();
}
diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor b/app/MindWork AI Studio/Components/SelectDirectory.razor
index 95f09d69..29a0fc8f 100644
--- a/app/MindWork AI Studio/Components/SelectDirectory.razor
+++ b/app/MindWork AI Studio/Components/SelectDirectory.razor
@@ -4,6 +4,7 @@
Text="@this.Directory"
Label="@this.Label"
ReadOnly="@true"
+ Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor.cs b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs
index ec4f6cd3..a4ebbf8b 100644
--- a/app/MindWork AI Studio/Components/SelectDirectory.razor.cs
+++ b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs
@@ -1,4 +1,5 @@
using AIStudio.Settings;
+using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
@@ -21,6 +22,9 @@ public partial class SelectDirectory : ComponentBase
[Parameter]
public string DirectoryDialogTitle { get; set; } = "Select Directory";
+ [Parameter]
+ public Func Validation { get; set; } = _ => null;
+
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
diff --git a/app/MindWork AI Studio/Components/SelectFile.razor b/app/MindWork AI Studio/Components/SelectFile.razor
new file mode 100644
index 00000000..34842360
--- /dev/null
+++ b/app/MindWork AI Studio/Components/SelectFile.razor
@@ -0,0 +1,17 @@
+
+
+
+
+ Choose File
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs
new file mode 100644
index 00000000..d4a03ad5
--- /dev/null
+++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs
@@ -0,0 +1,64 @@
+using AIStudio.Settings;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components;
+
+public partial class SelectFile : ComponentBase
+{
+ [Parameter]
+ public string File { get; set; } = string.Empty;
+
+ [Parameter]
+ public EventCallback FileChanged { get; set; }
+
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ [Parameter]
+ public string Label { get; set; } = string.Empty;
+
+ [Parameter]
+ public string FileDialogTitle { get; set; } = "Select File";
+
+ [Parameter]
+ public Func Validation { get; set; } = _ => null;
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ [Inject]
+ public RustService RustService { get; set; } = null!;
+
+ [Inject]
+ protected ILogger Logger { get; init; } = null!;
+
+ private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new();
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Configure the spellchecking for the instance name input:
+ this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private void InternalFileChanged(string file)
+ {
+ this.File = file;
+ this.FileChanged.InvokeAsync(file);
+ }
+
+ private async Task OpenFileDialog()
+ {
+ var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
+ this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
+
+ if (!response.UserCancelled)
+ this.InternalFileChanged(response.SelectedFilePath);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgenda.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgenda.razor
deleted file mode 100644
index 6412d159..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgenda.razor
+++ /dev/null
@@ -1,32 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgenda.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgenda.razor.cs
deleted file mode 100644
index 82368a5c..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgenda.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelAgenda : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor
index 6bdd3770..8da6ff3e 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentContentCleaner.razor
@@ -6,7 +6,7 @@
Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things,
and attempts to convert relative links into absolute links so that they can be used.
-
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor
new file mode 100644
index 00000000..f3c1df20
--- /dev/null
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor
@@ -0,0 +1,11 @@
+@inherits SettingsPanelBase
+
+
+
+
+ Use Case: this agent is used to select the appropriate data sources for the current prompt.
+
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor.cs
new file mode 100644
index 00000000..1c191a55
--- /dev/null
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentDataSourceSelection.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Components.Settings;
+
+public partial class SettingsPanelAgentDataSourceSelection : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor
new file mode 100644
index 00000000..7de4ea81
--- /dev/null
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor
@@ -0,0 +1,18 @@
+@inherits SettingsPanelBase
+
+
+
+ Use Case: this agent is used to validate any retrieval context of any retrieval process. Perhaps there are many of these
+ retrieval contexts and you want to validate them all. Therefore, you might want to use a cheap and fast LLM for this
+ job. When using a local or self-hosted LLM, look for a small (e.g. 3B) and fast model.
+
+
+ @if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
+ {
+
+
+
+
+
+ }
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor.cs
new file mode 100644
index 00000000..aaf0d938
--- /dev/null
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelAgentRetrievalContextValidation.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Components.Settings;
+
+public partial class SettingsPanelAgentRetrievalContextValidation : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
index 5cdb3263..e67aaf5f 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
@@ -19,6 +19,6 @@
}
}
-
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAssistantBias.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelAssistantBias.razor
deleted file mode 100644
index 40f178e2..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAssistantBias.razor
+++ /dev/null
@@ -1,29 +0,0 @@
-@using AIStudio.Settings
-@using AIStudio.Settings.DataModel
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
- You have learned about @this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias.Count out of @BiasCatalog.ALL_BIAS.Count biases.
-
-
- Reset
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs
index f7f3a1d2..c3384167 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs
@@ -1,4 +1,5 @@
using AIStudio.Settings;
+using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelChat.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelChat.razor
index 602af631..8b274405 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelChat.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelChat.razor
@@ -1,4 +1,5 @@
@using AIStudio.Settings
+@using AIStudio.Settings.DataModel
@inherits SettingsPanelBase
@@ -9,7 +10,13 @@
-
+
+
+ @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
+ {
+
+
+ }
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelCoding.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelCoding.razor
deleted file mode 100644
index 12bf8795..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelCoding.razor
+++ /dev/null
@@ -1,18 +0,0 @@
-@using AIStudio.Assistants.Coding
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.Coding.PreselectedProgrammingLanguage is CommonCodingLanguages.OTHER)
- {
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelCoding.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelCoding.razor.cs
deleted file mode 100644
index 060a30a6..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelCoding.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelCoding : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelERIServer.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelERIServer.razor
deleted file mode 100644
index 62d2fd0c..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelERIServer.razor
+++ /dev/null
@@ -1,19 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
- Most ERI server options can be customized and saved directly in the ERI server assistant.
- For this, the ERI server assistant has an auto-save function.
-
-
-
- Switch to ERI server assistant
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelERIServer.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelERIServer.razor.cs
deleted file mode 100644
index 0e01ed2f..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelERIServer.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelERIServer : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
index ea82ce7a..7203ff78 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
@@ -36,7 +36,7 @@
Name
Provider
Model
-
Actions
+
Actions
@context.Num
@@ -44,16 +44,18 @@
@context.UsedLLMProvider
@this.GetEmbeddingProviderModelName(context)
-
-
- Open Dashboard
-
-
- Edit
-
-
- Delete
-
+
+
+
+ Open Dashboard
+
+
+ Edit
+
+
+ Delete
+
+
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
index afa50246..7520e596 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
@@ -21,7 +21,17 @@ public partial class SettingsPanelEmbeddings : SettingsPanelBase
var modelName = provider.Model.ToString();
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
}
-
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ await this.UpdateEmbeddingProviders();
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
private async Task AddEmbeddingProvider()
{
var dialogParameters = new DialogParameters
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelGrammarSpelling.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelGrammarSpelling.razor
deleted file mode 100644
index 73b31ca6..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelGrammarSpelling.razor
+++ /dev/null
@@ -1,15 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelGrammarSpelling.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelGrammarSpelling.razor.cs
deleted file mode 100644
index b15d58a6..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelGrammarSpelling.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelGrammarSpelling : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelIconFinder.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelIconFinder.razor
deleted file mode 100644
index 76317e92..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelIconFinder.razor
+++ /dev/null
@@ -1,11 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelIconFinder.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelIconFinder.razor.cs
deleted file mode 100644
index e545163b..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelIconFinder.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelIconFinder : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelJobPostings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelJobPostings.razor
deleted file mode 100644
index b9b56a55..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelJobPostings.razor
+++ /dev/null
@@ -1,22 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.JobPostings.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelJobPostings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelJobPostings.razor.cs
deleted file mode 100644
index 20ecab68..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelJobPostings.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelJobPostings : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelLegalCheck.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelLegalCheck.razor
deleted file mode 100644
index 00c2f7d4..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelLegalCheck.razor
+++ /dev/null
@@ -1,14 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelLegalCheck.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelLegalCheck.razor.cs
deleted file mode 100644
index 66db4693..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelLegalCheck.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelLegalCheck : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelMyTasks.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelMyTasks.razor
deleted file mode 100644
index 2a443f76..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelMyTasks.razor
+++ /dev/null
@@ -1,16 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.MyTasks.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelMyTasks.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelMyTasks.razor.cs
deleted file mode 100644
index 2c4291dc..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelMyTasks.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelMyTasks : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor
index 924eb773..e49390f0 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProfiles.razor
@@ -23,18 +23,20 @@
#
Profile Name
- Actions
+ Actions
@context.Num
@context.Name
-
-
- Edit
-
-
- Delete
-
+
+
+
+ Edit
+
+
+ Delete
+
+
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
index cd1b4ffb..51db26b4 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
@@ -24,7 +24,7 @@
Instance Name
Provider
Model
- Actions
+ Actions
@context.Num
@@ -44,16 +44,18 @@
@("as selected by provider")
}
-
-
- Open Dashboard
-
-
- Edit
-
-
- Delete
-
+
+
+
+ Open Dashboard
+
+
+ Edit
+
+
+ Delete
+
+
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
index e2c434fe..0aa3afd0 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
@@ -26,6 +28,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
#endregion
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task AddLLMProvider()
{
var dialogParameters = new DialogParameters
@@ -48,6 +51,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
}
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
{
var dialogParameters = new DialogParameters
@@ -82,6 +86,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
}
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider)
{
var dialogParameters = new DialogParameters
@@ -112,6 +117,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
}
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private async Task UpdateProviders()
{
this.AvailableLLMProviders.Clear();
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelRewrite.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelRewrite.razor
deleted file mode 100644
index fe911994..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelRewrite.razor
+++ /dev/null
@@ -1,17 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelRewrite.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelRewrite.razor.cs
deleted file mode 100644
index ca72bef5..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelRewrite.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelRewrite : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelSynonyms.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelSynonyms.razor
deleted file mode 100644
index 6468c537..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelSynonyms.razor
+++ /dev/null
@@ -1,15 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelSynonyms.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelSynonyms.razor.cs
deleted file mode 100644
index c4a9ee40..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelSynonyms.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelSynonyms : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTextSummarizer.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTextSummarizer.razor
deleted file mode 100644
index f8afa494..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTextSummarizer.razor
+++ /dev/null
@@ -1,24 +0,0 @@
-@using AIStudio.Assistants.TextSummarizer
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
- @if(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity is Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS)
- {
-
- }
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTextSummarizer.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTextSummarizer.razor.cs
deleted file mode 100644
index 8eab6d8c..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTextSummarizer.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelTextSummarizer : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranslation.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranslation.razor
deleted file mode 100644
index 6ad87762..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranslation.razor
+++ /dev/null
@@ -1,20 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.Translation.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranslation.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranslation.razor.cs
deleted file mode 100644
index d68b5789..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranslation.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelTranslation : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelWritingEMails.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelWritingEMails.razor
deleted file mode 100644
index 0126ecf2..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelWritingEMails.razor
+++ /dev/null
@@ -1,19 +0,0 @@
-@using AIStudio.Settings
-@inherits SettingsPanelBase
-
-
-
-
-
-
-
- @if (this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage is CommonLanguages.OTHER)
- {
-
- }
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelWritingEMails.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelWritingEMails.razor.cs
deleted file mode 100644
index 5d87b618..00000000
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelWritingEMails.razor.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelWritingEMails : SettingsPanelBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/TextInfoLine.razor b/app/MindWork AI Studio/Components/TextInfoLine.razor
new file mode 100644
index 00000000..5e5de4da
--- /dev/null
+++ b/app/MindWork AI Studio/Components/TextInfoLine.razor
@@ -0,0 +1,19 @@
+
+
+
+ @if (this.ShowingCopyButton)
+ {
+
+
+
+ }
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/TextInfoLine.razor.cs b/app/MindWork AI Studio/Components/TextInfoLine.razor.cs
new file mode 100644
index 00000000..206fae4b
--- /dev/null
+++ b/app/MindWork AI Studio/Components/TextInfoLine.razor.cs
@@ -0,0 +1,51 @@
+using AIStudio.Settings;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components;
+
+public partial class TextInfoLine : ComponentBase
+{
+ [Parameter]
+ public string Label { get; set; } = string.Empty;
+
+ [Parameter]
+ public string Icon { get; set; } = Icons.Material.Filled.Info;
+
+ [Parameter]
+ public string Value { get; set; } = string.Empty;
+
+ [Parameter]
+ public string ClipboardTooltipSubject { get; set; } = "the text";
+
+ [Parameter]
+ public bool ShowingCopyButton { get; set; } = true;
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ [Inject]
+ private ISnackbar Snackbar { get; init; } = null!;
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Configure the spellchecking for the user input:
+ this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
+
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private static readonly Dictionary USER_INPUT_ATTRIBUTES = new();
+
+ private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard";
+
+ private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/TextInfoLines.razor b/app/MindWork AI Studio/Components/TextInfoLines.razor
new file mode 100644
index 00000000..68186316
--- /dev/null
+++ b/app/MindWork AI Studio/Components/TextInfoLines.razor
@@ -0,0 +1,21 @@
+
+
+
+ @if (this.ShowingCopyButton)
+ {
+
+
+
+ }
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/TextInfoLines.razor.cs b/app/MindWork AI Studio/Components/TextInfoLines.razor.cs
new file mode 100644
index 00000000..2427133c
--- /dev/null
+++ b/app/MindWork AI Studio/Components/TextInfoLines.razor.cs
@@ -0,0 +1,63 @@
+using AIStudio.Settings;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components;
+
+public partial class TextInfoLines : ComponentBase
+{
+ [Parameter]
+ public string Label { get; set; } = string.Empty;
+
+ [Parameter]
+ public string Value { get; set; } = string.Empty;
+
+ [Parameter]
+ public string ClipboardTooltipSubject { get; set; } = "the text";
+
+ [Parameter]
+ public int MaxLines { get; set; } = 30;
+
+ [Parameter]
+ public bool ShowingCopyButton { get; set; } = true;
+
+ [Parameter]
+ public TextColor Color { get; set; } = TextColor.DEFAULT;
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ [Inject]
+ private ISnackbar Snackbar { get; init; } = null!;
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Configure the spellchecking for the user input:
+ this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
+
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private static readonly Dictionary USER_INPUT_ATTRIBUTES = new();
+
+ private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard";
+
+ private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
+
+ private string GetColor()
+ {
+ var htmlColorCode = this.Color.GetHTMLColor(this.SettingsManager);
+ if(string.IsNullOrWhiteSpace(htmlColorCode))
+ return string.Empty;
+
+ return $"color: {htmlColorCode} !important;";
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Vision.razor.cs b/app/MindWork AI Studio/Components/Vision.razor.cs
index 9277c00a..2f7f2659 100644
--- a/app/MindWork AI Studio/Components/Vision.razor.cs
+++ b/app/MindWork AI Studio/Components/Vision.razor.cs
@@ -7,10 +7,14 @@ public partial class Vision : ComponentBase
private static readonly TextItem[] ITEMS_VISION =
[
new("Meet your needs", "Whatever your job or task is, MindWork AI Studio aims to meet your needs: whether you're a project manager, scientist, artist, author, software developer, or game developer."),
- new("One stop shop", "The app will strive to fulfill all your AI needs: text-generation AI (LLM), image-generation AI, audio-generation AI (text-to-speech, potentially even text-to-music), and audio input (transcription, dictation). When there's a provider and an API available, we'll try to integrate it."),
- new("Local file system", "When you wish, we aim to integrate your local system. Local documents could be incorporated using Retrieval-Augmented Generation (RAG), and we could directly save AI-generated content to your file system."),
- new("Local AI systems", "Want to use AI systems locally and offline? We aim to make that possible too."),
- new("Your AI systems", "Prefer to run your AI systems with providers like replicate.com? We plan to support that!"),
- new("Assistants", "We aim to integrate specialized user interfaces as assistants. For example, a UI specifically for writing emails, or one designed for translating and correcting text, and more."),
+ new("Integrating your data", "You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes."),
+ new("Integration of enterprise data", "It will soon be possible to integrate data from the corporate network using a specified interface (External Retrieval Interface, ERI for short). This will likely require development work by the organization in question."),
+ new("Useful assistants", "We'll develop more assistants for everyday tasks."),
+ new("Writing mode", "We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel."),
+ new("Specific requirements", "Want an assistant that suits your specific needs? We aim to offer a plugin architecture so organizations and enthusiasts can implement such ideas."),
+ new("Voice control", "You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation."),
+ new("Content creation", "There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers."),
+ new("Email monitoring", "You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats."),
+ new("Browser usage", "We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser."),
];
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs
index 1ec3c0e1..9b6b0bb6 100644
--- a/app/MindWork AI Studio/Components/Workspaces.razor.cs
+++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs
@@ -13,9 +13,6 @@ namespace AIStudio.Components;
public partial class Workspaces : ComponentBase
{
- [Inject]
- private SettingsManager SettingsManager { get; init; } = null!;
-
[Inject]
private IDialogService DialogService { get; init; } = null!;
diff --git a/app/MindWork AI Studio/Dialogs/ConfirmDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ConfirmDialog.razor.cs
index 05fd6be9..78fb9ad3 100644
--- a/app/MindWork AI Studio/Dialogs/ConfirmDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ConfirmDialog.razor.cs
@@ -8,7 +8,7 @@ namespace AIStudio.Dialogs;
public partial class ConfirmDialog : ComponentBase
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public string Message { get; set; } = string.Empty;
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor
new file mode 100644
index 00000000..dd6d2773
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor
@@ -0,0 +1,101 @@
+@using AIStudio.Settings.DataModel
+@using AIStudio.Tools.ERIClient.DataModel
+
+
+
+
+
+ Common data source information
+
+
+
+
+
+
+ @if (!this.IsConnectionEncrypted())
+ {
+
+ Please note: the connection to the ERI v1 server is not encrypted. This means that all
+ data sent to the server is transmitted in plain text. Please ask the ERI server administrator
+ to enable encryption.
+
+ }
+
+ @if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
+ {
+
+ }
+
+
+
+
+
+
+ Retrieval information
+
+ @if (!this.retrievalInfoformation.Any())
+ {
+
+ The data source does not provide any retrieval information.
+
+ }
+ else
+ {
+
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(this.selectedRetrievalInfo.Link))
+ {
+
+ Open web link, show more information
+
+ }
+
+
+ Embeddings
+
+ @* ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract *@
+ @if (this.selectedRetrievalInfo.Embeddings is null || !this.selectedRetrievalInfo.Embeddings.Any())
+ {
+
+ The data source does not provide any embedding information.
+
+ }
+ else
+ {
+
+ @for (var embeddingIndex = 0; embeddingIndex < this.selectedRetrievalInfo.Embeddings.Count; embeddingIndex++)
+ {
+ var embedding = this.selectedRetrievalInfo.Embeddings[embeddingIndex];
+
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(embedding.Link))
+ {
+
+ Open web link, show more information
+
+ }
+
+ }
+
+ }
+
+
+ }
+
+
+
+
+ @if (this.IsOperationInProgress)
+ {
+
+ }
+ Reload
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor.cs
new file mode 100644
index 00000000..2594ee58
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceERI-V1InfoDialog.razor.cs
@@ -0,0 +1,180 @@
+// ReSharper disable InconsistentNaming
+
+using System.Text;
+
+using AIStudio.Assistants.ERI;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.ERIClient;
+using AIStudio.Tools.ERIClient.DataModel;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+
+using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
+
+namespace AIStudio.Dialogs;
+
+public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposable, ISecretId
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public DataSourceERI_V1 DataSource { get; set; }
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.eriServerTasks.Add(this.GetERIMetadata());
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private readonly CancellationTokenSource cts = new();
+ private readonly List eriServerTasks = new();
+ private readonly List dataIssues = [];
+
+ private string serverDescription = string.Empty;
+ private ProviderType securityRequirements = ProviderType.NONE;
+ private IReadOnlyList retrievalInfoformation = [];
+ private RetrievalInfo selectedRetrievalInfo;
+
+ private bool IsOperationInProgress { get; set; } = true;
+
+ private bool IsConnectionEncrypted() => this.DataSource.Hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
+
+ private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
+
+ private string RetrievalName(RetrievalInfo retrievalInfo)
+ {
+ var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
+ var hasName = !string.IsNullOrWhiteSpace(retrievalInfo.Name);
+
+ if (hasId && hasName)
+ return $"[{retrievalInfo.Id}] {retrievalInfo.Name}";
+
+ if (hasId)
+ return $"[{retrievalInfo.Id}] Unnamed retrieval process";
+
+ return hasName ? retrievalInfo.Name : "Unnamed retrieval process";
+ }
+
+ private string RetrievalParameters(RetrievalInfo retrievalInfo)
+ {
+ var parameters = retrievalInfo.ParametersDescription;
+ if (parameters is null || parameters.Count == 0)
+ return "This retrieval process has no parameters.";
+
+ var sb = new StringBuilder();
+ foreach (var (paramName, description) in parameters)
+ {
+ sb.Append("Parameter: ");
+ sb.AppendLine(paramName);
+ sb.AppendLine(description);
+ sb.AppendLine();
+ }
+
+ return sb.ToString();
+ }
+
+ private async Task GetERIMetadata()
+ {
+ this.dataIssues.Clear();
+
+ try
+ {
+ this.IsOperationInProgress = true;
+ this.StateHasChanged();
+
+ using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
+ if(client is null)
+ {
+ this.dataIssues.Add("Failed to connect to the ERI v1 server. The server is not supported.");
+ return;
+ }
+
+ var loginResult = await client.AuthenticateAsync(this.RustService);
+ if (!loginResult.Successful)
+ {
+ this.dataIssues.Add(loginResult.Message);
+ return;
+ }
+
+ var dataSourceInfo = await client.GetDataSourceInfoAsync(this.cts.Token);
+ if (!dataSourceInfo.Successful)
+ {
+ this.dataIssues.Add(dataSourceInfo.Message);
+ return;
+ }
+
+ this.serverDescription = dataSourceInfo.Data.Description;
+
+ var securityRequirementsResult = await client.GetSecurityRequirementsAsync(this.cts.Token);
+ if (!securityRequirementsResult.Successful)
+ {
+ this.dataIssues.Add(securityRequirementsResult.Message);
+ return;
+ }
+
+ this.securityRequirements = securityRequirementsResult.Data.AllowedProviderType;
+
+ var retrievalInfoResult = await client.GetRetrievalInfoAsync(this.cts.Token);
+ if (!retrievalInfoResult.Successful)
+ {
+ this.dataIssues.Add(retrievalInfoResult.Message);
+ return;
+ }
+
+ this.retrievalInfoformation = retrievalInfoResult.Data ?? [];
+ this.selectedRetrievalInfo = this.retrievalInfoformation.FirstOrDefault(x => x.Id == this.DataSource.SelectedRetrievalId);
+ this.StateHasChanged();
+ }
+ catch (Exception e)
+ {
+ this.dataIssues.Add($"Failed to connect to the ERI v1 server. The message was: {e.Message}");
+ }
+ finally
+ {
+ this.IsOperationInProgress = false;
+ this.StateHasChanged();
+ }
+ }
+
+ private void Close()
+ {
+ this.cts.Cancel();
+ this.MudDialog.Close();
+ }
+
+ #region Implementation of ISecretId
+
+ public string SecretId => this.DataSource.Id;
+
+ public string SecretName => this.DataSource.Name;
+
+ #endregion
+
+ #region Implementation of IDisposable
+
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await this.cts.CancelAsync();
+ await Task.WhenAll(this.eriServerTasks);
+
+ this.cts.Dispose();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
new file mode 100644
index 00000000..a37a2d4d
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
@@ -0,0 +1,150 @@
+@using AIStudio.Settings.DataModel
+@using AIStudio.Tools.ERIClient.DataModel
+
+
+
+ @* ReSharper disable once CSharpWarnings::CS8974 *@
+
+
+
+ @* ReSharper disable once CSharpWarnings::CS8974 *@
+
+
+
+
+
+ @if (!this.IsConnectionEncrypted())
+ {
+
+ Please note: the connection to the ERI v1 server is not encrypted. This means that all
+ data sent to the server is transmitted in plain text. Please ask the ERI server administrator
+ to enable encryption.
+
+ }
+
+ @if (this.IsConnectionPossible())
+ {
+
+
+ Test connection & read available metadata
+
+
+ @this.GetTestResultText()
+
+
+ }
+
+ @if(this.availableAuthMethods.Count > 0 || this.dataAuthMethod != default)
+ {
+
+ @foreach (var authMethod in this.availableAuthMethods)
+ {
+ @authMethod.DisplayName()
+ }
+
+ }
+
+ @if (this.NeedsSecret())
+ {
+ if (this.dataAuthMethod is AuthMethod.USERNAME_PASSWORD)
+ {
+ @* ReSharper disable once CSharpWarnings::CS8974 *@
+
+ }
+
+ @* ReSharper disable once CSharpWarnings::CS8974 *@
+
+ }
+
+ @if (this.availableRetrievalProcesses.Count > 0)
+ {
+
+ @foreach (var retrievalProcess in this.availableRetrievalProcesses)
+ {
+
+ @retrievalProcess.Name
+
+ }
+
+ }
+
+
+ @foreach (var policy in Enum.GetValues())
+ {
+
+ @policy.ToSelectionText()
+
+ }
+
+
+
+
+
+
+ Cancel
+
+ @if(this.IsEditing)
+ {
+ @:Update
+ }
+ else
+ {
+ @:Add
+ }
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
new file mode 100644
index 00000000..e3547d9d
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
@@ -0,0 +1,333 @@
+using AIStudio.Assistants.ERI;
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.ERIClient;
+using AIStudio.Tools.ERIClient.DataModel;
+using AIStudio.Tools.Services;
+using AIStudio.Tools.Validation;
+
+using Microsoft.AspNetCore.Components;
+
+using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
+
+// ReSharper disable InconsistentNaming
+namespace AIStudio.Dialogs;
+
+public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public bool IsEditing { get; set; }
+
+ [Parameter]
+ public DataSourceERI_V1 DataSource { get; set; }
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ [Inject]
+ private ILogger Logger { get; init; } = null!;
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new();
+
+ private readonly DataSourceValidation dataSourceValidation;
+ private readonly Encryption encryption = Program.ENCRYPTION;
+
+ ///
+ /// The list of used data source names. We need this to check for uniqueness.
+ ///
+ private List UsedDataSourcesNames { get; set; } = [];
+
+ private bool dataIsValid;
+ private string[] dataIssues = [];
+ private string dataSecretStorageIssue = string.Empty;
+ private string dataEditingPreviousInstanceName = string.Empty;
+ private List availableAuthMethods = [];
+ private DataSourceSecurity dataSecurityPolicy;
+ private SecurityRequirements dataSourceSecurityRequirements;
+ private bool connectionTested;
+ private bool connectionSuccessfulTested;
+
+ private uint dataNum;
+ private string dataSecret = string.Empty;
+ private string dataId = Guid.NewGuid().ToString();
+ private string dataName = string.Empty;
+ private string dataHostname = string.Empty;
+ private int dataPort;
+ private AuthMethod dataAuthMethod;
+ private string dataUsername = string.Empty;
+ private List availableRetrievalProcesses = [];
+ private RetrievalInfo dataSelectedRetrievalProcess;
+
+ // We get the form reference from Blazor code to validate it manually:
+ private MudForm form = null!;
+
+ public DataSourceERI_V1Dialog()
+ {
+ this.dataSourceValidation = new()
+ {
+ GetAuthMethod = () => this.dataAuthMethod,
+ GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName,
+ GetUsedDataSourceNames = () => this.UsedDataSourcesNames,
+ GetSecretStorageIssue = () => this.dataSecretStorageIssue,
+ GetTestedConnection = () => this.connectionTested,
+ GetTestedConnectionResult = () => this.connectionSuccessfulTested,
+ GetAvailableAuthMethods = () => this.availableAuthMethods,
+ GetSecurityRequirements = () => this.dataSourceSecurityRequirements,
+ };
+ }
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Configure the spellchecking for the instance name input:
+ this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
+
+ // Load the used instance names:
+ this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList();
+
+ // When editing, we need to load the data:
+ if(this.IsEditing)
+ {
+ //
+ // Assign the data to the form fields:
+ //
+ this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
+ this.dataNum = this.DataSource.Num;
+ this.dataId = this.DataSource.Id;
+ this.dataName = this.DataSource.Name;
+ this.dataHostname = this.DataSource.Hostname;
+ this.dataPort = this.DataSource.Port;
+ this.dataAuthMethod = this.DataSource.AuthMethod;
+ this.dataUsername = this.DataSource.Username;
+ this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
+
+ if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
+ {
+ // Load the secret:
+ var requestedSecret = await this.RustService.GetSecret(this);
+ if (requestedSecret.Success)
+ this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
+ else
+ {
+ this.dataSecret = string.Empty;
+ this.dataSecretStorageIssue = $"Failed to load the auth. secret from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the secret again.";
+ await this.form.Validate();
+ }
+ }
+
+ // Load the data:
+ await this.TestConnection();
+
+ // Select the retrieval process:
+ this.dataSelectedRetrievalProcess = this.availableRetrievalProcesses.FirstOrDefault(n => n.Id == this.DataSource.SelectedRetrievalId);
+ }
+
+ await base.OnInitializedAsync();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ // Reset the validation when not editing and on the first render.
+ // We don't want to show validation errors when the user opens the dialog.
+ if(!this.IsEditing && firstRender)
+ this.form.ResetValidation();
+
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ #endregion
+
+ #region Implementation of ISecretId
+
+ public string SecretId => this.dataId;
+
+ public string SecretName => this.dataName;
+
+ #endregion
+
+ private DataSourceERI_V1 CreateDataSource()
+ {
+ var cleanedHostname = this.dataHostname.Trim();
+ return new DataSourceERI_V1
+ {
+ Id = this.dataId,
+ Num = this.dataNum,
+ Port = this.dataPort,
+ Name = this.dataName,
+ Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
+ AuthMethod = this.dataAuthMethod,
+ Username = this.dataUsername,
+ Type = DataSourceType.ERI_V1,
+ SecurityPolicy = this.dataSecurityPolicy,
+ SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
+ };
+ }
+
+ private bool IsConnectionEncrypted() => this.dataHostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
+
+ private bool IsConnectionPossible()
+ {
+ if(this.dataSourceValidation.ValidatingHostname(this.dataHostname) is not null)
+ return false;
+
+ if(this.dataSourceValidation.ValidatePort(this.dataPort) is not null)
+ return false;
+
+ return true;
+ }
+
+ private async Task TestConnection()
+ {
+ try
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(14));
+ this.DataSource = this.CreateDataSource();
+ using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
+ if(client is null)
+ {
+ await this.form.Validate();
+
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server is not supported.";
+ return;
+ }
+
+ var authSchemes = await client.GetAuthMethodsAsync(cts.Token);
+ if (!authSchemes.Successful)
+ {
+ await this.form.Validate();
+
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = authSchemes.Message;
+ return;
+ }
+
+ this.availableAuthMethods = authSchemes.Data!.Select(n => n.AuthMethod).ToList();
+
+ var loginResult = await client.AuthenticateAsync(this.RustService, this.dataSecret, cts.Token);
+ if (!loginResult.Successful)
+ {
+ await this.form.Validate();
+
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = loginResult.Message;
+ return;
+ }
+
+ var securityRequirementsRequest = await client.GetSecurityRequirementsAsync(cts.Token);
+ if (!securityRequirementsRequest.Successful)
+ {
+ await this.form.Validate();
+
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = securityRequirementsRequest.Message;
+ return;
+ }
+
+ this.dataSourceSecurityRequirements = securityRequirementsRequest.Data;
+
+ var retrievalInfoRequest = await client.GetRetrievalInfoAsync(cts.Token);
+ if (!retrievalInfoRequest.Successful)
+ {
+ await this.form.Validate();
+
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = retrievalInfoRequest.Message;
+ return;
+ }
+
+ this.availableRetrievalProcesses = retrievalInfoRequest.Data ?? [];
+
+ this.connectionTested = true;
+ this.connectionSuccessfulTested = true;
+ this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");
+ }
+ catch (Exception e)
+ {
+ await this.form.Validate();
+
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = $"Failed to connect to the ERI v1 server. The message was: {e.Message}";
+ this.Logger.LogError($"Failed to connect to the ERI v1 server. Message: {e.Message}");
+
+ this.connectionTested = true;
+ this.connectionSuccessfulTested = false;
+ }
+ }
+
+ private string GetTestResultText()
+ {
+ if(!this.connectionTested)
+ return "Not tested yet.";
+
+ return this.connectionSuccessfulTested ? "Connection successful." : "Connection failed.";
+ }
+
+ private Color GetTestResultColor()
+ {
+ if (!this.connectionTested)
+ return Color.Default;
+
+ return this.connectionSuccessfulTested ? Color.Success : Color.Error;
+ }
+
+ private string GetTestResultIcon()
+ {
+ if (!this.connectionTested)
+ return Icons.Material.Outlined.HourglassEmpty;
+
+ return this.connectionSuccessfulTested ? Icons.Material.Outlined.CheckCircle : Icons.Material.Outlined.Error;
+ }
+
+ private bool NeedsSecret() => this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD;
+
+ private string GetSecretLabel() => this.dataAuthMethod switch
+ {
+ AuthMethod.TOKEN => "Access Token",
+ AuthMethod.USERNAME_PASSWORD => "Password",
+ _ => "Secret",
+ };
+
+ private async Task Store()
+ {
+ await this.form.Validate();
+
+ var testConnectionValidation = this.dataSourceValidation.ValidateTestedConnection();
+ if(testConnectionValidation is not null)
+ {
+ Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
+ this.dataIssues[^1] = testConnectionValidation;
+ this.dataIsValid = false;
+ }
+
+ this.dataSecretStorageIssue = string.Empty;
+
+ // When the data is not valid, we don't store it:
+ if (!this.dataIsValid)
+ return;
+
+ var addedDataSource = this.CreateDataSource();
+ if (!string.IsNullOrWhiteSpace(this.dataSecret))
+ {
+ // Store the secret in the OS secure storage:
+ var storeResponse = await this.RustService.SetSecret(this, this.dataSecret);
+ if (!storeResponse.Success)
+ {
+ this.dataSecretStorageIssue = $"Failed to store the auth. secret in the operating system. The message was: {storeResponse.Issue}. Please try again.";
+ await this.form.Validate();
+ return;
+ }
+ }
+
+ this.MudDialog.Close(DialogResult.Ok(addedDataSource));
+ }
+
+ private void Cancel() => this.MudDialog.Cancel();
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
new file mode 100644
index 00000000..e7863285
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
@@ -0,0 +1,88 @@
+@using AIStudio.Settings.DataModel
+
+
+
+
+ @* ReSharper disable once CSharpWarnings::CS8974 *@
+
+
+
+ Select a root directory for this data source. All data in this directory and all
+ its subdirectories will be processed for this data source.
+
+
+
+
+ In order for the AI to be able to determine the appropriate data at any time, you must
+ choose an embedding method.
+
+
+ @foreach (var embedding in this.AvailableEmbeddings)
+ {
+ @embedding.Name
+ }
+
+
+ @if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
+ {
+ if (this.SelectedCloudEmbedding)
+ {
+
+ @if (string.IsNullOrWhiteSpace(this.dataPath))
+ {
+ @: Please note: the embedding you selected runs in the cloud. All your data will be sent to the cloud.
+ @: Please confirm that you have read and understood this.
+ }
+ else
+ {
+ @: Please note: the embedding you selected runs in the cloud. All your data from the
+ @: folder '@this.dataPath' and all its subdirectories will be sent to the cloud. Please
+ @: confirm that you have read and understood this.
+ }
+
+
+ }
+ else
+ {
+
+ The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
+
+ }
+ }
+
+
+ @foreach (var policy in Enum.GetValues())
+ {
+ @policy.ToSelectionText()
+ }
+
+
+
+
+
+ Cancel
+
+ @if(this.IsEditing)
+ {
+ @:Update
+ }
+ else
+ {
+ @:Add
+ }
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs
new file mode 100644
index 00000000..16c2e677
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs
@@ -0,0 +1,123 @@
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.Validation;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Dialogs;
+
+public partial class DataSourceLocalDirectoryDialog : ComponentBase
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public bool IsEditing { get; set; }
+
+ [Parameter]
+ public DataSourceLocalDirectory DataSource { get; set; }
+
+ [Parameter]
+ public IReadOnlyList> AvailableEmbeddings { get; set; } = [];
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new();
+
+ private readonly DataSourceValidation dataSourceValidation;
+
+ ///
+ /// The list of used data source names. We need this to check for uniqueness.
+ ///
+ private List UsedDataSourcesNames { get; set; } = [];
+
+ private bool dataIsValid;
+ private string[] dataIssues = [];
+ private string dataEditingPreviousInstanceName = string.Empty;
+
+ private uint dataNum;
+ private string dataId = Guid.NewGuid().ToString();
+ private string dataName = string.Empty;
+ private bool dataUserAcknowledgedCloudEmbedding;
+ private string dataEmbeddingId = string.Empty;
+ private string dataPath = string.Empty;
+ private DataSourceSecurity dataSecurityPolicy;
+
+ // We get the form reference from Blazor code to validate it manually:
+ private MudForm form = null!;
+
+ public DataSourceLocalDirectoryDialog()
+ {
+ this.dataSourceValidation = new()
+ {
+ GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding,
+ GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName,
+ GetUsedDataSourceNames = () => this.UsedDataSourcesNames,
+ };
+ }
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Configure the spellchecking for the instance name input:
+ this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
+
+ // Load the used instance names:
+ this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList();
+
+ // When editing, we need to load the data:
+ if(this.IsEditing)
+ {
+ this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
+ this.dataNum = this.DataSource.Num;
+ this.dataId = this.DataSource.Id;
+ this.dataName = this.DataSource.Name;
+ this.dataEmbeddingId = this.DataSource.EmbeddingId;
+ this.dataPath = this.DataSource.Path;
+ this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
+ }
+
+ await base.OnInitializedAsync();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ // Reset the validation when not editing and on the first render.
+ // We don't want to show validation errors when the user opens the dialog.
+ if(!this.IsEditing && firstRender)
+ this.form.ResetValidation();
+
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ #endregion
+
+ private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId).IsSelfHosted;
+
+ private DataSourceLocalDirectory CreateDataSource() => new()
+ {
+ Id = this.dataId,
+ Num = this.dataNum,
+ Name = this.dataName,
+ Type = DataSourceType.LOCAL_DIRECTORY,
+ EmbeddingId = this.dataEmbeddingId,
+ Path = this.dataPath,
+ SecurityPolicy = this.dataSecurityPolicy,
+ };
+
+ private async Task Store()
+ {
+ await this.form.Validate();
+
+ // When the data is not valid, we don't store it:
+ if (!this.dataIsValid)
+ return;
+
+ var addedDataSource = this.CreateDataSource();
+ this.MudDialog.Close(DialogResult.Ok(addedDataSource));
+ }
+
+ private void Cancel() => this.MudDialog.Cancel();
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor
new file mode 100644
index 00000000..a4b647b0
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor
@@ -0,0 +1,56 @@
+@using AIStudio.Settings.DataModel
+
+
+
+
+
+
+ @if (!this.IsDirectoryAvailable)
+ {
+
+ The directory chosen for the data source does not exist anymore. Please edit the data source and correct the path.
+
+ }
+ else
+ {
+
+ The directory chosen for the data source exists.
+
+ }
+
+
+ @if (this.IsCloudEmbedding)
+ {
+
+ The embedding runs in the cloud. All your data from the folder '@this.DataSource.Path' and all its subdirectories
+ will be sent to the cloud.
+
+ }
+ else
+ {
+
+ The embedding runs locally or in your organization. Your data is not sent to the cloud.
+
+ }
+
+
+
+
+
+ @if (this.directorySizeNumFiles > 100)
+ {
+
+ For performance reasons, only the first 100 files are shown. The directory contains @this.NumberFilesInDirectory files in total.
+
+ }
+
+
+
+
+ @if (this.IsOperationInProgress)
+ {
+
+ }
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs
new file mode 100644
index 00000000..af8c0df8
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs
@@ -0,0 +1,112 @@
+using System.Text;
+
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+
+using Microsoft.AspNetCore.Components;
+
+using Timer = System.Timers.Timer;
+
+namespace AIStudio.Dialogs;
+
+public partial class DataSourceLocalDirectoryInfoDialog : ComponentBase, IAsyncDisposable
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public DataSourceLocalDirectory DataSource { get; set; }
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ private readonly Timer refreshTimer = new(TimeSpan.FromSeconds(1.6))
+ {
+ AutoReset = true,
+ };
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId);
+ this.directoryInfo = new DirectoryInfo(this.DataSource.Path);
+
+ if (this.directoryInfo.Exists)
+ {
+ this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token);
+ this.refreshTimer.Elapsed += (_, _) => this.InvokeAsync(this.StateHasChanged);
+ this.refreshTimer.Start();
+ }
+
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private const int MAX_FILES_TO_SHOW = 100;
+
+ private readonly CancellationTokenSource cts = new();
+
+ private EmbeddingProvider embeddingProvider;
+ private DirectoryInfo directoryInfo = null!;
+ private long directorySizeBytes;
+ private long directorySizeNumFiles;
+ private readonly StringBuilder directoryFiles = new();
+ private Task directorySizeTask = Task.CompletedTask;
+
+ private bool IsOperationInProgress { get; set; } = true;
+
+ private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
+
+ private bool IsDirectoryAvailable => this.directoryInfo.Exists;
+
+ private void UpdateFileList(string file)
+ {
+ this.directoryFiles.Append("- ");
+ this.directoryFiles.AppendLine(file);
+ }
+
+ private void UpdateDirectorySize(long size)
+ {
+ this.directorySizeBytes = size;
+ }
+
+ private void UpdateDirectoryFiles(long numFiles) => this.directorySizeNumFiles = numFiles;
+
+ private void DirectoryOperationDone()
+ {
+ this.refreshTimer.Stop();
+ this.IsOperationInProgress = false;
+ this.InvokeAsync(this.StateHasChanged);
+ }
+
+ private string NumberFilesInDirectory => $"{this.directorySizeNumFiles:###,###,###,###}";
+
+ private void Close()
+ {
+ this.cts.Cancel();
+ this.MudDialog.Close();
+ }
+
+ #region Implementation of IDisposable
+
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await this.cts.CancelAsync();
+ await this.directorySizeTask;
+
+ this.cts.Dispose();
+ this.refreshTimer.Stop();
+ this.refreshTimer.Dispose();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
new file mode 100644
index 00000000..ebd0a5bc
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
@@ -0,0 +1,86 @@
+@using AIStudio.Settings.DataModel
+
+
+
+ @* ReSharper disable once CSharpWarnings::CS8974 *@
+
+
+
+ Select a file for this data source. The content of this file will be processed for the data source.
+
+
+
+
+ In order for the AI to be able to determine the appropriate data at any time, you must
+ choose an embedding method.
+
+
+ @foreach (var embedding in this.AvailableEmbeddings)
+ {
+ @embedding.Name
+ }
+
+
+ @if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
+ {
+ if (this.SelectedCloudEmbedding)
+ {
+
+ @if (string.IsNullOrWhiteSpace(this.dataFilePath))
+ {
+ @: Please note: the embedding you selected runs in the cloud. All your data will be sent to the cloud.
+ @: Please confirm that you have read and understood this.
+ }
+ else
+ {
+ @: Please note: the embedding you selected runs in the cloud. All your data within the
+ @: file '@this.dataFilePath' will be sent to the cloud. Please confirm that you have read
+ @: and understood this.
+ }
+
+
+ }
+ else
+ {
+
+ The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
+
+ }
+ }
+
+
+ @foreach (var policy in Enum.GetValues())
+ {
+ @policy.ToSelectionText()
+ }
+
+
+
+
+
+ Cancel
+
+ @if(this.IsEditing)
+ {
+ @:Update
+ }
+ else
+ {
+ @:Add
+ }
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs
new file mode 100644
index 00000000..902899fe
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs
@@ -0,0 +1,123 @@
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.Validation;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Dialogs;
+
+public partial class DataSourceLocalFileDialog : ComponentBase
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public bool IsEditing { get; set; }
+
+ [Parameter]
+ public DataSourceLocalFile DataSource { get; set; }
+
+ [Parameter]
+ public IReadOnlyList> AvailableEmbeddings { get; set; } = [];
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new();
+
+ private readonly DataSourceValidation dataSourceValidation;
+
+ ///
+ /// The list of used data source names. We need this to check for uniqueness.
+ ///
+ private List UsedDataSourcesNames { get; set; } = [];
+
+ private bool dataIsValid;
+ private string[] dataIssues = [];
+ private string dataEditingPreviousInstanceName = string.Empty;
+
+ private uint dataNum;
+ private string dataId = Guid.NewGuid().ToString();
+ private string dataName = string.Empty;
+ private bool dataUserAcknowledgedCloudEmbedding;
+ private string dataEmbeddingId = string.Empty;
+ private string dataFilePath = string.Empty;
+ private DataSourceSecurity dataSecurityPolicy;
+
+ // We get the form reference from Blazor code to validate it manually:
+ private MudForm form = null!;
+
+ public DataSourceLocalFileDialog()
+ {
+ this.dataSourceValidation = new()
+ {
+ GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding,
+ GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName,
+ GetUsedDataSourceNames = () => this.UsedDataSourcesNames,
+ };
+ }
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Configure the spellchecking for the instance name input:
+ this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
+
+ // Load the used instance names:
+ this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList();
+
+ // When editing, we need to load the data:
+ if(this.IsEditing)
+ {
+ this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
+ this.dataNum = this.DataSource.Num;
+ this.dataId = this.DataSource.Id;
+ this.dataName = this.DataSource.Name;
+ this.dataEmbeddingId = this.DataSource.EmbeddingId;
+ this.dataFilePath = this.DataSource.FilePath;
+ this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
+ }
+
+ await base.OnInitializedAsync();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ // Reset the validation when not editing and on the first render.
+ // We don't want to show validation errors when the user opens the dialog.
+ if(!this.IsEditing && firstRender)
+ this.form.ResetValidation();
+
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ #endregion
+
+ private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId).IsSelfHosted;
+
+ private DataSourceLocalFile CreateDataSource() => new()
+ {
+ Id = this.dataId,
+ Num = this.dataNum,
+ Name = this.dataName,
+ Type = DataSourceType.LOCAL_FILE,
+ EmbeddingId = this.dataEmbeddingId,
+ FilePath = this.dataFilePath,
+ SecurityPolicy = this.dataSecurityPolicy,
+ };
+
+ private async Task Store()
+ {
+ await this.form.Validate();
+
+ // When the data is not valid, we don't store it:
+ if (!this.dataIsValid)
+ return;
+
+ var addedDataSource = this.CreateDataSource();
+ this.MudDialog.Close(DialogResult.Ok(addedDataSource));
+ }
+
+ private void Cancel() => this.MudDialog.Cancel();
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor
new file mode 100644
index 00000000..0605ff93
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor
@@ -0,0 +1,42 @@
+@using AIStudio.Settings.DataModel
+
+
+
+
+
+
+ @if (!this.IsFileAvailable)
+ {
+
+ The file chosen for the data source does not exist anymore. Please edit the data source and choose another file or correct the path.
+
+ }
+ else
+ {
+
+ The file chosen for the data source exists.
+
+ }
+
+
+ @if (this.IsCloudEmbedding)
+ {
+
+ The embedding runs in the cloud. All your data within the
+ file '@this.DataSource.FilePath' will be sent to the cloud.
+
+ }
+ else
+ {
+
+ The embedding runs locally or in your organization. Your data is not sent to the cloud.
+
+ }
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs
new file mode 100644
index 00000000..7dc204c1
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs
@@ -0,0 +1,40 @@
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Dialogs;
+
+public partial class DataSourceLocalFileInfoDialog : ComponentBase
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public DataSourceLocalFile DataSource { get; set; }
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId);
+ this.fileInfo = new FileInfo(this.DataSource.FilePath);
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private EmbeddingProvider embeddingProvider;
+ private FileInfo fileInfo = null!;
+
+ private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
+
+ private bool IsFileAvailable => this.fileInfo.Exists;
+
+ private string FileSize => this.fileInfo.FileSize();
+
+ private void Close() => this.MudDialog.Close();
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs
index c2733fbc..36ee9417 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs
@@ -8,7 +8,7 @@ namespace AIStudio.Dialogs;
public partial class EmbeddingMethodDialog : ComponentBase
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
///
/// The user chosen embedding name.
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
index 3a7b092b..1d09fa52 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
@@ -1,5 +1,6 @@
using AIStudio.Provider;
using AIStudio.Settings;
+using AIStudio.Tools.Services;
using AIStudio.Tools.Validation;
using Microsoft.AspNetCore.Components;
@@ -11,7 +12,7 @@ namespace AIStudio.Dialogs;
public partial class EmbeddingProviderDialog : ComponentBase, ISecretId
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
///
/// The embedding's number in the list.
@@ -112,13 +113,24 @@ public partial class EmbeddingProviderDialog : ComponentBase, ISecretId
private EmbeddingProvider CreateEmbeddingProviderSettings()
{
var cleanedHostname = this.DataHostname.Trim();
+ Model model = default;
+ if(this.DataLLMProvider is LLMProviders.SELF_HOSTED)
+ {
+ if (this.DataHost is Host.OLLAMA)
+ model = new Model(this.dataManuallyModel, null);
+ else if (this.DataHost is Host.LM_STUDIO)
+ model = this.DataModel;
+ }
+ else
+ model = this.DataModel;
+
return new()
{
Num = this.DataNum,
Id = this.DataId,
Name = this.DataName,
UsedLLMProvider = this.DataLLMProvider,
- Model = this.DataLLMProvider is LLMProviders.SELF_HOSTED ? new Model(this.dataManuallyModel, null) : this.DataModel,
+ Model = model,
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
@@ -197,8 +209,7 @@ public partial class EmbeddingProviderDialog : ComponentBase, ISecretId
private async Task Store()
{
await this.form.Validate();
- if (!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue))
- this.dataAPIKeyStorageIssue = string.Empty;
+ this.dataAPIKeyStorageIssue = string.Empty;
// When the data is not valid, we don't store it:
if (!this.dataIsValid)
diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs
index 7391c1d5..28b9b4b1 100644
--- a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs
@@ -7,7 +7,7 @@ namespace AIStudio.Dialogs;
public partial class ProfileDialog : ComponentBase
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
///
/// The profile's number in the list.
diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
index 87e5569e..f1a71739 100644
--- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
@@ -1,11 +1,11 @@
using AIStudio.Provider;
using AIStudio.Settings;
+using AIStudio.Tools.Services;
using AIStudio.Tools.Validation;
using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host;
-using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Dialogs;
@@ -15,7 +15,7 @@ namespace AIStudio.Dialogs;
public partial class ProviderDialog : ComponentBase, ISecretId
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
///
/// The provider's number in the list.
@@ -137,7 +137,9 @@ public partial class ProviderDialog : ComponentBase, 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
// When editing, we need to load the data:
if(this.IsEditing)
diff --git a/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs
index df01a7c1..99b5d9f8 100644
--- a/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs
@@ -8,7 +8,7 @@ namespace AIStudio.Dialogs;
public partial class RetrievalProcessDialog : ComponentBase
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
///
/// The user chosen retrieval process name.
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor
new file mode 100644
index 00000000..cfdbdd47
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor
@@ -0,0 +1,43 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Agenda Planner Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor.cs
new file mode 100644
index 00000000..261d6b31
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogAgenda : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor
new file mode 100644
index 00000000..1aef4b4e
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor
@@ -0,0 +1,40 @@
+@using AIStudio.Settings
+@using AIStudio.Settings.DataModel
+@inherits SettingsDialogBase
+
+
+
+
+ Assistant: Bias of the Day Options
+
+
+
+
+
+
+
+
+ You have learned about @this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias.Count out of @BiasCatalog.ALL_BIAS.Count biases.
+
+
+ Reset
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelAssistantBias.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor.cs
similarity index 84%
rename from app/MindWork AI Studio/Components/Settings/SettingsPanelAssistantBias.razor.cs
rename to app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor.cs
index 375d644b..75767049 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelAssistantBias.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor.cs
@@ -1,10 +1,6 @@
-using AIStudio.Dialogs;
+namespace AIStudio.Dialogs.Settings;
-using DialogOptions = AIStudio.Dialogs.DialogOptions;
-
-namespace AIStudio.Components.Settings;
-
-public partial class SettingsPanelAssistantBias : SettingsPanelBase
+public partial class SettingsDialogAssistantBias : SettingsDialogBase
{
private async Task ResetBiasOfTheDayHistory()
{
@@ -24,4 +20,5 @@ public partial class SettingsPanelAssistantBias : SettingsPanelBase
await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
}
+
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs
new file mode 100644
index 00000000..b1568427
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs
@@ -0,0 +1,94 @@
+using System.Diagnostics.CodeAnalysis;
+
+using AIStudio.Settings;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Dialogs.Settings;
+
+public abstract class SettingsDialogBase : ComponentBase, IMessageBusReceiver, IDisposable
+{
+ [CascadingParameter]
+ protected IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Inject]
+ protected SettingsManager SettingsManager { get; init; } = null!;
+
+ [Inject]
+ protected IDialogService DialogService { get; init; } = null!;
+
+ [Inject]
+ protected MessageBus MessageBus { get; init; } = null!;
+
+ [Inject]
+ protected RustService RustService { get; init; } = null!;
+
+ protected readonly List> availableLLMProviders = new();
+ protected readonly List> availableEmbeddingProviders = new();
+
+ #region Overrides of ComponentBase
+
+ ///
+ protected override async Task OnInitializedAsync()
+ {
+ // Register this component with the message bus:
+ this.MessageBus.RegisterComponent(this);
+ this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
+
+ this.UpdateProviders();
+ this.UpdateEmbeddingProviders();
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ 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)
+ this.availableLLMProviders.Add(new (provider.InstanceName, provider.Id));
+ }
+
+ private void UpdateEmbeddingProviders()
+ {
+ this.availableEmbeddingProviders.Clear();
+ foreach (var provider in this.SettingsManager.ConfigurationData.EmbeddingProviders)
+ this.availableEmbeddingProviders.Add(new (provider.Name, provider.Id));
+ }
+
+ #region Implementation of IMessageBusReceiver
+
+ public string ComponentName => nameof(Settings);
+
+ public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
+ {
+ switch (triggeredEvent)
+ {
+ case Event.CONFIGURATION_CHANGED:
+ this.StateHasChanged();
+ break;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
+ {
+ return Task.FromResult(default);
+ }
+
+ #endregion
+
+ #region Implementation of IDisposable
+
+ public void Dispose()
+ {
+ this.MessageBus.Unregister(this);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor
new file mode 100644
index 00000000..c5a09136
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor
@@ -0,0 +1,29 @@
+@using AIStudio.Assistants.Coding
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Coding Options
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.Coding.PreselectedProgrammingLanguage is CommonCodingLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor.cs
new file mode 100644
index 00000000..f914bd72
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogCoding : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor
new file mode 100644
index 00000000..17c05fc2
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor
@@ -0,0 +1,68 @@
+@using AIStudio.Settings.DataModel
+@inherits SettingsDialogBase
+
+
+
+
+
+
+ Configured Data Sources
+
+
+
+
+ You might configure different data sources. A data source can include one file, all files
+ in a directory, or data from your company. Later, you can incorporate these data sources
+ as needed when the AI requires this data to complete a certain task.
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Name
+ Type
+ Embedding
+ Actions
+
+
+ @context.Num
+ @context.Name
+ @context.Type.GetDisplayName()
+ @this.GetEmbeddingName(context)
+
+
+
+
+
+ Edit
+
+
+ Delete
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
+ {
+ No data sources configured yet.
+ }
+
+
+ External Data (ERI-Server v1)
+ Local Directory
+ Local File
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs
new file mode 100644
index 00000000..d2c6cef8
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs
@@ -0,0 +1,223 @@
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.ERIClient.DataModel;
+
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogDataSources : SettingsDialogBase
+{
+ private string GetEmbeddingName(IDataSource dataSource)
+ {
+ if(dataSource is IInternalDataSource internalDataSource)
+ {
+ var matchedEmbedding = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == internalDataSource.EmbeddingId);
+ if(matchedEmbedding == default)
+ return "No valid embedding";
+
+ return matchedEmbedding.Name;
+ }
+
+ if(dataSource is IExternalDataSource)
+ return "External (ERI)";
+
+ return "Unknown";
+ }
+
+ private async Task AddDataSource(DataSourceType type)
+ {
+ IDataSource? addedDataSource = null;
+ switch (type)
+ {
+ case DataSourceType.LOCAL_FILE:
+ var localFileDialogParameters = new DialogParameters
+ {
+ { x => x.IsEditing, false },
+ { x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
+ };
+
+ var localFileDialogReference = await this.DialogService.ShowAsync("Add Local File as Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN);
+ var localFileDialogResult = await localFileDialogReference.Result;
+ if (localFileDialogResult is null || localFileDialogResult.Canceled)
+ return;
+
+ var localFile = (DataSourceLocalFile)localFileDialogResult.Data!;
+ localFile = localFile with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
+ addedDataSource = localFile;
+ break;
+
+ case DataSourceType.LOCAL_DIRECTORY:
+ var localDirectoryDialogParameters = new DialogParameters
+ {
+ { x => x.IsEditing, false },
+ { x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
+ };
+
+ var localDirectoryDialogReference = await this.DialogService.ShowAsync("Add Local Directory as Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
+ var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
+ if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
+ return;
+
+ var localDirectory = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
+ localDirectory = localDirectory with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
+ addedDataSource = localDirectory;
+ break;
+
+ case DataSourceType.ERI_V1:
+ var eriDialogParameters = new DialogParameters
+ {
+ { x => x.IsEditing, false },
+ };
+
+ var eriDialogReference = await this.DialogService.ShowAsync("Add ERI v1 Data Source", eriDialogParameters, DialogOptions.FULLSCREEN);
+ var eriDialogResult = await eriDialogReference.Result;
+ if (eriDialogResult is null || eriDialogResult.Canceled)
+ return;
+
+ var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
+ eriDataSource = eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
+ addedDataSource = eriDataSource;
+ break;
+ }
+
+ if(addedDataSource is null)
+ return;
+
+ this.SettingsManager.ConfigurationData.DataSources.Add(addedDataSource);
+ await this.SettingsManager.StoreSettings();
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ }
+
+ private async Task EditDataSource(IDataSource dataSource)
+ {
+ IDataSource? editedDataSource = null;
+ switch (dataSource)
+ {
+ case DataSourceLocalFile localFile:
+ var localFileDialogParameters = new DialogParameters
+ {
+ { x => x.IsEditing, true },
+ { x => x.DataSource, localFile },
+ { x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
+ };
+
+ var localFileDialogReference = await this.DialogService.ShowAsync("Edit Local File Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN);
+ var localFileDialogResult = await localFileDialogReference.Result;
+ if (localFileDialogResult is null || localFileDialogResult.Canceled)
+ return;
+
+ editedDataSource = (DataSourceLocalFile)localFileDialogResult.Data!;
+ break;
+
+ case DataSourceLocalDirectory localDirectory:
+ var localDirectoryDialogParameters = new DialogParameters
+ {
+ { x => x.IsEditing, true },
+ { x => x.DataSource, localDirectory },
+ { x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
+ };
+
+ var localDirectoryDialogReference = await this.DialogService.ShowAsync("Edit Local Directory Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
+ var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
+ if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
+ return;
+
+ editedDataSource = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
+ break;
+
+ case DataSourceERI_V1 eriDataSource:
+ var eriDialogParameters = new DialogParameters
+ {
+ { x => x.IsEditing, true },
+ { x => x.DataSource, eriDataSource },
+ };
+
+ var eriDialogReference = await this.DialogService.ShowAsync("Edit ERI v1 Data Source", eriDialogParameters, DialogOptions.FULLSCREEN);
+ var eriDialogResult = await eriDialogReference.Result;
+ if (eriDialogResult is null || eriDialogResult.Canceled)
+ return;
+
+ editedDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
+ break;
+ }
+
+ if(editedDataSource is null)
+ return;
+
+ this.SettingsManager.ConfigurationData.DataSources[this.SettingsManager.ConfigurationData.DataSources.IndexOf(dataSource)] = editedDataSource;
+
+ await this.SettingsManager.StoreSettings();
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ }
+
+ private async Task DeleteDataSource(IDataSource dataSource)
+ {
+ var dialogParameters = new DialogParameters
+ {
+ { "Message", $"Are you sure you want to delete the data source '{dataSource.Name}' of type {dataSource.Type.GetDisplayName()}?" },
+ };
+
+ var dialogReference = await this.DialogService.ShowAsync("Delete Data Source", dialogParameters, DialogOptions.FULLSCREEN);
+ var dialogResult = await dialogReference.Result;
+ if (dialogResult is null || dialogResult.Canceled)
+ return;
+
+ var applyChanges = dataSource is IInternalDataSource;
+
+ // External data sources may need a secret for authentication:
+ if (dataSource is IExternalDataSource externalDataSource)
+ {
+ // When the auth method is NONE or KERBEROS, we don't need to delete a secret.
+ // In the case of KERBEROS, we don't store the Kerberos ticket in the secret store.
+ if(dataSource is IERIDataSource { AuthMethod: AuthMethod.NONE or AuthMethod.KERBEROS })
+ applyChanges = true;
+
+ // All other auth methods require a secret, which we need to delete now:
+ else
+ {
+ var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource);
+ if (deleteSecretResponse.Success)
+ applyChanges = true;
+ }
+ }
+
+ if(applyChanges)
+ {
+ this.SettingsManager.ConfigurationData.DataSources.Remove(dataSource);
+ await this.SettingsManager.StoreSettings();
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ }
+ }
+
+ private async Task ShowInformation(IDataSource dataSource)
+ {
+ switch (dataSource)
+ {
+ case DataSourceLocalFile localFile:
+ var localFileDialogParameters = new DialogParameters
+ {
+ { x => x.DataSource, localFile },
+ };
+
+ await this.DialogService.ShowAsync("Local File Data Source Information", localFileDialogParameters, DialogOptions.FULLSCREEN);
+ break;
+
+ case DataSourceLocalDirectory localDirectory:
+ var localDirectoryDialogParameters = new DialogParameters
+ {
+ { x => x.DataSource, localDirectory },
+ };
+
+ await this.DialogService.ShowAsync("Local Directory Data Source Information", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
+ break;
+
+ case DataSourceERI_V1 eriV1DataSource:
+ var eriV1DialogParameters = new DialogParameters
+ {
+ { x => x.DataSource, eriV1DataSource },
+ };
+
+ await this.DialogService.ShowAsync("ERI v1 Data Source Information", eriV1DialogParameters, DialogOptions.FULLSCREEN);
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor
new file mode 100644
index 00000000..7ce8cc3b
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor
@@ -0,0 +1,27 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+
+ Assistant: ERI Server Options
+
+
+
+
+
+
+
+
+
+ Most ERI server options can be customized and saved directly in the ERI server assistant.
+ For this, the ERI server assistant has an auto-save function.
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor.cs
new file mode 100644
index 00000000..d2f36c8a
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogERIServer : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogGrammarSpelling.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogGrammarSpelling.razor
new file mode 100644
index 00000000..c7ae29dd
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogGrammarSpelling.razor
@@ -0,0 +1,26 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Grammar & Spelling Checker Options
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogGrammarSpelling.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogGrammarSpelling.razor.cs
new file mode 100644
index 00000000..e2a0da0c
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogGrammarSpelling.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogGrammarSpelling : SettingsDialogBase;
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogIconFinder.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogIconFinder.razor
new file mode 100644
index 00000000..0562b38e
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogIconFinder.razor
@@ -0,0 +1,22 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Icon Finder Options
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogIconFinder.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogIconFinder.razor.cs
new file mode 100644
index 00000000..708efdd0
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogIconFinder.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogIconFinder : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogJobPostings.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogJobPostings.razor
new file mode 100644
index 00000000..505d5624
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogJobPostings.razor
@@ -0,0 +1,33 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Job Posting Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.JobPostings.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogJobPostings.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogJobPostings.razor.cs
new file mode 100644
index 00000000..328adc2f
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogJobPostings.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogJobPostings : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor
new file mode 100644
index 00000000..10b2c286
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor
@@ -0,0 +1,24 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+ Assistant: Legal Check Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor.cs
new file mode 100644
index 00000000..9139c228
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogLegalCheck : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor
new file mode 100644
index 00000000..91d318af
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor
@@ -0,0 +1,27 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: My Tasks Options
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.MyTasks.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor.cs
new file mode 100644
index 00000000..d18c9860
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogMyTasks : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogRewrite.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogRewrite.razor
new file mode 100644
index 00000000..e6bcf79f
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogRewrite.razor
@@ -0,0 +1,28 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Rewrite & Improve Text Options
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogRewrite.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogRewrite.razor.cs
new file mode 100644
index 00000000..12a74394
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogRewrite.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogRewrite : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSynonyms.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSynonyms.razor
new file mode 100644
index 00000000..2d7e29f4
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSynonyms.razor
@@ -0,0 +1,26 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Synonyms Options
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSynonyms.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSynonyms.razor.cs
new file mode 100644
index 00000000..729e02c0
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSynonyms.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogSynonyms : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTextSummarizer.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTextSummarizer.razor
new file mode 100644
index 00000000..111fda45
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTextSummarizer.razor
@@ -0,0 +1,35 @@
+@using AIStudio.Assistants.TextSummarizer
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Text Summarizer Options
+
+
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+ @if(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity is Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS)
+ {
+
+ }
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTextSummarizer.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTextSummarizer.razor.cs
new file mode 100644
index 00000000..de7bb910
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTextSummarizer.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogTextSummarizer : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTranslation.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTranslation.razor
new file mode 100644
index 00000000..8cb87ac7
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTranslation.razor
@@ -0,0 +1,30 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+ Assistant: Translator Options
+
+
+
+
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.Translation.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTranslation.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTranslation.razor.cs
new file mode 100644
index 00000000..1dc31ea8
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogTranslation.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogTranslation : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor
new file mode 100644
index 00000000..a6f4bca9
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor
@@ -0,0 +1,30 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ Assistant: Writing E-Mails Options
+
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+
+ Close
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor.cs
new file mode 100644
index 00000000..11c04324
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogWritingEMails : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs b/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs
index 5b0e8e88..914b20e4 100644
--- a/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs
@@ -7,7 +7,7 @@ namespace AIStudio.Dialogs;
public partial class SingleInputDialog : ComponentBase
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public string Message { get; set; } = string.Empty;
diff --git a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs
index aefd5518..555cb182 100644
--- a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs
@@ -15,7 +15,7 @@ public partial class UpdateDialog : ComponentBase
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute()!;
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public UpdateResponse UpdateResponse { get; set; }
diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs
index f123fc87..f2c3d090 100644
--- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs
@@ -9,7 +9,7 @@ namespace AIStudio.Dialogs;
public partial class WorkspaceSelectionDialog : ComponentBase
{
[CascadingParameter]
- private MudDialogInstance MudDialog { get; set; } = null!;
+ private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public string Message { get; set; } = string.Empty;
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index e18afe1d..4c45a3b9 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -1,6 +1,7 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
@@ -8,7 +9,6 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
-using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Layout;
@@ -82,6 +82,23 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
// Ensure that all settings are loaded:
await this.SettingsManager.LoadSettings();
+ //
+ // We cannot process the plugins before the settings are loaded,
+ // and we know our data directory.
+ //
+ if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
+ {
+ // Ensure that all internal plugins are present:
+ await PluginFactory.EnsureInternalPlugins();
+
+ // Load (but not start) all plugins, without waiting for them:
+ var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+ _ = PluginFactory.LoadAll(pluginLoadingTimeout.Token);
+
+ // Set up hot reloading for plugins:
+ PluginFactory.SetUpHotReloading();
+ }
+
// Register this component with the message bus:
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED ]);
@@ -103,39 +120,15 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
private void LoadNavItems()
{
- var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
- var isWriterModePreviewEnabled = PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager);
- if (!isWriterModePreviewEnabled)
- {
- this.navItems = new List
- {
- new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
- new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
- new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
- new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
- new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
- new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),
- };
- }
- else
- {
- this.navItems = new List
- {
- new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
- new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
- new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
- new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false),
- new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
- new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
- new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),
- };
- }
+ this.navItems = new List(this.GetNavItems());
}
#endregion
#region Implementation of IMessageBusReceiver
+ public string ComponentName => nameof(MainLayout);
+
public async Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
@@ -180,6 +173,25 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
}
#endregion
+
+ private IEnumerable GetNavItems()
+ {
+ var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
+
+ yield return new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true);
+ yield return new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
+ yield return new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false);
+
+ if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))
+ yield return new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false);
+
+ if (PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
+ yield return new("Plugins", Icons.Material.TwoTone.Extension, palette.DarkLighten, palette.GrayLight, Routes.PLUGINS, false);
+
+ yield return new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false);
+ yield return new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false);
+ yield return new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false);
+ }
private async Task DismissUpdate()
{
diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj
index 0d6fc288..90069618 100644
--- a/app/MindWork AI Studio/MindWork AI Studio.csproj
+++ b/app/MindWork AI Studio/MindWork AI Studio.csproj
@@ -5,7 +5,7 @@
Thorsten Sommer
- net8.0
+ net9.0
enable
CS8600;CS8602;CS8603
enable
@@ -31,6 +31,7 @@
CS8974: Converting method group to non-delegate type; Did you intend to invoke the method? We have this issue with MudBlazor validation methods.
-->
IL2026, CS8974
+ latest
@@ -40,17 +41,22 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor
index 07f87804..282e5e72 100644
--- a/app/MindWork AI Studio/Pages/About.razor
+++ b/app/MindWork AI Studio/Pages/About.razor
@@ -1,64 +1,122 @@
@attribute [Route(Routes.ABOUT)]
-About MindWork AI Studio
+
+ About MindWork AI Studio
-
-
-
-
- The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
-
-
-
-
-
-
-
-
-
-
-
- Check for updates
-
-
-
-
-
-
+
+
+
+
+ The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
+
+
+
+
+
+
+
+
+
+
+
+ Check for updates
+
+
-
-
-
+
+
+
+ Discover MindWork AI's mission and vision on our official homepage.
+
+
+ Browse AI Studio's source code on GitHub — we welcome your contributions.
+
+
+ Connect AI Studio to your organization's data with our External Retrieval Interface (ERI).
+
+
+ View our project roadmap and help shape AI Studio's future development.
+
+
+ Did you find a bug or are you experiencing issues? Report your concern here.
+
+
+ Have feature ideas? Submit suggestions for future AI Studio enhancements.
+
+
+
+
+
+
+
+
+
+
+ Explanation
+
+
+ AI Studio creates a log file at startup, in which events during startup are recorded. After startup,
+ another log file is created that records all events that occur during the use of the app. This
+ includes any errors that may occur. Depending on when an error occurs (at startup or during use),
+ the contents of these log files can be helpful for troubleshooting. Sensitive information such as
+ passwords is not included in the log files.
+
+
+
+ By clicking on the respective path, the path is copied to the clipboard. You might open these files
+ with a text editor to view their contents.
+
+
+
+ Startup log file
+
+
+
+
+
+
+ Usage log file
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs
index 100ebdbe..51727506 100644
--- a/app/MindWork AI Studio/Pages/About.razor.cs
+++ b/app/MindWork AI Studio/Pages/About.razor.cs
@@ -1,5 +1,8 @@
using System.Reflection;
+using AIStudio.Tools.Rust;
+using AIStudio.Tools.Services;
+
using Microsoft.AspNetCore.Components;
namespace AIStudio.Pages;
@@ -9,9 +12,15 @@ public partial class About : ComponentBase
[Inject]
private MessageBus MessageBus { get; init; } = null!;
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ [Inject]
+ private ISnackbar Snackbar { get; init; } = null!;
+
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute()!;
-
+
private static string VersionDotnetRuntime => $"Used .NET runtime: v{META_DATA.DotnetVersion}";
private static string VersionDotnetSdk => $"Used .NET SDK: v{META_DATA.DotnetSdkVersion}";
@@ -26,6 +35,28 @@ public partial class About : ComponentBase
private static string TauriVersion => $"Tauri: v{META_DATA.TauriVersion}";
+ private GetLogPathsResponse logPaths;
+
+ #region Overrides of ComponentBase
+
+ private async Task CopyStartupLogPath()
+ {
+ await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
+ }
+
+ private async Task CopyAppLogPath()
+ {
+ await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogAppPath);
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.logPaths = await this.RustService.GetLogPaths();
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
private const string LICENSE = """
# Functional Source License, Version 1.1, MIT Future License
diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor
index 212f8b5a..58c0deb1 100644
--- a/app/MindWork AI Studio/Pages/Assistants.razor
+++ b/app/MindWork AI Studio/Pages/Assistants.razor
@@ -1,51 +1,54 @@
+@using AIStudio.Dialogs.Settings
@using AIStudio.Settings.DataModel
@attribute [Route(Routes.ASSISTANTS)]
-
- Assistants
-
+
+
+ Assistants
+
-
-
-
- General
-
-
-
-
-
-
-
-
+
+
+
+ General
+
+
+
+
+
+
+
+
-
- Business
-
-
-
-
-
-
-
-
-
+
+ Business
+
+
+
+
+
+
+
+
+
-
- Learning
-
-
-
-
-
-
- Software Engineering
-
-
-
- @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
- {
-
- }
-
-
-
\ No newline at end of file
+
+ Learning
+
+
+
+
+
+
+ Software Engineering
+
+
+
+ @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
+ {
+
+ }
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor
index 2f56c2b5..b77edf18 100644
--- a/app/MindWork AI Studio/Pages/Chat.razor
+++ b/app/MindWork AI Studio/Pages/Chat.razor
@@ -2,109 +2,100 @@
@using AIStudio.Settings.DataModel
@inherits MSGComponentBase
-
- @if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
- {
- @($"Chat in Workspace \"{this.currentWorkspaceName}\"")
- }
- else
- {
- @("Temporary Chat")
- }
-
+
+
+
+ @if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
+ {
+ @($"Chat in Workspace \"{this.currentWorkspaceName}\"")
+ }
+ else
+ {
+ @("Temporary Chat")
+ }
+
-
-@if (this.AreWorkspacesVisible)
-{
-
-
- @if (this.AreWorkspacesHidden)
- {
-
-
-
-
-
- }
- @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
- {
- @if ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE)
+
+ @if (this.AreWorkspacesVisible)
+ {
+
+
+ @if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
{
- @if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
- {
-
-
-
-
-
-
-
-
-
-
- }
- else
- {
-
-
-
-
-
- }
+ // Case: Sidebar can be toggled and is currently visible
+
+
+
+
+
+
+
+
+
+
}
- }
-
-
-
+ else
+ {
+ // Case: Sidebar is always visible
+
+
+
+
+
+ }
+
+
+
+
+
+ }
+ else if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR)
+ {
+ // Case: Sidebar can be toggled and is currently hidden
+
+
+
+
+
+
+
-
-
-
-}
-else if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR)
-{
-
-
-
-
-
-
-
+ WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
+
+ }
+ else
+ {
+ // Case: Workspaces are disabled or shown in an overlay
-
-}
-else
-{
-
-}
+ WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
+ }
-@if (
- this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
- && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
-{
-
-
-
-
- Your workspaces
-
-
-
-
-
-
-
-
-}
\ No newline at end of file
+ @if (
+ this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
+ && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
+ {
+
+
+
+
+ Your workspaces
+
+
+
+
+
+
+
+
+ }
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs
index 8546b64d..3ee7ecc9 100644
--- a/app/MindWork AI Studio/Pages/Chat.razor.cs
+++ b/app/MindWork AI Studio/Pages/Chat.razor.cs
@@ -28,6 +28,8 @@ public partial class Chat : MSGComponentBase
protected override async Task OnInitializedAsync()
{
+ this.ApplyFilters([], [ Event.WORKSPACE_TOGGLE_OVERLAY ]);
+
this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition;
this.splitterSaveTimer.AutoReset = false;
this.splitterSaveTimer.Elapsed += async (_, _) =>
@@ -71,9 +73,17 @@ public partial class Chat : MSGComponentBase
}
private double ReadSplitterPosition => this.AreWorkspacesHidden ? 6 : this.splitterPosition;
+
+ private void UpdateWorkspaceName(string workspaceName)
+ {
+ this.currentWorkspaceName = workspaceName;
+ this.StateHasChanged();
+ }
#region Overrides of MSGComponentBase
+ public override string ComponentName => nameof(Chat);
+
public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor
index a86b8372..de74a626 100644
--- a/app/MindWork AI Studio/Pages/Home.razor
+++ b/app/MindWork AI Studio/Pages/Home.razor
@@ -1,40 +1,42 @@
@attribute [Route(Routes.HOME)]
-
-Let's get started
+
+
+ Let's get started
-
-
+
+
-
-
- Welcome to MindWork AI Studio!
-
-
- Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness
- the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated
- LLM. Instead, you will need to bring an API key from a suitable provider.
-
-
- Here's what makes MindWork AI Studio stand out:
-
-
-
- We hope you enjoy using MindWork AI Studio to bring your AI projects to life!
-
-
+
+
+ Welcome to MindWork AI Studio!
+
+
+ Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness
+ the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated
+ LLM. Instead, you will need to bring an API key from a suitable provider.
+
+
+ Here's what makes MindWork AI Studio stand out:
+
+
+
+ We hope you enjoy using MindWork AI Studio to bring your AI projects to life!
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs
index 924767f1..026fafe5 100644
--- a/app/MindWork AI Studio/Pages/Home.razor.cs
+++ b/app/MindWork AI Studio/Pages/Home.razor.cs
@@ -26,17 +26,18 @@ public partial class Home : ComponentBase
private async Task ReadLastChangeAsync()
{
var latest = Changelog.LOGS.MaxBy(n => n.Build);
- var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}");
+ using var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}");
this.LastChangeContent = await response.Content.ReadAsStringAsync();
}
private static readonly TextItem[] ITEMS_ADVANTAGES =
[
new("Free of charge", "The app is free to use, both for personal and commercial purposes."),
- new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."),
+ new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."),
+ new("Assistants", "You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants."),
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
new("Cost-effective", "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."),
- new("Privacy", "The data entered into the app is not used for training by the providers since we are using the provider's API."),
+ new("Privacy", "You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems."),
new("Flexibility", "Choose the provider and model best suited for your current task."),
new("No bloatware", "The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life."),
];
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor
new file mode 100644
index 00000000..6d5bf8ce
--- /dev/null
+++ b/app/MindWork AI Studio/Pages/Plugins.razor
@@ -0,0 +1,77 @@
+@using AIStudio.Tools.PluginSystem
+@attribute [Route(Routes.PLUGINS)]
+
+
+
+ Plugins
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugins
+ Actions
+
+
+
+ @switch (context.Key)
+ {
+ case GROUP_ENABLED:
+
+ Enabled Plugins
+
+ break;
+
+ case GROUP_DISABLED:
+
+ Disabled Plugins
+
+ break;
+
+ case GROUP_INTERNAL:
+
+ Internal Plugins
+
+ break;
+ }
+
+
+
+
+
+
+ @((MarkupString)context.IconSVG)
+
+
+
+
+
+
+ @context.Name
+
+
+ @context.Description
+
+
+
+
+ @if (!context.IsInternal)
+ {
+ var isEnabled = this.SettingsManager.IsPluginEnabled(context);
+
+
+
+ }
+
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs
new file mode 100644
index 00000000..9504c0ff
--- /dev/null
+++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs
@@ -0,0 +1,82 @@
+using AIStudio.Settings;
+using AIStudio.Tools.PluginSystem;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Pages;
+
+public partial class Plugins : ComponentBase, IMessageBusReceiver
+{
+ private const string GROUP_ENABLED = "Enabled";
+ private const string GROUP_DISABLED = "Disabled";
+ private const string GROUP_INTERNAL = "Internal";
+
+ [Inject]
+ private MessageBus MessageBus { get; init; } = null!;
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ private TableGroupDefinition groupConfig = null!;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.MessageBus.RegisterComponent(this);
+ this.MessageBus.ApplyFilters(this, [], [ Event.PLUGINS_RELOADED ]);
+
+ this.groupConfig = new TableGroupDefinition
+ {
+ Expandable = true,
+ IsInitiallyExpanded = true,
+ Selector = pluginMeta =>
+ {
+ if (pluginMeta.IsInternal)
+ return GROUP_INTERNAL;
+
+ return this.SettingsManager.IsPluginEnabled(pluginMeta)
+ ? GROUP_ENABLED
+ : GROUP_DISABLED;
+ }
+ };
+
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
+ {
+ if (this.SettingsManager.IsPluginEnabled(pluginMeta))
+ this.SettingsManager.ConfigurationData.EnabledPlugins.Remove(pluginMeta.Id);
+ else
+ this.SettingsManager.ConfigurationData.EnabledPlugins.Add(pluginMeta.Id);
+
+ await this.SettingsManager.StoreSettings();
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ }
+
+ #region Implementation of IMessageBusReceiver
+
+ public string ComponentName => nameof(Plugins);
+
+ public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
+ {
+ switch (triggeredEvent)
+ {
+ case Event.PLUGINS_RELOADED:
+ this.InvokeAsync(this.StateHasChanged);
+ break;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
+ {
+ return Task.FromResult(default);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor
index 4a004734..c048a542 100644
--- a/app/MindWork AI Studio/Pages/Settings.razor
+++ b/app/MindWork AI Studio/Pages/Settings.razor
@@ -1,30 +1,31 @@
@attribute [Route(Routes.SETTINGS)]
@using AIStudio.Components.Settings
+@using AIStudio.Settings.DataModel
-Settings
+
+ Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+ @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
+ {
+
+ }
+
+
+
+
+
+
+ @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
+ {
+
+
+ }
+
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs
index d06ab02e..57c34c5a 100644
--- a/app/MindWork AI Studio/Pages/Settings.razor.cs
+++ b/app/MindWork AI Studio/Pages/Settings.razor.cs
@@ -6,6 +6,9 @@ namespace AIStudio.Pages;
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
{
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
[Inject]
private MessageBus MessageBus { get; init; } = null!;
@@ -27,6 +30,8 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
#region Implementation of IMessageBusReceiver
+ public string ComponentName => nameof(Settings);
+
public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{
switch (triggeredEvent)
diff --git a/app/MindWork AI Studio/Pages/Supporters.razor b/app/MindWork AI Studio/Pages/Supporters.razor
index 6793d6d9..91a744a3 100644
--- a/app/MindWork AI Studio/Pages/Supporters.razor
+++ b/app/MindWork AI Studio/Pages/Supporters.razor
@@ -1,63 +1,103 @@
@attribute [Route(Routes.SUPPORTERS)]
-Supporters
+
+
Supporters
+
-
-
-
- Our Titans
-
- In this section, we highlight the titan supporters of MindWork AI Studio. Titans are prestigious companies that provide significant support to our mission.
-
-
- For companies, sponsoring MindWork AI Studio is not only a way to support innovation but also a valuable opportunity for public relations and marketing. Your company's name and logo will be featured prominently, showcasing your commitment to using cutting-edge AI tools and enhancing your reputation as an innovative enterprise.
-
-
- Become our first Titan
-
-
-
-
-
-
-
-
- Individual Contributors
-
+
+
+
+
+ Our Titans
+
+ In this section, we highlight the titan supporters of MindWork AI Studio. Titans are prestigious companies that provide significant support to our mission.
+
+
+ For companies, sponsoring MindWork AI Studio is not only a way to support innovation but also a valuable opportunity for public relations and marketing. Your company's name and logo will be featured prominently, showcasing your commitment to using cutting-edge AI tools and enhancing your reputation as an innovative enterprise.
+
+
+ Become our first Titan
+
+
-
- Become a contributor
-
-
-
- The first 10 supporters who make a monthly contribution:
-
-
-
-
-
+
+
+
+
+
+ Individual Contributors
+
-
- The first 10 supporters who make a one-time contribution:
-
-
-
-
-
-
+
+ Become a contributor
+
-
-
-
-
- Business Contributors
-
+
+ The first 10 supporters who make a monthly contribution:
+
+
+
+
+
+
-
- Become a contributor
-
-
-
-
+
+ The first 10 supporters who make a one-time contribution:
+
+
+
+
+
+
-
\ No newline at end of file
+
+
+
+
+ Business Contributors
+
+
+
+ Become a contributor
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Code Contributions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Moderation, Design, Wiki, and Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Writer.razor b/app/MindWork AI Studio/Pages/Writer.razor
index bf65c3e7..790e3e07 100644
--- a/app/MindWork AI Studio/Pages/Writer.razor
+++ b/app/MindWork AI Studio/Pages/Writer.razor
@@ -1,57 +1,58 @@
@attribute [Route(Routes.WRITER)]
@inherits MSGComponentBase
-
- Writer
-
+
+
+ Writer
+
+
-
+
+
+
+
-
-
-
-
-
-
-
-
- @if (this.isStreaming)
- {
-
- }
-
-
-
\ No newline at end of file
+
+
+
+ @if (this.isStreaming)
+ {
+
+ }
+
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Writer.razor.cs b/app/MindWork AI Studio/Pages/Writer.razor.cs
index 7b6a8cf5..42b23abc 100644
--- a/app/MindWork AI Studio/Pages/Writer.razor.cs
+++ b/app/MindWork AI Studio/Pages/Writer.razor.cs
@@ -41,6 +41,8 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
#region Overrides of MSGComponentBase
+ public override string ComponentName => nameof(Writer);
+
public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
return Task.CompletedTask;
@@ -104,17 +106,19 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
};
var time = DateTimeOffset.Now;
+ var lastUserPrompt = new ContentText
+ {
+ // We use the maximum 160 characters from the end of the text:
+ Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
+ };
+
this.chatThread.Blocks.Clear();
this.chatThread.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
- Content = new ContentText
- {
- // We use the maximum 160 characters from the end of the text:
- Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
- },
+ Content = lastUserPrompt,
});
var aiText = new ContentText
@@ -135,7 +139,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
this.isStreaming = true;
this.StateHasChanged();
- await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
+ this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
this.suggestion = aiText.Text;
this.isStreaming = false;
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/contentHome.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/contentHome.lua
new file mode 100644
index 00000000..f26d9aff
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/contentHome.lua
@@ -0,0 +1,3 @@
+CONTENT_HOME = {
+ LetsGetStarted = "Lass uns anfangen",
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua
new file mode 100644
index 00000000..c10dd294
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua
@@ -0,0 +1,11 @@
+SVG = [[
+
+
+
+
+
+
+
+
+
+ ]]
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
new file mode 100644
index 00000000..ab05f0bc
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -0,0 +1,45 @@
+require("contentHome")
+require("icon")
+
+-- The ID for this plugin:
+ID = "43065dbc-78d0-45b7-92be-f14c2926e2dc"
+
+-- The icon for the plugin:
+ICON_SVG = SVG
+
+-- The name of the plugin:
+NAME = "MindWork AI Studio - German / Deutsch"
+
+-- The description of the plugin:
+DESCRIPTION = "Dieses Plugin bietet deutsche Sprachunterstützung für MindWork AI Studio."
+
+-- The version of the plugin:
+VERSION = "1.0.0"
+
+-- The type of the plugin:
+TYPE = "LANGUAGE"
+
+-- The authors of the plugin:
+AUTHORS = {"MindWork AI Community"}
+
+-- The support contact for the plugin:
+SUPPORT_CONTACT = "MindWork AI Community"
+
+-- The source URL for the plugin:
+SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
+
+-- The categories for the plugin:
+CATEGORIES = { "CORE" }
+
+-- The target groups for the plugin:
+TARGET_GROUPS = { "EVERYONE" }
+
+-- The flag for whether the plugin is maintained:
+IS_MAINTAINED = true
+
+-- When the plugin is deprecated, this message will be shown to users:
+DEPRECATION_MESSAGE = ""
+
+UI_TEXT_CONTENT = {
+ HOME = CONTENT_HOME,
+}
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/contentHome.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/contentHome.lua
new file mode 100644
index 00000000..d1805fd0
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/contentHome.lua
@@ -0,0 +1,3 @@
+CONTENT_HOME = {
+ LetsGetStarted = "Let's get started",
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua
new file mode 100644
index 00000000..75320473
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua
@@ -0,0 +1,12 @@
+SVG = [[
+
+
+
+
+
+
+
+
+
+
+ ]]
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
new file mode 100644
index 00000000..d6c98d49
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -0,0 +1,45 @@
+require("contentHome")
+require("icon")
+
+-- The ID for this plugin:
+ID = "97dfb1ba-50c4-4440-8dfa-6575daf543c8"
+
+-- The icon for the plugin:
+ICON_SVG = SVG
+
+-- The name of the plugin:
+NAME = "MindWork AI Studio - US English"
+
+-- The description of the plugin:
+DESCRIPTION = "This plugin provides US English language support for MindWork AI Studio."
+
+-- The version of the plugin:
+VERSION = "1.0.0"
+
+-- The type of the plugin:
+TYPE = "LANGUAGE"
+
+-- The authors of the plugin:
+AUTHORS = {"MindWork AI Community"}
+
+-- The support contact for the plugin:
+SUPPORT_CONTACT = "MindWork AI Community"
+
+-- The source URL for the plugin:
+SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
+
+-- The categories for the plugin:
+CATEGORIES = { "CORE" }
+
+-- The target groups for the plugin:
+TARGET_GROUPS = { "EVERYONE" }
+
+-- The flag for whether the plugin is maintained:
+IS_MAINTAINED = true
+
+-- When the plugin is deprecated, this message will be shown to users:
+DEPRECATION_MESSAGE = ""
+
+UI_TEXT_CONTENT = {
+ HOME = CONTENT_HOME,
+}
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index 0844cae7..8283a481 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -1,5 +1,6 @@
using AIStudio.Agents;
using AIStudio.Settings;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -21,6 +22,8 @@ internal sealed class Program
public static RustService RUST_SERVICE = null!;
public static Encryption ENCRYPTION = null!;
public static string API_TOKEN = null!;
+ public static IServiceProvider SERVICE_PROVIDER = null!;
+ public static ILoggerFactory LOGGER_FACTORY = null!;
public static async Task Main(string[] args)
{
@@ -115,7 +118,10 @@ internal sealed class Program
builder.Services.AddMudMarkdownClipboardService();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
@@ -143,12 +149,25 @@ internal sealed class Program
// Execute the builder to get the app:
var app = builder.Build();
+ // Get the logging factory for e.g., static classes:
+ LOGGER_FACTORY = app.Services.GetRequiredService();
+
+ // Get a program logger:
+ var programLogger = app.Services.GetRequiredService>();
+ programLogger.LogInformation("Starting the AI Studio server.");
+
+ // Store the service provider (DI). We need it later for some classes,
+ // which are not part of the request pipeline:
+ SERVICE_PROVIDER = app.Services;
+
// Initialize the encryption service:
+ programLogger.LogInformation("Initializing the encryption service.");
var encryptionLogger = app.Services.GetRequiredService>();
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
var encryptionInitializer = encryption.Initialize();
// Set the logger for the Rust service:
+ programLogger.LogInformation("Initializing the Rust service.");
var rustLogger = app.Services.GetRequiredService>();
rust.SetLogger(rustLogger);
rust.SetEncryptor(encryption);
@@ -156,6 +175,7 @@ internal sealed class Program
RUST_SERVICE = rust;
ENCRYPTION = encryption;
+ programLogger.LogInformation("Initialize internal file system.");
app.Use(Redirect.HandlerContentAsync);
#if DEBUG
@@ -175,9 +195,22 @@ internal sealed class Program
.AddInteractiveServerRenderMode();
var serverTask = app.RunAsync();
+ programLogger.LogInformation("Server was started successfully.");
await encryptionInitializer;
await rust.AppIsReady();
+ programLogger.LogInformation("The AI Studio server is ready.");
+
+ TaskScheduler.UnobservedTaskException += (sender, taskArgs) =>
+ {
+ programLogger.LogError(taskArgs.Exception, $"Unobserved task exception by sender '{sender ?? "n/a"}'.");
+ taskArgs.SetObserved();
+ };
+
await serverTask;
+
+ RUST_SERVICE.Dispose();
+ PluginFactory.Dispose();
+ programLogger.LogInformation("The AI Studio server was stopped.");
}
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs
index d0e20d34..7693d21f 100644
--- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs
+++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs
@@ -1,3 +1,4 @@
+using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
@@ -86,21 +87,15 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
///
public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
- return Task.FromResult(new[]
+ var additionalModels = new[]
{
- new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (latest)"),
- new Model("claude-3-5-sonnet-20240620", "Claude 3.5 Sonnet (20. June 2024)"),
- new Model("claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet (22. October 2024)"),
-
- new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (latest)"),
- new Model("claude-3-5-heiku-20241022", "Claude 3.5 Haiku (22. October 2024)"),
-
- new Model("claude-3-opus-20240229", "Claude 3.0 Opus (29. February 2024)"),
- new Model("claude-3-opus-latest", "Claude 3.0 Opus (latest)"),
-
- new Model("claude-3-sonnet-20240229", "Claude 3.0 Sonnet (29. February 2024)"),
- new Model("claude-3-haiku-20240307", "Claude 3.0 Haiku (7. March 2024)"),
- }.AsEnumerable());
+ new Model("claude-3-7-sonnet-latest", "Claude 3.7 Sonnet (Latest)"),
+ new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (Latest)"),
+ new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (Latest)"),
+ new Model("claude-3-opus-latest", "Claude 3 Opus (Latest)"),
+ };
+
+ return this.LoadModels(token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
}
///
@@ -116,4 +111,37 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
}
#endregion
+
+ private async Task> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
+ {
+ var secretKey = apiKeyProvisional switch
+ {
+ not null => apiKeyProvisional,
+ _ => await RUST_SERVICE.GetAPIKey(this) switch
+ {
+ { Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
+ _ => null,
+ }
+ };
+
+ if (secretKey is null)
+ return [];
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models?limit=100");
+
+ // Set the authorization header:
+ request.Headers.Add("x-api-key", secretKey);
+
+ // Set the Anthropic version:
+ request.Headers.Add("anthropic-version", "2023-06-01");
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
+
+ using var response = await this.httpClient.SendAsync(request, token);
+ if(!response.IsSuccessStatusCode)
+ return [];
+
+ var modelResponse = await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, token);
+ return modelResponse.Data;
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs
index 25143834..3ad4e8f2 100644
--- a/app/MindWork AI Studio/Provider/BaseProvider.cs
+++ b/app/MindWork AI Studio/Provider/BaseProvider.cs
@@ -4,8 +4,7 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Settings;
-
-using RustService = AIStudio.Tools.RustService;
+using AIStudio.Tools.Services;
namespace AIStudio.Provider;
@@ -103,9 +102,14 @@ public abstract class BaseProvider : IProvider, ISecretId
{
using var request = await requestBuilder();
+ //
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
+ //
+ // Please notice: We do not dispose the response here. The caller is responsible
+ // for disposing the response object. This is important because the response
+ // object is used to read the stream.
var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
if (nextResponse.IsSuccessStatusCode)
{
diff --git a/app/MindWork AI Studio/Provider/Confidence.cs b/app/MindWork AI Studio/Provider/Confidence.cs
index ac5557a8..087ac4e0 100644
--- a/app/MindWork AI Studio/Provider/Confidence.cs
+++ b/app/MindWork AI Studio/Provider/Confidence.cs
@@ -53,6 +53,12 @@ public sealed record Confidence
Description = "The provider operates its service from the USA and is subject to **US jurisdiction**. In case of suspicion, authorities in the USA can access your data. However, **your data is not used for training** purposes.",
};
+ public static readonly Confidence CHINA_NO_TRAINING = new()
+ {
+ Level = ConfidenceLevel.MODERATE,
+ Description = "The provider operates its service from China. In case of suspicion, authorities in the respective countries of operation may access your data. However, **your data is not used for training** purposes.",
+ };
+
public static readonly Confidence GDPR_NO_TRAINING = new()
{
Level = ConfidenceLevel.MEDIUM,
diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs
new file mode 100644
index 00000000..b4ce57a0
--- /dev/null
+++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs
@@ -0,0 +1,136 @@
+using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+
+using AIStudio.Chat;
+using AIStudio.Provider.OpenAI;
+using AIStudio.Settings;
+
+namespace AIStudio.Provider.DeepSeek;
+
+public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api.deepseek.com/", logger)
+{
+ #region Implementation of IProvider
+
+ ///
+ public override string Id => LLMProviders.DEEP_SEEK.ToName();
+
+ ///
+ public override string InstanceName { get; set; } = "DeepSeek";
+
+ ///
+ public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ // Get the API key:
+ var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
+ if(!requestedSecret.Success)
+ yield break;
+
+ // Prepare the system prompt:
+ var systemPrompt = new Message
+ {
+ Role = "system",
+ Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
+ };
+
+ // Prepare the DeepSeek HTTP chat request:
+ var deepSeekChatRequest = JsonSerializer.Serialize(new ChatRequest
+ {
+ Model = chatModel.Id,
+
+ // Build the messages:
+ // - First of all the system prompt
+ // - Then none-empty user and AI messages
+ Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
+ {
+ Role = n.Role switch
+ {
+ ChatRole.USER => "user",
+ ChatRole.AI => "assistant",
+ ChatRole.AGENT => "assistant",
+ ChatRole.SYSTEM => "system",
+
+ _ => "user",
+ },
+
+ Content = n.Content switch
+ {
+ ContentText text => text.Text,
+ _ => string.Empty,
+ }
+ }).ToList()],
+ Stream = true,
+ }, JSON_SERIALIZER_OPTIONS);
+
+ async Task RequestBuilder()
+ {
+ // Build the HTTP post request:
+ var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
+
+ // Set the authorization header:
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
+
+ // Set the content:
+ request.Content = new StringContent(deepSeekChatRequest, Encoding.UTF8, "application/json");
+ return request;
+ }
+
+ await foreach (var content in this.StreamChatCompletionInternal("Helmholtz", RequestBuilder, token))
+ yield return content;
+ }
+
+ #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ ///
+ public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ yield break;
+ }
+ #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+
+ ///
+ public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ return this.LoadModels(token, apiKeyProvisional);
+ }
+
+ ///
+ public override Task> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ ///
+ public override Task> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ #endregion
+
+ private async Task> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
+ {
+ var secretKey = apiKeyProvisional switch
+ {
+ not null => apiKeyProvisional,
+ _ => await RUST_SERVICE.GetAPIKey(this) switch
+ {
+ { Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
+ _ => null,
+ }
+ };
+
+ if (secretKey is null)
+ return [];
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
+
+ using var response = await this.httpClient.SendAsync(request, token);
+ if(!response.IsSuccessStatusCode)
+ return [];
+
+ var modelResponse = await response.Content.ReadFromJsonAsync(token);
+ return modelResponse.Data;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs
new file mode 100644
index 00000000..c0562a69
--- /dev/null
+++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs
@@ -0,0 +1,138 @@
+using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+
+using AIStudio.Chat;
+using AIStudio.Provider.OpenAI;
+using AIStudio.Settings;
+
+namespace AIStudio.Provider.GWDG;
+
+public sealed class ProviderGWDG(ILogger logger) : BaseProvider("https://chat-ai.academiccloud.de/v1/", logger)
+{
+ #region Implementation of IProvider
+
+ ///
+ public override string Id => LLMProviders.GWDG.ToName();
+
+ ///
+ public override string InstanceName { get; set; } = "GWDG SAIA";
+
+ ///
+ public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ // Get the API key:
+ var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
+ if(!requestedSecret.Success)
+ yield break;
+
+ // Prepare the system prompt:
+ var systemPrompt = new Message
+ {
+ Role = "system",
+ Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
+ };
+
+ // Prepare the GWDG HTTP chat request:
+ var gwdgChatRequest = JsonSerializer.Serialize(new ChatRequest
+ {
+ Model = chatModel.Id,
+
+ // Build the messages:
+ // - First of all the system prompt
+ // - Then none-empty user and AI messages
+ Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
+ {
+ Role = n.Role switch
+ {
+ ChatRole.USER => "user",
+ ChatRole.AI => "assistant",
+ ChatRole.AGENT => "assistant",
+ ChatRole.SYSTEM => "system",
+
+ _ => "user",
+ },
+
+ Content = n.Content switch
+ {
+ ContentText text => text.Text,
+ _ => string.Empty,
+ }
+ }).ToList()],
+ Stream = true,
+ }, JSON_SERIALIZER_OPTIONS);
+
+ async Task RequestBuilder()
+ {
+ // Build the HTTP post request:
+ var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
+
+ // Set the authorization header:
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
+
+ // Set the content:
+ request.Content = new StringContent(gwdgChatRequest, Encoding.UTF8, "application/json");
+ return request;
+ }
+
+ await foreach (var content in this.StreamChatCompletionInternal("GWDG", RequestBuilder, token))
+ yield return content;
+ }
+
+ #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ ///
+ public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ yield break;
+ }
+ #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+
+ ///
+ public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ var models = await this.LoadModels(token, apiKeyProvisional);
+ return models.Where(model => !model.Id.StartsWith("e5-mistral-7b-instruct", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ public override Task> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ ///
+ public override async Task> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ var models = await this.LoadModels(token, apiKeyProvisional);
+ return models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ #endregion
+
+ private async Task> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
+ {
+ var secretKey = apiKeyProvisional switch
+ {
+ not null => apiKeyProvisional,
+ _ => await RUST_SERVICE.GetAPIKey(this) switch
+ {
+ { Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
+ _ => null,
+ }
+ };
+
+ if (secretKey is null)
+ return [];
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
+
+ using var response = await this.httpClient.SendAsync(request, token);
+ if(!response.IsSuccessStatusCode)
+ return [];
+
+ var modelResponse = await response.Content.ReadFromJsonAsync(token);
+ return modelResponse.Data;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs
index e0d5da77..942cb245 100644
--- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs
+++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs
@@ -136,8 +136,8 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
if (secretKey is null)
return default;
- var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
- var response = await this.httpClient.SendAsync(request, token);
+ using var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
+ using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return default;
diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs
index 2fc7e88f..f32a31b5 100644
--- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs
+++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs
@@ -127,10 +127,10 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
if (secretKey is null)
return [];
- var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
- var response = await this.httpClient.SendAsync(request, token);
+ using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return [];
diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs
new file mode 100644
index 00000000..b8450503
--- /dev/null
+++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs
@@ -0,0 +1,142 @@
+using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+
+using AIStudio.Chat;
+using AIStudio.Provider.OpenAI;
+using AIStudio.Settings;
+
+namespace AIStudio.Provider.Helmholtz;
+
+public sealed class ProviderHelmholtz(ILogger logger) : BaseProvider("https://api.helmholtz-blablador.fz-juelich.de/v1/", logger)
+{
+ #region Implementation of IProvider
+
+ ///
+ public override string Id => LLMProviders.HELMHOLTZ.ToName();
+
+ ///
+ public override string InstanceName { get; set; } = "Helmholtz Blablador";
+
+ ///
+ public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ // Get the API key:
+ var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
+ if(!requestedSecret.Success)
+ yield break;
+
+ // Prepare the system prompt:
+ var systemPrompt = new Message
+ {
+ Role = "system",
+ Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
+ };
+
+ // Prepare the Helmholtz HTTP chat request:
+ var helmholtzChatRequest = JsonSerializer.Serialize(new ChatRequest
+ {
+ Model = chatModel.Id,
+
+ // Build the messages:
+ // - First of all the system prompt
+ // - Then none-empty user and AI messages
+ Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
+ {
+ Role = n.Role switch
+ {
+ ChatRole.USER => "user",
+ ChatRole.AI => "assistant",
+ ChatRole.AGENT => "assistant",
+ ChatRole.SYSTEM => "system",
+
+ _ => "user",
+ },
+
+ Content = n.Content switch
+ {
+ ContentText text => text.Text,
+ _ => string.Empty,
+ }
+ }).ToList()],
+ Stream = true,
+ }, JSON_SERIALIZER_OPTIONS);
+
+ async Task RequestBuilder()
+ {
+ // Build the HTTP post request:
+ var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
+
+ // Set the authorization header:
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
+
+ // Set the content:
+ request.Content = new StringContent(helmholtzChatRequest, Encoding.UTF8, "application/json");
+ return request;
+ }
+
+ await foreach (var content in this.StreamChatCompletionInternal("Helmholtz", RequestBuilder, token))
+ yield return content;
+ }
+
+ #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ ///
+ public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ yield break;
+ }
+ #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+
+ ///
+ public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ var models = await this.LoadModels(token, apiKeyProvisional);
+ return models.Where(model => !model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) &&
+ !model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ public override Task> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ ///
+ public override async Task> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
+ {
+ var models = await this.LoadModels(token, apiKeyProvisional);
+ return models.Where(model =>
+ model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase) ||
+ model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) ||
+ model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ #endregion
+
+ private async Task> LoadModels(CancellationToken token, string? apiKeyProvisional = null)
+ {
+ var secretKey = apiKeyProvisional switch
+ {
+ not null => apiKeyProvisional,
+ _ => await RUST_SERVICE.GetAPIKey(this) switch
+ {
+ { Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
+ _ => null,
+ }
+ };
+
+ if (secretKey is null)
+ return [];
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
+
+ using var response = await this.httpClient.SendAsync(request, token);
+ if(!response.IsSuccessStatusCode)
+ return [];
+
+ var modelResponse = await response.Content.ReadFromJsonAsync(token);
+ return modelResponse.Data;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/LLMProviders.cs b/app/MindWork AI Studio/Provider/LLMProviders.cs
index 2078e5d0..92c0873b 100644
--- a/app/MindWork AI Studio/Provider/LLMProviders.cs
+++ b/app/MindWork AI Studio/Provider/LLMProviders.cs
@@ -12,9 +12,13 @@ public enum LLMProviders
MISTRAL = 3,
GOOGLE = 7,
X = 8,
+ DEEP_SEEK = 11,
FIREWORKS = 5,
GROQ = 6,
SELF_HOSTED = 4,
+
+ HELMHOLTZ = 9,
+ GWDG = 10,
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs
index f9addf8d..7fb80e7f 100644
--- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs
+++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs
@@ -1,7 +1,10 @@
using AIStudio.Provider.Anthropic;
+using AIStudio.Provider.DeepSeek;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Google;
using AIStudio.Provider.Groq;
+using AIStudio.Provider.GWDG;
+using AIStudio.Provider.Helmholtz;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
@@ -28,12 +31,16 @@ public static class LLMProvidersExtensions
LLMProviders.MISTRAL => "Mistral",
LLMProviders.GOOGLE => "Google",
LLMProviders.X => "xAI",
+ LLMProviders.DEEP_SEEK => "DeepSeek",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
LLMProviders.SELF_HOSTED => "Self-hosted",
+ LLMProviders.HELMHOLTZ => "Helmholtz Blablador",
+ LLMProviders.GWDG => "GWDG SAIA",
+
_ => "Unknown",
};
@@ -66,8 +73,13 @@ public static class LLMProvidersExtensions
LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
+ LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
+
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
+ LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
+ LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
+
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
};
@@ -84,6 +96,7 @@ public static class LLMProvidersExtensions
LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true,
LLMProviders.GOOGLE => true,
+ LLMProviders.HELMHOLTZ => true,
//
// Providers that do not support embeddings:
@@ -92,6 +105,8 @@ public static class LLMProvidersExtensions
LLMProviders.ANTHROPIC => false,
LLMProviders.FIREWORKS => false,
LLMProviders.X => false,
+ LLMProviders.GWDG => false,
+ LLMProviders.DEEP_SEEK => false,
//
// Self-hosted providers are treated as a special case anyway.
@@ -134,12 +149,16 @@ public static class LLMProvidersExtensions
LLMProviders.MISTRAL => new ProviderMistral(logger) { InstanceName = instanceName },
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = instanceName },
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
+ LLMProviders.DEEP_SEEK => new ProviderDeepSeek(logger) { InstanceName = instanceName },
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, host, hostname) { InstanceName = instanceName },
+ LLMProviders.HELMHOLTZ => new ProviderHelmholtz(logger) { InstanceName = instanceName },
+ LLMProviders.GWDG => new ProviderGWDG(logger) { InstanceName = instanceName },
+
_ => new NoProvider(),
};
}
@@ -157,10 +176,14 @@ public static class LLMProvidersExtensions
LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard",
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
LLMProviders.X => "https://accounts.x.ai/sign-up",
+ LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up",
LLMProviders.GROQ => "https://console.groq.com/",
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
+ LLMProviders.HELMHOLTZ => "https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/#step-1-register-on-gitlab",
+ LLMProviders.GWDG => "https://docs.hpc.gwdg.de/services/saia/index.html#api-request",
+
_ => string.Empty,
};
@@ -173,6 +196,7 @@ public static class LLMProvidersExtensions
LLMProviders.GROQ => "https://console.groq.com/settings/usage",
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
+ LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage",
_ => string.Empty,
};
@@ -186,6 +210,7 @@ public static class LLMProvidersExtensions
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
LLMProviders.GOOGLE => true,
+ LLMProviders.DEEP_SEEK => true,
_ => false,
};
@@ -227,9 +252,12 @@ public static class LLMProvidersExtensions
LLMProviders.ANTHROPIC => true,
LLMProviders.GOOGLE => true,
LLMProviders.X => true,
+ LLMProviders.DEEP_SEEK => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
+ LLMProviders.HELMHOLTZ => true,
+ LLMProviders.GWDG => true,
LLMProviders.SELF_HOSTED => host is Host.OLLAMA,
@@ -243,9 +271,12 @@ public static class LLMProvidersExtensions
LLMProviders.ANTHROPIC => true,
LLMProviders.GOOGLE => true,
LLMProviders.X => true,
+ LLMProviders.DEEP_SEEK => true,
LLMProviders.GROQ => true,
LLMProviders.FIREWORKS => true,
+ LLMProviders.HELMHOLTZ => true,
+ LLMProviders.GWDG => true,
_ => false,
};
diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs
index ebde4c7b..024f60d3 100644
--- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs
+++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs
@@ -138,10 +138,10 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
if (secretKey is null)
return default;
- var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
- var response = await this.httpClient.SendAsync(request, token);
+ using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return default;
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs
index 6ddeeabc..ed092174 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs
@@ -154,10 +154,10 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
if (secretKey is null)
return [];
- var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
- var response = await this.httpClient.SendAsync(request, token);
+ using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return [];
diff --git a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs
index ada7920b..a4e29648 100644
--- a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs
+++ b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs
@@ -15,19 +15,11 @@ public static class HostExtensions
public static string BaseURL(this Host host) => host switch
{
- Host.LM_STUDIO => "/v1/",
- Host.LLAMACPP => "/v1/",
- Host.OLLAMA => "/v1/",
-
_ => "/v1/",
};
public static string ChatURL(this Host host) => host switch
{
- Host.LM_STUDIO => "chat/completions",
- Host.LLAMACPP => "chat/completions",
- Host.OLLAMA => "chat/completions",
-
_ => "chat/completions",
};
diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs
index 3abda28c..4ba45c6b 100644
--- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs
+++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs
@@ -154,11 +154,11 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
}
};
- var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
+ using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
if(secretKey is not null)
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
- var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
+ using var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
if(!lmStudioResponse.IsSuccessStatusCode)
return [];
diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs
index c915c964..a8334c8d 100644
--- a/app/MindWork AI Studio/Provider/X/ProviderX.cs
+++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs
@@ -127,10 +127,10 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v
if (secretKey is null)
return [];
- var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
- var response = await this.httpClient.SendAsync(request, token);
+ using var response = await this.httpClient.SendAsync(request, token);
if(!response.IsSuccessStatusCode)
return [];
diff --git a/app/MindWork AI Studio/Redirect.cs b/app/MindWork AI Studio/Redirect.cs
index 1b974a3d..29c42bce 100644
--- a/app/MindWork AI Studio/Redirect.cs
+++ b/app/MindWork AI Studio/Redirect.cs
@@ -13,7 +13,6 @@ internal static class Redirect
await nextHandler();
return;
}
-
#if DEBUG
diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs
index 98326821..b6318820 100644
--- a/app/MindWork AI Studio/Routes.razor.cs
+++ b/app/MindWork AI Studio/Routes.razor.cs
@@ -9,6 +9,7 @@ public sealed partial class Routes
public const string SETTINGS = "/settings";
public const string SUPPORTERS = "/supporters";
public const string WRITER = "/writer";
+ public const string PLUGINS = "/plugins";
// ReSharper disable InconsistentNaming
public const string ASSISTANT_TRANSLATION = "/assistant/translation";
diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs
index 3f6ec933..5a2b8f11 100644
--- a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs
+++ b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs
@@ -94,6 +94,12 @@ public static class ConfigurationSelectDataFactory
yield return new(source.GetPreviewDescription(), source);
}
+ public static IEnumerable> GetSendToChatDataSourceBehaviorData()
+ {
+ foreach (var behavior in Enum.GetValues())
+ yield return new(behavior.Description(), behavior);
+ }
+
public static IEnumerable> GetNavBehaviorData()
{
yield return new("Navigation expands on mouse hover", NavBehavior.EXPAND_ON_HOVER);
diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs
index f6f08a5a..b47eba49 100644
--- a/app/MindWork AI Studio/Settings/DataModel/Data.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs
@@ -25,12 +25,22 @@ public sealed class Data
/// A collection of embedding providers configured.
///
public List EmbeddingProviders { get; init; } = [];
+
+ ///
+ /// A collection of data sources configured.
+ ///
+ public List DataSources { get; set; } = [];
///
/// List of configured profiles.
///
public List Profiles { get; init; } = [];
+ ///
+ /// List of enabled plugins.
+ ///
+ public List EnabledPlugins { get; set; } = [];
+
///
/// The next provider number to use.
///
@@ -41,6 +51,11 @@ public sealed class Data
///
public uint NextEmbeddingNum { get; set; } = 1;
+ ///
+ /// The next data source number to use.
+ ///
+ public uint NextDataSourceNum { get; set; } = 1;
+
///
/// The next profile number to use.
///
@@ -64,6 +79,10 @@ public sealed class Data
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
+ public DataAgentDataSourceSelection AgentDataSourceSelection { get; init; } = new();
+
+ public DataAgentRetrievalContextValidation AgentRetrievalContextValidation { get; init; } = new();
+
public DataAgenda Agenda { get; init; } = new();
public DataGrammarSpelling GrammarSpelling { get; init; } = new();
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataAgentDataSourceSelection.cs b/app/MindWork AI Studio/Settings/DataModel/DataAgentDataSourceSelection.cs
new file mode 100644
index 00000000..55473dcc
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataAgentDataSourceSelection.cs
@@ -0,0 +1,14 @@
+namespace AIStudio.Settings.DataModel;
+
+public sealed class DataAgentDataSourceSelection
+{
+ ///
+ /// Preselect any data source selection options?
+ ///
+ public bool PreselectAgentOptions { get; set; }
+
+ ///
+ /// Preselect a data source selection provider?
+ ///
+ public string PreselectedAgentProvider { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataAgentRetrievalContextValidation.cs b/app/MindWork AI Studio/Settings/DataModel/DataAgentRetrievalContextValidation.cs
new file mode 100644
index 00000000..a4ae0a8f
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataAgentRetrievalContextValidation.cs
@@ -0,0 +1,24 @@
+namespace AIStudio.Settings.DataModel;
+
+public sealed class DataAgentRetrievalContextValidation
+{
+ ///
+ /// Enable the retrieval context validation agent?
+ ///
+ public bool EnableRetrievalContextValidation { get; set; }
+
+ ///
+ /// Preselect any retrieval context validation options?
+ ///
+ public bool PreselectAgentOptions { get; set; }
+
+ ///
+ /// Preselect a retrieval context validation provider?
+ ///
+ public string PreselectedAgentProvider { get; set; } = string.Empty;
+
+ ///
+ /// Configure how many parallel validations to run.
+ ///
+ public int NumParallelValidations { get; set; } = 3;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs
index 8283150b..baf995fd 100644
--- a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs
@@ -17,6 +17,11 @@ public sealed class DataChat
///
public AddChatProviderBehavior AddChatProviderBehavior { get; set; } = AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER;
+ ///
+ /// Defines the data source behavior when sending assistant results to a chat.
+ ///
+ public SendToChatDataSourceBehavior SendToChatDataSourceBehavior { get; set; } = SendToChatDataSourceBehavior.NO_DATA_SOURCES;
+
///
/// Preselect any chat options?
///
@@ -31,6 +36,11 @@ public sealed class DataChat
/// Preselect a profile?
///
public string PreselectedProfile { get; set; } = string.Empty;
+
+ ///
+ /// Should we preselect data sources options for a created chat?
+ ///
+ public DataSourceOptions PreselectedDataSourceOptions { get; set; } = new();
///
/// Should we show the latest message after loading? When false, we show the first (aka oldest) message.
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs b/app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs
index a716476a..30ad8bab 100644
--- a/app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs
@@ -22,7 +22,7 @@ public sealed class DataLLMProviders
///
/// Which confidence scheme to use.
///
- public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_USA_EUROPE;
+ public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_ALL;
///
/// Provide custom confidence levels for each LLM provider.
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs
new file mode 100644
index 00000000..cc43a3eb
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs
@@ -0,0 +1,137 @@
+// ReSharper disable InconsistentNaming
+
+using AIStudio.Assistants.ERI;
+using AIStudio.Chat;
+using AIStudio.Tools.ERIClient;
+using AIStudio.Tools.ERIClient.DataModel;
+using AIStudio.Tools.RAG;
+using AIStudio.Tools.Services;
+
+using ChatThread = AIStudio.Chat.ChatThread;
+using ContentType = AIStudio.Tools.ERIClient.DataModel.ContentType;
+
+namespace AIStudio.Settings.DataModel;
+
+///
+/// An external data source, accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI.
+///
+public readonly record struct DataSourceERI_V1 : IERIDataSource
+{
+ public DataSourceERI_V1()
+ {
+ }
+
+ ///
+ public uint Num { get; init; }
+
+ ///
+ public string Id { get; init; } = Guid.Empty.ToString();
+
+ ///
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ public DataSourceType Type { get; init; } = DataSourceType.NONE;
+
+ ///
+ public string Hostname { get; init; } = string.Empty;
+
+ ///
+ public int Port { get; init; }
+
+ ///
+ public AuthMethod AuthMethod { get; init; } = AuthMethod.NONE;
+
+ ///
+ public string Username { get; init; } = string.Empty;
+
+ ///
+ public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
+
+ ///
+ public ERIVersion Version { get; init; } = ERIVersion.V1;
+
+ ///
+ public string SelectedRetrievalId { get; init; } = string.Empty;
+
+ ///
+ public async Task> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
+ {
+ // Important: Do not dispose the RustService here, as it is a singleton.
+ var rustService = Program.SERVICE_PROVIDER.GetRequiredService();
+ var logger = Program.SERVICE_PROVIDER.GetRequiredService>();
+
+ using var eriClient = ERIClientFactory.Get(this.Version, this)!;
+ var authResponse = await eriClient.AuthenticateAsync(rustService, cancellationToken: token);
+ if (authResponse.Successful)
+ {
+ var retrievalRequest = new RetrievalRequest
+ {
+ LatestUserPromptType = lastPrompt.ToERIContentType,
+ LatestUserPrompt = lastPrompt switch
+ {
+ ContentText text => text.Text,
+ ContentImage image => await image.AsBase64(token),
+ _ => string.Empty
+ },
+
+ Thread = await thread.ToERIChatThread(token),
+ MaxMatches = 10,
+ RetrievalProcessId = string.IsNullOrWhiteSpace(this.SelectedRetrievalId) ? null : this.SelectedRetrievalId,
+ Parameters = null, // The ERI server selects useful default parameters
+ };
+
+ var retrievalResponse = await eriClient.ExecuteRetrievalAsync(retrievalRequest, token);
+ if(retrievalResponse is { Successful: true, Data: not null })
+ {
+ //
+ // Next, we have to transform the ERI context back to our generic retrieval context:
+ //
+ var genericRetrievalContexts = new List(retrievalResponse.Data.Count);
+ foreach (var eriContext in retrievalResponse.Data)
+ {
+ switch (eriContext.Type)
+ {
+ case ContentType.TEXT:
+ genericRetrievalContexts.Add(new RetrievalTextContext
+ {
+ Path = eriContext.Path ?? string.Empty,
+ Type = eriContext.ToRetrievalContentType(),
+ Links = eriContext.Links,
+ Category = eriContext.Type.ToRetrievalContentCategory(),
+ MatchedText = eriContext.MatchedContent,
+ DataSourceName = this.Name,
+ SurroundingContent = eriContext.SurroundingContent,
+ });
+ break;
+
+ case ContentType.IMAGE:
+ genericRetrievalContexts.Add(new RetrievalImageContext
+ {
+ Path = eriContext.Path ?? string.Empty,
+ Type = eriContext.ToRetrievalContentType(),
+ Links = eriContext.Links,
+ Source = eriContext.MatchedContent,
+ Category = eriContext.Type.ToRetrievalContentCategory(),
+ SourceType = ContentImageSource.BASE64,
+ DataSourceName = this.Name,
+ });
+ break;
+
+ default:
+ logger.LogWarning($"The ERI context type '{eriContext.Type}' is not supported yet.");
+ break;
+ }
+ }
+
+ return genericRetrievalContexts;
+ }
+
+ logger.LogWarning($"Was not able to retrieve data from the ERI data source '{this.Name}'. Message: {retrievalResponse.Message}");
+ return [];
+ }
+
+ logger.LogWarning($"Was not able to authenticate with the ERI data source '{this.Name}'. Message: {authResponse.Message}");
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs
new file mode 100644
index 00000000..d81e30db
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs
@@ -0,0 +1,44 @@
+using AIStudio.Chat;
+using AIStudio.Tools.RAG;
+
+namespace AIStudio.Settings.DataModel;
+
+///
+/// Represents a local directory as a data source.
+///
+public readonly record struct DataSourceLocalDirectory : IInternalDataSource
+{
+ public DataSourceLocalDirectory()
+ {
+ }
+
+ ///
+ public uint Num { get; init; }
+
+ ///
+ public string Id { get; init; } = Guid.Empty.ToString();
+
+ ///
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ public DataSourceType Type { get; init; } = DataSourceType.NONE;
+
+ ///
+ public string EmbeddingId { get; init; } = Guid.Empty.ToString();
+
+ ///
+ public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
+
+ ///
+ public Task> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
+ {
+ IReadOnlyList retrievalContext = new List();
+ return Task.FromResult(retrievalContext);
+ }
+
+ ///
+ /// The path to the directory.
+ ///
+ public string Path { get; init; } = string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs
new file mode 100644
index 00000000..5788a2a6
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs
@@ -0,0 +1,44 @@
+using AIStudio.Chat;
+using AIStudio.Tools.RAG;
+
+namespace AIStudio.Settings.DataModel;
+
+///
+/// Represents one local file as a data source.
+///
+public readonly record struct DataSourceLocalFile : IInternalDataSource
+{
+ public DataSourceLocalFile()
+ {
+ }
+
+ ///
+ public uint Num { get; init; }
+
+ ///
+ public string Id { get; init; } = Guid.Empty.ToString();
+
+ ///
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ public DataSourceType Type { get; init; } = DataSourceType.NONE;
+
+ ///
+ public string EmbeddingId { get; init; } = Guid.Empty.ToString();
+
+ ///
+ public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
+
+ ///
+ public Task> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
+ {
+ IReadOnlyList retrievalContext = new List();
+ return Task.FromResult(retrievalContext);
+ }
+
+ ///
+ /// The path to the file.
+ ///
+ public string FilePath { get; init; } = string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceOptions.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceOptions.cs
new file mode 100644
index 00000000..1def9887
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceOptions.cs
@@ -0,0 +1,65 @@
+namespace AIStudio.Settings.DataModel;
+
+public sealed class DataSourceOptions
+{
+ ///
+ /// Whether data sources are disabled in this context.
+ ///
+ public bool DisableDataSources { get; set; } = true;
+
+ ///
+ /// Whether the data sources should be selected automatically.
+ ///
+ ///
+ /// When true, the appropriate data sources for the current prompt are
+ /// selected automatically. When false, the user has to select the
+ /// data sources manually.
+ ///
+ /// This setting does not affect the selection of the actual data
+ /// for augmentation.
+ ///
+ public bool AutomaticDataSourceSelection { get; set; }
+
+ ///
+ /// Whether the retrieved data should be validated for the current prompt.
+ ///
+ ///
+ /// When true, the retrieved data is validated against the current prompt.
+ /// An AI will decide whether the data point is useful for answering the
+ /// prompt or not.
+ ///
+ public bool AutomaticValidation { get; set; }
+
+ ///
+ /// The preselected data source IDs. When these data sources are available
+ /// for the selected provider, they are pre-selected.
+ ///
+ public List PreselectedDataSourceIds { get; set; } = [];
+
+ ///
+ /// Returns true when data sources are enabled.
+ ///
+ /// True when data sources are enabled.
+ public bool IsEnabled()
+ {
+ if(this.DisableDataSources)
+ return false;
+
+ if(this.AutomaticDataSourceSelection)
+ return true;
+
+ return this.PreselectedDataSourceIds.Count > 0;
+ }
+
+ ///
+ /// Creates a copy of the current data source options.
+ ///
+ /// A copy of the current data source options.
+ public DataSourceOptions CreateCopy() => new()
+ {
+ DisableDataSources = this.DisableDataSources,
+ AutomaticDataSourceSelection = this.AutomaticDataSourceSelection,
+ AutomaticValidation = this.AutomaticValidation,
+ PreselectedDataSourceIds = [..this.PreselectedDataSourceIds],
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurity.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurity.cs
new file mode 100644
index 00000000..58caf457
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurity.cs
@@ -0,0 +1,19 @@
+namespace AIStudio.Settings.DataModel;
+
+public enum DataSourceSecurity
+{
+ ///
+ /// The security of the data source is not specified yet.
+ ///
+ NOT_SPECIFIED,
+
+ ///
+ /// This data can be used with any LLM provider.
+ ///
+ ALLOW_ANY,
+
+ ///
+ /// This data can only be used for self-hosted LLM providers.
+ ///
+ SELF_HOSTED,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs
new file mode 100644
index 00000000..6e52d0fd
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceSecurityExtensions.cs
@@ -0,0 +1,32 @@
+namespace AIStudio.Settings.DataModel;
+
+public static class DataSourceSecurityExtensions
+{
+ public static string ToSelectionText(this DataSourceSecurity security) => security switch
+ {
+ DataSourceSecurity.NOT_SPECIFIED => "Please select a security policy",
+
+ DataSourceSecurity.ALLOW_ANY => "This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider.",
+ DataSourceSecurity.SELF_HOSTED => "This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider.",
+
+ _ => "Unknown security policy"
+ };
+
+ public static string ToInfoText(this DataSourceSecurity security) => security switch
+ {
+ DataSourceSecurity.NOT_SPECIFIED => "The security of the data source is not specified yet. You cannot use this data source until you specify a security policy.",
+
+ DataSourceSecurity.ALLOW_ANY => "This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider.",
+ DataSourceSecurity.SELF_HOSTED => "This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider.",
+
+ _ => "Unknown security policy"
+ };
+
+ public static TextColor GetColor(this DataSourceSecurity security) => security switch
+ {
+ DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
+ DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
+
+ _ => TextColor.ERROR
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs
new file mode 100644
index 00000000..9c8b031f
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs
@@ -0,0 +1,27 @@
+namespace AIStudio.Settings.DataModel;
+
+///
+/// AI Studio data source types.
+///
+public enum DataSourceType
+{
+ ///
+ /// No data source.
+ ///
+ NONE = 0,
+
+ ///
+ /// One file on the local machine (or a network share).
+ ///
+ LOCAL_FILE,
+
+ ///
+ /// A directory on the local machine (or a network share).
+ ///
+ LOCAL_DIRECTORY,
+
+ ///
+ /// External data source accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI.
+ ///
+ ERI_V1,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs
new file mode 100644
index 00000000..196eac78
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs
@@ -0,0 +1,21 @@
+namespace AIStudio.Settings.DataModel;
+
+///
+/// Extension methods for data source types.
+///
+public static class DataSourceTypeExtension
+{
+ ///
+ /// Get the display name of the data source type.
+ ///
+ /// The data source type.
+ /// The display name of the data source type.
+ public static string GetDisplayName(this DataSourceType type) => type switch
+ {
+ DataSourceType.LOCAL_FILE => "Local File",
+ DataSourceType.LOCAL_DIRECTORY => "Local Directory",
+ DataSourceType.ERI_V1 => "External ERI Server (v1)",
+
+ _ => "None",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
index 825e8037..ff642a0a 100644
--- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
@@ -8,4 +8,6 @@ public enum PreviewFeatures
//
PRE_WRITER_MODE_2024,
PRE_RAG_2024,
+
+ PRE_PLUGINS_2025,
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeatureExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs
similarity index 78%
rename from app/MindWork AI Studio/Settings/DataModel/PreviewFeatureExtensions.cs
rename to app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs
index a6e6cf0e..2eb25587 100644
--- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeatureExtensions.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs
@@ -1,11 +1,12 @@
namespace AIStudio.Settings.DataModel;
-public static class PreviewFeatureExtensions
+public static class PreviewFeaturesExtensions
{
public static string GetPreviewDescription(this PreviewFeatures feature) => feature switch
{
PreviewFeatures.PRE_WRITER_MODE_2024 => "Writer Mode: Experiments about how to write long texts using AI",
PreviewFeatures.PRE_RAG_2024 => "RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company",
+ PreviewFeatures.PRE_PLUGINS_2025 => "Plugins: Preview of our plugin system where you can extend the functionality of the app",
_ => "Unknown preview feature"
};
diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs
index f80939f6..b0f07716 100644
--- a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs
@@ -25,6 +25,7 @@ public static class PreviewVisibilityExtensions
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
+ features.Add(PreviewFeatures.PRE_PLUGINS_2025);
}
return features;
diff --git a/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehavior.cs b/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehavior.cs
new file mode 100644
index 00000000..fcbcaf4b
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehavior.cs
@@ -0,0 +1,7 @@
+namespace AIStudio.Settings.DataModel;
+
+public enum SendToChatDataSourceBehavior
+{
+ NO_DATA_SOURCES,
+ APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs
new file mode 100644
index 00000000..3894ba2b
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/SendToChatDataSourceBehaviorExtensions.cs
@@ -0,0 +1,12 @@
+namespace AIStudio.Settings.DataModel;
+
+public static class SendToChatDataSourceBehaviorExtensions
+{
+ public static string Description(this SendToChatDataSourceBehavior behavior) => behavior switch
+ {
+ SendToChatDataSourceBehavior.NO_DATA_SOURCES => "Use no data sources, when sending an assistant result to a chat",
+ SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => "Apply standard chat data source options, when sending an assistant result to a chat",
+
+ _ => "Unknown behavior",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs
index 7942ab7b..5d36a1bc 100644
--- a/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs
@@ -2,15 +2,12 @@ namespace AIStudio.Settings.DataModel;
public static class ThemesExtensions
{
- public static string GetName(this Themes theme)
+ public static string GetName(this Themes theme) => theme switch
{
- return theme switch
- {
- Themes.SYSTEM => "Synchronized with the operating system settings",
- Themes.LIGHT => "Always use light theme",
- Themes.DARK => "Always use dark theme",
-
- _ => "Unknown setting",
- };
- }
+ Themes.SYSTEM => "Synchronized with the operating system settings",
+ Themes.LIGHT => "Always use light theme",
+ Themes.DARK => "Always use dark theme",
+
+ _ => "Unknown setting",
+ };
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/IDataSource.cs b/app/MindWork AI Studio/Settings/IDataSource.cs
new file mode 100644
index 00000000..7ee47e1c
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/IDataSource.cs
@@ -0,0 +1,51 @@
+using System.Text.Json.Serialization;
+
+using AIStudio.Chat;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.RAG;
+
+namespace AIStudio.Settings;
+
+///
+/// The common interface for all data sources.
+///
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type_discriminator")]
+[JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))]
+[JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))]
+[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))]
+public interface IDataSource
+{
+ ///
+ /// The number of the data source.
+ ///
+ public uint Num { get; init; }
+
+ ///
+ /// The unique identifier of the data source.
+ ///
+ public string Id { get; init; }
+
+ ///
+ /// The name of the data source.
+ ///
+ public string Name { get; init; }
+
+ ///
+ /// Which type of data source is this?
+ ///
+ public DataSourceType Type { get; init; }
+
+ ///
+ /// Which data security policy is applied to this data source?
+ ///
+ public DataSourceSecurity SecurityPolicy { get; init; }
+
+ ///
+ /// Perform the data retrieval process.
+ ///
+ /// The last prompt from the chat.
+ /// The chat thread.
+ /// The cancellation token.
+ /// The retrieved data context.
+ public Task> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/IERIDataSource.cs b/app/MindWork AI Studio/Settings/IERIDataSource.cs
new file mode 100644
index 00000000..55138978
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/IERIDataSource.cs
@@ -0,0 +1,37 @@
+using AIStudio.Assistants.ERI;
+using AIStudio.Tools.ERIClient.DataModel;
+
+namespace AIStudio.Settings;
+
+public interface IERIDataSource : IExternalDataSource
+{
+ ///
+ /// The hostname of the ERI server.
+ ///
+ public string Hostname { get; init; }
+
+ ///
+ /// The port of the ERI server.
+ ///
+ public int Port { get; init; }
+
+ ///
+ /// The authentication method to use.
+ ///
+ public AuthMethod AuthMethod { get; init; }
+
+ ///
+ /// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
+ ///
+ public string Username { get; init; }
+
+ ///
+ /// The ERI specification to use.
+ ///
+ public ERIVersion Version { get; init; }
+
+ ///
+ /// The ID of the selected retrieval process.
+ ///
+ public string SelectedRetrievalId { get; init; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/IExternalDataSource.cs b/app/MindWork AI Studio/Settings/IExternalDataSource.cs
new file mode 100644
index 00000000..8a7c067c
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/IExternalDataSource.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Settings;
+
+public interface IExternalDataSource : IDataSource, ISecretId
+{
+ #region Implementation of ISecretId
+
+ [JsonIgnore]
+ string ISecretId.SecretId => this.Id;
+
+ [JsonIgnore]
+ string ISecretId.SecretName => this.Name;
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/IInternalDataSource.cs b/app/MindWork AI Studio/Settings/IInternalDataSource.cs
new file mode 100644
index 00000000..0ffa7dea
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/IInternalDataSource.cs
@@ -0,0 +1,9 @@
+namespace AIStudio.Settings;
+
+public interface IInternalDataSource : IDataSource
+{
+ ///
+ /// The unique identifier of the embedding method used by this internal data source.
+ ///
+ public string EmbeddingId { get; init; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs
index 0fa3e184..ec74cab8 100644
--- a/app/MindWork AI Studio/Settings/SettingsManager.cs
+++ b/app/MindWork AI Studio/Settings/SettingsManager.cs
@@ -1,8 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
// ReSharper disable NotAccessedPositionalProperty.Local
@@ -141,7 +143,10 @@ public sealed class SettingsManager(ILogger logger)
return minimumLevel;
}
- public Provider GetPreselectedProvider(Tools.Components component, string? chatProviderId = null)
+ public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id);
+
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
+ public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
{
var minimumLevel = this.GetMinimumConfidenceLevel(component);
@@ -149,17 +154,43 @@ public sealed class SettingsManager(ILogger logger)
if (this.ConfigurationData.Providers.Count == 1 && this.ConfigurationData.Providers[0].UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
return this.ConfigurationData.Providers[0];
- // When there is a chat provider, and it has a confidence level that is high enough, we return it:
- if (chatProviderId is not null && !string.IsNullOrWhiteSpace(chatProviderId))
+ // Is there a current provider with a sufficiently high confidence level?
+ Provider currentProvider = default;
+ if (currentProviderId is not null && !string.IsNullOrWhiteSpace(currentProviderId))
{
- var chatProvider = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == chatProviderId);
- if (chatProvider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
- return chatProvider;
+ var currentProviderProbe = this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == currentProviderId);
+ if (currentProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
+ currentProvider = currentProviderProbe;
}
- // When there is a component-preselected provider, and it has a confidence level that is high enough, we return it:
- var preselectedProvider = component.PreselectedProvider(this);
- if(preselectedProvider != default && preselectedProvider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
+ // Is there a component-preselected provider with a sufficiently high confidence level?
+ Provider preselectedProvider = default;
+ var preselectedProviderProbe = component.PreselectedProvider(this);
+ if(preselectedProviderProbe != default && preselectedProviderProbe.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel)
+ preselectedProvider = preselectedProviderProbe;
+
+ //
+ // Case: The preselected provider should be used before the current provider,
+ // and the preselected provider is available and has a confidence level
+ // that is high enough.
+ //
+ if(usePreselectionBeforeCurrentProvider && preselectedProvider != default)
+ return preselectedProvider;
+
+ //
+ // Case: The current provider is available and has a confidence level that is
+ // high enough.
+ //
+ if(currentProvider != default)
+ return currentProvider;
+
+ //
+ // Case: The current provider should be used before the preselected provider,
+ // but the current provider is not available or does not have a confidence
+ // level that is high enough. The preselected provider is available and
+ // has a confidence level that is high enough.
+ //
+ if(preselectedProvider != default)
return preselectedProvider;
// When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it:
@@ -183,11 +214,19 @@ public sealed class SettingsManager(ILogger logger)
switch (this.ConfigurationData.LLMProviders.ConfidenceScheme)
{
+ case ConfidenceSchemes.TRUST_ALL:
+ return llmProvider switch
+ {
+ LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
+
+ _ => ConfidenceLevel.MEDIUM,
+ };
+
case ConfidenceSchemes.TRUST_USA_EUROPE:
return llmProvider switch
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
- LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
+ LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
};
@@ -196,8 +235,10 @@ public sealed class SettingsManager(ILogger logger)
return llmProvider switch
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
- LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
LLMProviders.MISTRAL => ConfidenceLevel.LOW,
+ LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW,
+ LLMProviders.GWDG => ConfidenceLevel.LOW,
+ LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
_ => ConfidenceLevel.MEDIUM,
};
@@ -206,8 +247,18 @@ public sealed class SettingsManager(ILogger logger)
return llmProvider switch
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
- LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM,
+ LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM,
+ LLMProviders.GWDG => ConfidenceLevel.MEDIUM,
+
+ _ => ConfidenceLevel.LOW,
+ };
+
+ case ConfidenceSchemes.TRUST_ASIA:
+ return llmProvider switch
+ {
+ LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
+ LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM,
_ => ConfidenceLevel.LOW,
};
@@ -216,7 +267,6 @@ public sealed class SettingsManager(ILogger logger)
return llmProvider switch
{
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
- LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
_ => ConfidenceLevel.VERY_LOW,
};
diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs
index da2a5eef..98482ceb 100644
--- a/app/MindWork AI Studio/Settings/SettingsMigrations.cs
+++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs
@@ -84,7 +84,7 @@ public static class SettingsMigrations
{
Version = Version.V2,
- Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = "" }).ToList(),
+ Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = string.Empty }).ToList(),
EnableSpellchecking = previousData.EnableSpellchecking,
IsSavingEnergy = previousData.IsSavingEnergy,
diff --git a/app/MindWork AI Studio/Tools/AllowedSelectedDataSources.cs b/app/MindWork AI Studio/Tools/AllowedSelectedDataSources.cs
new file mode 100644
index 00000000..1aed9d1c
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/AllowedSelectedDataSources.cs
@@ -0,0 +1,13 @@
+using AIStudio.Settings;
+
+namespace AIStudio.Tools;
+
+///
+/// Contains both the allowed and selected data sources.
+///
+///
+/// The selected data sources are a subset of the allowed data sources.
+///
+/// The allowed data sources.
+/// The selected data sources, which are a subset of the allowed data sources.
+public readonly record struct AllowedSelectedDataSources(IReadOnlyList AllowedDataSources, IReadOnlyList SelectedDataSources);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs
new file mode 100644
index 00000000..4d11017a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs
@@ -0,0 +1,16 @@
+using AIStudio.Tools.ERIClient.DataModel;
+
+namespace AIStudio.Tools;
+
+public static class AuthMethodsV1Extensions
+{
+ public static string DisplayName(this AuthMethod authMethod) => authMethod switch
+ {
+ AuthMethod.NONE => "None",
+ AuthMethod.USERNAME_PASSWORD => "Username & Password",
+ AuthMethod.KERBEROS => "SSO (Kerberos)",
+ AuthMethod.TOKEN => "Access Token",
+
+ _ => "Unknown authentication method",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/CommonTools.cs b/app/MindWork AI Studio/Tools/CommonTools.cs
new file mode 100644
index 00000000..26150880
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/CommonTools.cs
@@ -0,0 +1,22 @@
+using System.Text;
+
+namespace AIStudio.Tools;
+
+public static class CommonTools
+{
+ ///
+ /// Get all the values (the names) of an enum as a string, separated by commas.
+ ///
+ /// The enum type to get the values of.
+ /// The values to exclude from the result.
+ /// The values of the enum as a string, separated by commas.
+ public static string GetAllEnumValues(params TEnum[] exceptions) where TEnum : struct, Enum
+ {
+ var sb = new StringBuilder();
+ foreach (var value in Enum.GetValues())
+ if(!exceptions.Contains(value))
+ sb.Append(value).Append(", ");
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Components.cs b/app/MindWork AI Studio/Tools/Components.cs
index c910b7ce..d65a5c5d 100644
--- a/app/MindWork AI Studio/Tools/Components.cs
+++ b/app/MindWork AI Studio/Tools/Components.cs
@@ -20,4 +20,9 @@ public enum Components
ERI_ASSISTANT,
CHAT,
+ APP_SETTINGS,
+
+ AGENT_TEXT_CONTENT_CLEANER,
+ AGENT_DATA_SOURCE_SELECTION,
+ AGENT_RETRIEVAL_CONTEXT_VALIDATION,
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs
index 0f8107cc..6112debb 100644
--- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs
+++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
using AIStudio.Provider;
using AIStudio.Settings;
@@ -10,6 +12,11 @@ public static class ComponentsExtensions
Components.NONE => false,
Components.ERI_ASSISTANT => false,
Components.BIAS_DAY_ASSISTANT => false,
+ Components.APP_SETTINGS => false,
+
+ Components.AGENT_TEXT_CONTENT_CLEANER => false,
+ Components.AGENT_DATA_SOURCE_SELECTION => false,
+ Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false,
_ => true,
};
@@ -75,6 +82,7 @@ public static class ComponentsExtensions
_ => default,
};
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : default,
@@ -94,6 +102,10 @@ public static class ComponentsExtensions
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default,
+ Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : default,
+ Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : default,
+ Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : default,
+
_ => default,
};
diff --git a/app/MindWork AI Studio/Tools/ConfidenceSchemes.cs b/app/MindWork AI Studio/Tools/ConfidenceSchemes.cs
index 97d4f4d6..b5085284 100644
--- a/app/MindWork AI Studio/Tools/ConfidenceSchemes.cs
+++ b/app/MindWork AI Studio/Tools/ConfidenceSchemes.cs
@@ -2,11 +2,13 @@ namespace AIStudio.Tools;
public enum ConfidenceSchemes
{
- TRUST_USA_EUROPE = 0,
- TRUST_USA = 1,
- TRUST_EUROPE = 2,
+ TRUST_ALL,
+ TRUST_USA_EUROPE,
+ TRUST_USA,
+ TRUST_EUROPE,
+ TRUST_ASIA,
- LOCAL_TRUST_ONLY = 3,
+ LOCAL_TRUST_ONLY,
CUSTOM = 10_000,
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs b/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs
index d36993f1..035be948 100644
--- a/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs
+++ b/app/MindWork AI Studio/Tools/ConfidenceSchemesExtensions.cs
@@ -4,9 +4,11 @@ public static class ConfidenceSchemesExtensions
{
public static string GetListDescription(this ConfidenceSchemes scheme) => scheme switch
{
+ ConfidenceSchemes.TRUST_ALL => "Trust all LLM providers",
ConfidenceSchemes.TRUST_USA_EUROPE => "Trust LLM providers from the USA and Europe",
ConfidenceSchemes.TRUST_USA => "Trust LLM providers from the USA",
ConfidenceSchemes.TRUST_EUROPE => "Trust LLM providers from Europe",
+ ConfidenceSchemes.TRUST_ASIA => "Trust LLM providers from Asia",
ConfidenceSchemes.LOCAL_TRUST_ONLY => "Trust only local LLM providers",
ConfidenceSchemes.CUSTOM => "Configure your own confidence scheme",
diff --git a/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs b/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs
new file mode 100644
index 00000000..70adcab7
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/DirectoryInfoExtensions.cs
@@ -0,0 +1,66 @@
+namespace AIStudio.Tools;
+
+public static class DirectoryInfoExtensions
+{
+ private static readonly EnumerationOptions ENUMERATION_OPTIONS = new()
+ {
+ IgnoreInaccessible = true,
+ RecurseSubdirectories = true,
+ ReturnSpecialDirectories = false,
+ };
+
+ ///
+ /// Determines the size of the directory and all its subdirectories, as well as the number of files. When desired,
+ /// it can report the found files up to a certain limit.
+ ///
+ ///
+ /// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number
+ /// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able
+ /// to cancel slow operations, e.g., when the directory is on a slow network drive.
+ ///
+ /// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation.
+ ///
+ /// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the
+ /// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync
+ /// method to update the UI from a background thread.
+ ///
+ /// The root directory to determine the size of.
+ /// The callback to report the current total size of the directory.
+ /// The callback to report the current number of files found.
+ /// The callback to report the next file found. The file name is relative to the root directory.
+ /// The maximum number of files to report. A negative value reports all files.
+ /// The callback to report that the operation is done.
+ /// The cancellation token to stop the operation.
+ public static async Task DetermineContentSize(this DirectoryInfo directoryInfo, Action reportCurrentTotalSize, Action reportCurrentNumFiles, Action reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default)
+ {
+ var rootDirectoryLen = directoryInfo.FullName.Length;
+ long totalSize = 0;
+ long numFiles = 0;
+
+ await Task.Factory.StartNew(() => {
+ foreach (var file in directoryInfo.EnumerateFiles("*", ENUMERATION_OPTIONS))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ totalSize += file.Length;
+ numFiles++;
+
+ if (numFiles % 100 == 0)
+ {
+ reportCurrentTotalSize(totalSize);
+ reportCurrentNumFiles(numFiles);
+ }
+
+ if (reportMaxFiles < 0 || numFiles <= reportMaxFiles)
+ reportNextFile(file.FullName[rootDirectoryLen..]);
+ }
+ }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+
+ reportCurrentTotalSize(totalSize);
+ reportCurrentNumFiles(numFiles);
+
+ if(done is not null)
+ done();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/APIResponse.cs b/app/MindWork AI Studio/Tools/ERIClient/APIResponse.cs
new file mode 100644
index 00000000..1f82914c
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/APIResponse.cs
@@ -0,0 +1,19 @@
+namespace AIStudio.Tools.ERIClient;
+
+public sealed class APIResponse
+{
+ ///
+ /// Was the API call successful?
+ ///
+ public bool Successful { get; set; }
+
+ ///
+ /// When the API call was not successful, this will contain the error message.
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// The data returned by the API call.
+ ///
+ public T? Data { get; set; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthField.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthField.cs
new file mode 100644
index 00000000..89727430
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthField.cs
@@ -0,0 +1,13 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// An authentication field.
+///
+public enum AuthField
+{
+ NONE,
+ USERNAME,
+ PASSWORD,
+ TOKEN,
+ KERBEROS_TICKET,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthFieldMapping.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthFieldMapping.cs
new file mode 100644
index 00000000..e11c3d9f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthFieldMapping.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// The mapping between an AuthField and the field name in the authentication request.
+///
+/// The AuthField that is mapped to the field name.
+/// The field name in the authentication request.
+public record AuthFieldMapping(AuthField AuthField, string FieldName);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthMethod.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthMethod.cs
new file mode 100644
index 00000000..7494ce9b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthMethod.cs
@@ -0,0 +1,9 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+public enum AuthMethod
+{
+ NONE,
+ KERBEROS,
+ USERNAME_PASSWORD,
+ TOKEN,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthResponse.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthResponse.cs
new file mode 100644
index 00000000..cdc325dd
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthResponse.cs
@@ -0,0 +1,9 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// The response to an authentication request.
+///
+/// True, when the authentication was successful.
+/// The token to use for further requests.
+/// When the authentication was not successful, this contains the reason.
+public readonly record struct AuthResponse(bool Success, string? Token, string? Message);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthScheme.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthScheme.cs
new file mode 100644
index 00000000..bde0175b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/AuthScheme.cs
@@ -0,0 +1,9 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Describes one authentication scheme for this data source.
+///
+/// The method used for authentication, e.g., "API Key," "Username/Password," etc.
+/// A list of field mappings for the authentication method. The client must know,
+/// e.g., how the password field is named in the request.
+public readonly record struct AuthScheme(AuthMethod AuthMethod, List AuthFieldMappings);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ChatThread.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ChatThread.cs
new file mode 100644
index 00000000..8d6a0983
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ChatThread.cs
@@ -0,0 +1,7 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// A chat thread, which is a list of content blocks.
+///
+/// The content blocks in this chat thread.
+public readonly record struct ChatThread(List ContentBlocks);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentBlock.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentBlock.cs
new file mode 100644
index 00000000..0a46d3b5
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentBlock.cs
@@ -0,0 +1,12 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// A block of content of a chat thread.
+///
+///
+/// Images and other media are base64 encoded.
+///
+/// The content of the block. Remember that images and other media are base64 encoded.
+/// The role of the content in the chat thread.
+/// The type of the content, e.g., text, image, video, etc.
+public readonly record struct ContentBlock(string Content, Role Role, ContentType Type);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentType.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentType.cs
new file mode 100644
index 00000000..14203728
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ContentType.cs
@@ -0,0 +1,16 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// The type of content.
+///
+public enum ContentType
+{
+ NONE,
+ UNKNOWN,
+
+ TEXT,
+ IMAGE,
+ VIDEO,
+ AUDIO,
+ SPEECH,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/Context.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Context.cs
new file mode 100644
index 00000000..fb5c13f5
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Context.cs
@@ -0,0 +1,27 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Matching context returned by the data source as a result of a retrieval request.
+///
+/// The name of the source, e.g., a document name, database name,
+/// collection name, etc.
+/// What are the contents of the source? For example, is it a
+/// dictionary, a book chapter, business concept, a paper, etc.
+/// The path to the content, e.g., a URL, a file path, a path in a
+/// graph database, etc.
+/// The type of the content, e.g., text, image, video, audio, speech, etc.
+/// The content that matched the user prompt. For text, you
+/// return the matched text and, e.g., three words before and after it.
+/// The surrounding content of the matched content.
+/// For text, you may return, e.g., one sentence or paragraph before and after
+/// the matched content.
+/// Links to related content, e.g., links to Wikipedia articles,
+/// links to sources, etc.
+public readonly record struct Context(
+ string Name,
+ string Category,
+ string? Path,
+ ContentType Type,
+ string MatchedContent,
+ string[] SurroundingContent,
+ string[] Links);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/DataSourceInfo.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/DataSourceInfo.cs
new file mode 100644
index 00000000..07a8f92e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/DataSourceInfo.cs
@@ -0,0 +1,9 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Information about the data source.
+///
+/// The name of the data source, e.g., "Internal Organization Documents."
+/// A short description of the data source. What kind of data does it contain?
+/// What is the data source used for?
+public readonly record struct DataSourceInfo(string Name, string Description);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/EmbeddingInfo.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/EmbeddingInfo.cs
new file mode 100644
index 00000000..47285126
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/EmbeddingInfo.cs
@@ -0,0 +1,20 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Represents information about the used embedding for this data source. The purpose of this information is to give the
+/// interested user an idea of what kind of embedding is used and what it does.
+///
+/// What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word
+/// Embedding," "Graph Embedding," etc.
+/// Name the embedding used. This can be a library, a framework, or the name of the used
+/// algorithm.
+/// A short description of the embedding. Describe what the embedding is doing.
+/// Describe when the embedding is used. For example, when the user prompt contains certain
+/// keywords, or anytime?
+/// A link to the embedding's documentation or the source code. Might be null.
+public readonly record struct EmbeddingInfo(
+ string EmbeddingType,
+ string EmbeddingName,
+ string Description,
+ string UsedWhen,
+ string? Link);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderType.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderType.cs
new file mode 100644
index 00000000..008811f8
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderType.cs
@@ -0,0 +1,22 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Known types of providers that can process data.
+///
+public enum ProviderType
+{
+ ///
+ /// The related data is not allowed to be sent to any provider.
+ ///
+ NONE,
+
+ ///
+ /// The related data can be sent to any provider.
+ ///
+ ANY,
+
+ ///
+ /// The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally.
+ ///
+ SELF_HOSTED,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs
new file mode 100644
index 00000000..ecdbcc19
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/ProviderTypeExtensions.cs
@@ -0,0 +1,13 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+public static class ProviderTypeExtensions
+{
+ public static string Explain(this ProviderType providerType) => providerType switch
+ {
+ ProviderType.NONE => "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.",
+ ProviderType.ANY => "The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted).",
+ ProviderType.SELF_HOSTED => "The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed.",
+
+ _ => "Unknown configuration. This data source cannot be used at the moment.",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalInfo.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalInfo.cs
new file mode 100644
index 00000000..cdd71a6b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalInfo.cs
@@ -0,0 +1,20 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Information about a retrieval process, which this data source implements.
+///
+/// A unique identifier for the retrieval process. This can be a GUID, a unique name, or an increasing integer.
+/// The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval".
+/// A short description of the retrieval process. What kind of retrieval process is it?
+/// A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.
+/// A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
+/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the
+/// expected type and range, e.g., 0.0 to 1.0 for a float parameter.
+/// A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.
+public readonly record struct RetrievalInfo(
+ string Id,
+ string Name,
+ string Description,
+ string? Link,
+ Dictionary? ParametersDescription,
+ List Embeddings);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalRequest.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalRequest.cs
new file mode 100644
index 00000000..abeac50b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/RetrievalRequest.cs
@@ -0,0 +1,25 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// The retrieval request sent by AI Studio.
+///
+///
+/// Images and other media are base64 encoded.
+///
+/// The latest user prompt that AI Studio received.
+/// The type of the latest user prompt, e.g., text, image, etc.
+/// The chat thread that the user is currently in.
+/// Optional. The ID of the retrieval process that the data source should use.
+/// When null, the data source chooses an appropriate retrieval process. Selecting a retrieval process is optional
+/// for AI Studio users. Most users do not specify a retrieval process.
+/// A dictionary of parameters that the data source should use for the retrieval process.
+/// Although each parameter will be sent as a string, the retrieval process specifies the expected type and range.
+/// The maximum number of matches that the data source should return. AI Studio uses
+/// any value below 1 to indicate that the data source should return as many matches as appropriate.
+public readonly record struct RetrievalRequest(
+ string LatestUserPrompt,
+ ContentType LatestUserPromptType,
+ ChatThread Thread,
+ string? RetrievalProcessId,
+ Dictionary? Parameters,
+ int MaxMatches);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/Role.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Role.cs
new file mode 100644
index 00000000..a8f3fdf8
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/Role.cs
@@ -0,0 +1,15 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Possible roles of any chat thread.
+///
+public enum Role
+{
+ NONE,
+ UNKNOWN,
+
+ SYSTEM,
+ USER,
+ AI,
+ AGENT,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/DataModel/SecurityRequirements.cs b/app/MindWork AI Studio/Tools/ERIClient/DataModel/SecurityRequirements.cs
new file mode 100644
index 00000000..aa4df1f5
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/DataModel/SecurityRequirements.cs
@@ -0,0 +1,7 @@
+namespace AIStudio.Tools.ERIClient.DataModel;
+
+///
+/// Represents the security requirements for this data source.
+///
+/// Which provider types are allowed to process the data?
+public readonly record struct SecurityRequirements(ProviderType AllowedProviderType);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs
new file mode 100644
index 00000000..5458bedc
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientBase.cs
@@ -0,0 +1,41 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using AIStudio.Settings;
+
+namespace AIStudio.Tools.ERIClient;
+
+public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
+{
+ protected readonly IERIDataSource dataSource = dataSource;
+
+ protected static readonly JsonSerializerOptions JSON_OPTIONS = new()
+ {
+ WriteIndented = true,
+ AllowTrailingCommas = true,
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
+ Converters =
+ {
+ new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
+ }
+ };
+
+ protected readonly HttpClient httpClient = new()
+ {
+ BaseAddress = new Uri($"{dataSource.Hostname}:{dataSource.Port}"),
+ };
+
+ protected string securityToken = string.Empty;
+
+ #region Implementation of IDisposable
+
+ public void Dispose()
+ {
+ this.httpClient.Dispose();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientFactory.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientFactory.cs
new file mode 100644
index 00000000..0beb0f64
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientFactory.cs
@@ -0,0 +1,14 @@
+using AIStudio.Assistants.ERI;
+using AIStudio.Settings;
+
+namespace AIStudio.Tools.ERIClient;
+
+public static class ERIClientFactory
+{
+ public static IERIClient? Get(ERIVersion version, IERIDataSource dataSource) => version switch
+ {
+ ERIVersion.V1 => new ERIClientV1(dataSource),
+
+ _ => null
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs
new file mode 100644
index 00000000..178849dd
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs
@@ -0,0 +1,492 @@
+using System.Text;
+using System.Text.Json;
+
+using AIStudio.Settings;
+using AIStudio.Tools.ERIClient.DataModel;
+using AIStudio.Tools.Services;
+
+namespace AIStudio.Tools.ERIClient;
+
+public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource), IERIClient
+{
+ #region Implementation of IERIClient
+
+ public async Task>> GetAuthMethodsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var response = await this.httpClient.GetAsync("/auth/methods", cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the authentication methods: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
+ };
+ }
+
+ var authMethods = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken);
+ if (authMethods is null)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
+ };
+ }
+
+ return new()
+ {
+ Successful = true,
+ Data = authMethods
+ };
+ }
+ catch (TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the authentication methods: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the authentication methods due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ public async Task> AuthenticateAsync(RustService rustService, string? temporarySecret = null, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var authMethod = this.dataSource.AuthMethod;
+ var username = this.dataSource.Username;
+ switch (this.dataSource.AuthMethod)
+ {
+ case AuthMethod.NONE:
+ using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
+ {
+ using var noneAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!noneAuthResponse.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to authenticate with the ERI server. Code: {noneAuthResponse.StatusCode}, Reason: {noneAuthResponse.ReasonPhrase}"
+ };
+ }
+
+ var noneAuthResult = await noneAuthResponse.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken);
+ if(noneAuthResult == default)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to authenticate with the ERI server: the response was invalid."
+ };
+ }
+
+ this.securityToken = noneAuthResult.Token ?? string.Empty;
+ return new()
+ {
+ Successful = true,
+ Data = noneAuthResult
+ };
+ }
+
+ case AuthMethod.USERNAME_PASSWORD:
+ string password;
+ if (string.IsNullOrWhiteSpace(temporarySecret))
+ {
+ var passwordResponse = await rustService.GetSecret(this.dataSource);
+ if (!passwordResponse.Success)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the password."
+ };
+ }
+
+ password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION);
+ }
+ else
+ password = temporarySecret;
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
+ {
+ // We must send both values inside the header. The username field is named 'user'.
+ // The password field is named 'password'.
+ request.Headers.Add("user", username);
+ request.Headers.Add("password", password);
+
+ using var usernamePasswordAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!usernamePasswordAuthResponse.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to authenticate with the ERI server. Code: {usernamePasswordAuthResponse.StatusCode}, Reason: {usernamePasswordAuthResponse.ReasonPhrase}"
+ };
+ }
+
+ var usernamePasswordAuthResult = await usernamePasswordAuthResponse.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken);
+ if(usernamePasswordAuthResult == default)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to authenticate with the server: the response was invalid."
+ };
+ }
+
+ this.securityToken = usernamePasswordAuthResult.Token ?? string.Empty;
+ return new()
+ {
+ Successful = true,
+ Data = usernamePasswordAuthResult
+ };
+ }
+
+ case AuthMethod.TOKEN:
+ string token;
+ if (string.IsNullOrWhiteSpace(temporarySecret))
+ {
+ var tokenResponse = await rustService.GetSecret(this.dataSource);
+ if (!tokenResponse.Success)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the access token."
+ };
+ }
+
+ token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION);
+ }
+ else
+ token = temporarySecret;
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
+ {
+ request.Headers.Add("Authorization", $"Bearer {token}");
+
+ using var tokenAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!tokenAuthResponse.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to authenticate with the ERI server. Code: {tokenAuthResponse.StatusCode}, Reason: {tokenAuthResponse.ReasonPhrase}"
+ };
+ }
+
+ var tokenAuthResult = await tokenAuthResponse.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken);
+ if(tokenAuthResult == default)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to authenticate with the ERI server: the response was invalid."
+ };
+ }
+
+ this.securityToken = tokenAuthResult.Token ?? string.Empty;
+ return new()
+ {
+ Successful = true,
+ Data = tokenAuthResult
+ };
+ }
+
+ default:
+ this.securityToken = string.Empty;
+ return new()
+ {
+ Successful = false,
+ Message = "The authentication method is not supported yet."
+ };
+ }
+ }
+ catch(TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to authenticate with the ERI server: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to authenticate with the ERI server due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ public async Task> GetDataSourceInfoAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, "/dataSource");
+ request.Headers.Add("token", this.securityToken);
+
+ using var response = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!response.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the data source information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
+ };
+ }
+
+ var dataSourceInfo = await response.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken);
+ if(dataSourceInfo == default)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the data source information: the ERI server did not return a valid response."
+ };
+ }
+
+ return new()
+ {
+ Successful = true,
+ Data = dataSourceInfo
+ };
+ }
+ catch(TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the data source information: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the data source information due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ public async Task>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, "/embedding/info");
+ request.Headers.Add("token", this.securityToken);
+
+ using var response = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!response.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the embedding information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
+ };
+ }
+
+ var embeddingInfo = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken);
+ if(embeddingInfo is null)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the embedding information: the ERI server did not return a valid response."
+ };
+ }
+
+ return new()
+ {
+ Successful = true,
+ Data = embeddingInfo
+ };
+ }
+ catch(TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the embedding information: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the embedding information due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ public async Task>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, "/retrieval/info");
+ request.Headers.Add("token", this.securityToken);
+
+ using var response = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!response.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the retrieval information: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
+ };
+ }
+
+ var retrievalInfo = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken);
+ if(retrievalInfo is null)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the retrieval information: the ERI server did not return a valid response."
+ };
+ }
+
+ return new()
+ {
+ Successful = true,
+ Data = retrievalInfo
+ };
+ }
+ catch(TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the retrieval information: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the retrieval information due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ public async Task>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/retrieval");
+ requestMessage.Headers.Add("token", this.securityToken);
+
+ using var content = new StringContent(JsonSerializer.Serialize(request, JSON_OPTIONS), Encoding.UTF8, "application/json");
+ requestMessage.Content = content;
+
+ using var response = await this.httpClient.SendAsync(requestMessage, cancellationToken);
+ if(!response.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to execute the retrieval request: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
+ };
+ }
+
+ var contexts = await response.Content.ReadFromJsonAsync>(JSON_OPTIONS, cancellationToken);
+ if(contexts is null)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to execute the retrieval request: the ERI server did not return a valid response."
+ };
+ }
+
+ return new()
+ {
+ Successful = true,
+ Data = contexts
+ };
+ }
+ catch(TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to execute the retrieval request: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to execute the retrieval request due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ public async Task> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, "/security/requirements");
+ request.Headers.Add("token", this.securityToken);
+
+ using var response = await this.httpClient.SendAsync(request, cancellationToken);
+ if(!response.IsSuccessStatusCode)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the security requirements: there was an issue communicating with the ERI server. Code: {response.StatusCode}, Reason: {response.ReasonPhrase}"
+ };
+ }
+
+ var securityRequirements = await response.Content.ReadFromJsonAsync(JSON_OPTIONS, cancellationToken);
+ if(securityRequirements == default)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the security requirements: the ERI server did not return a valid response."
+ };
+ }
+
+ return new()
+ {
+ Successful = true,
+ Data = securityRequirements
+ };
+ }
+ catch(TaskCanceledException)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
+ };
+ }
+ catch (Exception e)
+ {
+ return new()
+ {
+ Successful = false,
+ Message = $"Failed to retrieve the security requirements due to an exception: {e.Message}"
+ };
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ERIClient/IERIClient.cs b/app/MindWork AI Studio/Tools/ERIClient/IERIClient.cs
new file mode 100644
index 00000000..80d8c4f7
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ERIClient/IERIClient.cs
@@ -0,0 +1,62 @@
+using AIStudio.Tools.ERIClient.DataModel;
+using AIStudio.Tools.Services;
+
+namespace AIStudio.Tools.ERIClient;
+
+public interface IERIClient : IDisposable
+{
+ ///
+ /// Retrieves the available authentication methods from the ERI server.
+ ///
+ ///
+ /// No authentication is required to retrieve the available authentication methods.
+ ///
+ /// The cancellation token.
+ /// The available authentication methods.
+ public Task>> GetAuthMethodsAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Authenticate the user to the ERI server.
+ ///
+ /// The Rust service.
+ /// The temporary secret when adding a new data source, and the secret is not yet stored in the OS.
+ /// The cancellation token.
+ /// The authentication response.
+ public Task> AuthenticateAsync(RustService rustService, string? temporarySecret = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the data source information from the ERI server.
+ ///
+ /// The cancellation token.
+ /// The data source information.
+ public Task> GetDataSourceInfoAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the embedding information from the ERI server.
+ ///
+ /// The cancellation token.
+ /// A list of embedding information.
+ public Task>> GetEmbeddingInfoAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the retrieval information from the ERI server.
+ ///
+ /// The cancellation token.
+ /// A list of retrieval information.
+ public Task>> GetRetrievalInfoAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Executes a retrieval request on the ERI server.
+ ///
+ /// The retrieval request.
+ /// The cancellation token.
+ /// The retrieved contexts to use for augmentation and generation.
+ public Task>> ExecuteRetrievalAsync(RetrievalRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the security requirements from the ERI server.
+ ///
+ /// The cancellation token.
+ /// The security requirements.
+ public Task> GetSecurityRequirementsAsync(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs
index 37a855fd..38235c8e 100644
--- a/app/MindWork AI Studio/Tools/Event.cs
+++ b/app/MindWork AI Studio/Tools/Event.cs
@@ -8,6 +8,7 @@ public enum Event
STATE_HAS_CHANGED,
CONFIGURATION_CHANGED,
COLOR_THEME_CHANGED,
+ PLUGINS_RELOADED,
// Update events:
USER_SEARCH_FOR_UPDATE,
@@ -23,6 +24,9 @@ public enum Event
WORKSPACE_LOADED_CHAT_CHANGED,
WORKSPACE_TOGGLE_OVERLAY,
+ // RAG events:
+ RAG_AUTO_DATA_SOURCES_SELECTED,
+
// Send events:
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
SEND_TO_ICON_FINDER_ASSISTANT,
diff --git a/app/MindWork AI Studio/Tools/FileInfoExtensions.cs b/app/MindWork AI Studio/Tools/FileInfoExtensions.cs
new file mode 100644
index 00000000..c3002624
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/FileInfoExtensions.cs
@@ -0,0 +1,17 @@
+namespace AIStudio.Tools;
+
+public static class FileInfoExtensions
+{
+ ///
+ /// Returns the file size in human-readable format.
+ ///
+ /// The file info object.
+ /// The file size in human-readable format.
+ public static string FileSize(this FileInfo fileInfo)
+ {
+ if (!fileInfo.Exists)
+ return "N/A";
+
+ return fileInfo.Length.FileSize();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/IConfidence.cs b/app/MindWork AI Studio/Tools/IConfidence.cs
new file mode 100644
index 00000000..dff9343a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/IConfidence.cs
@@ -0,0 +1,16 @@
+namespace AIStudio.Tools;
+
+///
+/// A contract for data classes with a confidence value.
+///
+///
+/// Using this confidence contract allows us to provide
+/// algorithms based on confidence values.
+///
+public interface IConfidence
+{
+ ///
+ /// How confident is the AI in this task or decision?
+ ///
+ public float Confidence { get; init; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs b/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs
new file mode 100644
index 00000000..f6f15bfd
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/IConfidenceExtensions.cs
@@ -0,0 +1,101 @@
+namespace AIStudio.Tools;
+
+public static class IConfidenceExtensions
+{
+ public static TargetWindow DetermineTargetWindow(this IReadOnlyList items, TargetWindowStrategy strategy, int numMaximumItems = 30) where T : IConfidence
+ {
+ switch (strategy)
+ {
+ case TargetWindowStrategy.A_FEW_GOOD_ONES:
+ return new(1, 2, 3, 0f);
+
+ case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING:
+ var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f);
+ if(numItemsBetterThanGuessing < 3)
+ return new(1, 2, 3, 0.5f);
+
+ // We want the top 10% of items better than guessing:
+ var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f);
+
+ // When these 10% are just a few items, we take them all:
+ if (numTop10Percent <= 10)
+ {
+ var diff = numItemsBetterThanGuessing - numTop10Percent;
+ var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f);
+ return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f);
+ }
+
+ // Let's define the size of the window:
+ const int MIN_NUM_ITEMS = 3;
+ var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent);
+ windowMin = Math.Min(windowMin, numMaximumItems - 1);
+ var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3);
+ var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent));
+ var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize);
+ return new(totalMin, windowMin, windowMax, 0.5f);
+
+ case TargetWindowStrategy.NONE:
+ default:
+ return new(-1, -1, -1, 0f);
+ }
+ }
+
+ ///
+ /// Determine the optimal confidence threshold for a list of items
+ /// in order to match a target window of number of items.
+ ///
+ /// The list of confidence items to analyze.
+ /// The target window for the number of items.
+ /// The maximum number of steps to search for the threshold.
+ /// The type of items in the list.
+ /// The confidence threshold.
+ public static float GetConfidenceThreshold(this IReadOnlyList items, TargetWindow targetWindow, int maxSteps = 10) where T : IConfidence
+ {
+ if(!targetWindow.IsValid())
+ {
+ var logger = Program.SERVICE_PROVIDER.GetService>()!;
+ logger.LogWarning("The target window is invalid. Returning 0f as threshold.");
+ return 0f;
+ }
+
+ var confidenceValues = items.Select(x => x.Confidence).ToList();
+ var minConfidence = confidenceValues.Min();
+ var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold);
+ var upperBound = confidenceValues.Max();
+
+ //
+ // We search for a threshold so that we have between
+ // targetWindowMin and targetWindowMax items. When not
+ // possible, we take all items (e.g., threshold = 0f; depends on the used window strategy)
+ //
+ var threshold = 0.0f;
+
+ // Check the case where the confidence values are too close:
+ if (upperBound - minConfidence >= 0.01)
+ {
+ var previousThreshold = threshold;
+ for (var i = 0; i < maxSteps; i++)
+ {
+ threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps;
+ var numMatches = items.Count(x => x.Confidence >= threshold);
+ if (numMatches <= targetWindow.NumMinItems)
+ {
+ threshold = previousThreshold;
+ break;
+ }
+
+ if (targetWindow.InsideWindow(numMatches))
+ break;
+
+ previousThreshold = threshold;
+ }
+ }
+ else
+ {
+ var logger = Program.SERVICE_PROVIDER.GetService>()!;
+ logger.LogWarning("The confidence values are too close. Returning 0f as threshold.");
+ }
+
+ return threshold;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/IMessageBusReceiver.cs b/app/MindWork AI Studio/Tools/IMessageBusReceiver.cs
index 019ce115..044e425b 100644
--- a/app/MindWork AI Studio/Tools/IMessageBusReceiver.cs
+++ b/app/MindWork AI Studio/Tools/IMessageBusReceiver.cs
@@ -4,6 +4,8 @@ namespace AIStudio.Tools;
public interface IMessageBusReceiver
{
+ public string ComponentName { get; }
+
public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
diff --git a/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs b/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs
index c78bf2d0..702d2732 100644
--- a/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs
+++ b/app/MindWork AI Studio/Tools/JsRuntimeExtensions.cs
@@ -6,7 +6,7 @@ public static class JsRuntimeExtensions
{
public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2)
{
- await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantBase.RESULT_DIV_ID, AssistantBase.BEFORE_RESULT_DIV_ID);
+ await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID);
}
public static async Task ClearDiv(this IJSRuntime jsRuntime, string divId)
diff --git a/app/MindWork AI Studio/Tools/LongExtensions.cs b/app/MindWork AI Studio/Tools/LongExtensions.cs
new file mode 100644
index 00000000..3209e47a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/LongExtensions.cs
@@ -0,0 +1,22 @@
+namespace AIStudio.Tools;
+
+public static class LongExtensions
+{
+ ///
+ /// Formats the file size in a human-readable format.
+ ///
+ /// The size in bytes.
+ /// The formatted file size.
+ public static string FileSize(this long sizeBytes)
+ {
+ string[] sizes = { "B", "kB", "MB", "GB", "TB" };
+ var order = 0;
+ while (sizeBytes >= 1024 && order < sizes.Length - 1)
+ {
+ order++;
+ sizeBytes /= 1024;
+ }
+
+ return $"{sizeBytes:0.##} {sizes[order]}";
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/NoComponent.cs b/app/MindWork AI Studio/Tools/NoComponent.cs
new file mode 100644
index 00000000..f0072efa
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/NoComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Tools;
+
+public sealed class NoComponent: IComponent
+{
+ public void Attach(RenderHandle renderHandle)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SetParametersAsync(ParameterView parameters)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs b/app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs
new file mode 100644
index 00000000..b38459d6
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs
@@ -0,0 +1,99 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Checks if a plugin is forbidden.
+///
+public static class ForbiddenPlugins
+{
+ private const string ID_PATTERN = "ID = \"";
+ private static readonly int ID_PATTERN_LEN = ID_PATTERN.Length;
+
+ ///
+ /// Checks if the given code represents a forbidden plugin.
+ ///
+ /// The code to check.
+ /// The result of the check.
+ public static PluginCheckResult Check(ReadOnlySpan code)
+ {
+ var endIndex = 0;
+ var foundAnyId = false;
+ var id = ReadOnlySpan.Empty;
+ while (true)
+ {
+ // Create a slice of the code starting at the end index.
+ // This way we can search for all IDs in the code:
+ code = code[endIndex..];
+
+ // Read the next ID as a string:
+ if (!TryGetId(code, out id, out endIndex))
+ {
+ // When no ID was found at all, we block this plugin.
+ // When another ID was found previously, we allow this plugin.
+ if(foundAnyId)
+ return new PluginCheckResult(false, null);
+
+ return new PluginCheckResult(true, "No ID was found.");
+ }
+
+ // Try to parse the ID as a GUID:
+ if (!Guid.TryParse(id, out var parsedGuid))
+ {
+ // Again, when no ID was found at all, we block this plugin.
+ if(foundAnyId)
+ return new PluginCheckResult(false, null);
+
+ return new PluginCheckResult(true, "The ID is not a valid GUID.");
+ }
+
+ // Check if the GUID is forbidden:
+ if (FORBIDDEN_PLUGINS.TryGetValue(parsedGuid, out var reason))
+ return new PluginCheckResult(true, reason);
+
+ foundAnyId = true;
+ }
+ }
+
+ private static bool TryGetId(ReadOnlySpan code, out ReadOnlySpan id, out int endIndex)
+ {
+ //
+ // Please note: the code variable is a slice of the original code.
+ // That means the indices are relative to the slice, not the original code.
+ //
+
+ id = ReadOnlySpan.Empty;
+ endIndex = 0;
+
+ // Find the next ID:
+ var idStartIndex = code.IndexOf(ID_PATTERN);
+ if (idStartIndex < 0)
+ return false;
+
+ // Find the start index of the value (Guid):
+ var valueStartIndex = idStartIndex + ID_PATTERN_LEN;
+
+ // Find the end index of the value. In order to do that,
+ // we create a slice of the code starting at the value
+ // start index. That means that the end index is relative
+ // to the inner slice, not the original code nor the outer slice.
+ var valueEndIndex = code[valueStartIndex..].IndexOf('"');
+ if (valueEndIndex < 0)
+ return false;
+
+ // From the perspective of the start index is the end index
+ // the length of the value:
+ endIndex = valueStartIndex + valueEndIndex;
+ id = code.Slice(valueStartIndex, valueEndIndex);
+ return true;
+ }
+
+ ///
+ /// The forbidden plugins.
+ ///
+ ///
+ /// A dictionary that maps the GUID of a plugin to the reason why it is forbidden.
+ ///
+ // ReSharper disable once CollectionNeverUpdated.Local
+ private static readonly Dictionary FORBIDDEN_PLUGINS =
+ [
+ ];
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
new file mode 100644
index 00000000..a33bf3f5
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
@@ -0,0 +1,21 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a contract for a language plugin.
+///
+public interface ILanguagePlugin
+{
+ ///
+ /// Tries to get a text from the language plugin.
+ ///
+ ///
+ /// When the key does not exist, the value will be an empty string.
+ /// Please note that the key is case-sensitive. Furthermore, the keys
+ /// are in the format "root::key". That means that the keys are
+ /// hierarchical and separated by "::".
+ ///
+ /// The key to use to get the text.
+ /// The desired text.
+ /// True if the key exists, false otherwise.
+ public bool TryGetText(string key, out string value);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs
new file mode 100644
index 00000000..95d26b34
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs
@@ -0,0 +1,74 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public interface IPluginMetadata
+{
+ ///
+ /// The icon of this plugin.
+ ///
+ public string IconSVG { get; }
+
+ ///
+ /// The type of this plugin.
+ ///
+ public PluginType Type { get; }
+
+ ///
+ /// The ID of this plugin.
+ ///
+ public Guid Id { get; }
+
+ ///
+ /// The name of this plugin.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The description of this plugin.
+ ///
+ public string Description { get; }
+
+ ///
+ /// The version of this plugin.
+ ///
+ public PluginVersion Version { get; }
+
+ ///
+ /// The authors of this plugin.
+ ///
+ public string[] Authors { get; }
+
+ ///
+ /// The support contact for this plugin.
+ ///
+ public string SupportContact { get; }
+
+ ///
+ /// The source URL of this plugin.
+ ///
+ public string SourceURL { get; }
+
+ ///
+ /// The categories of this plugin.
+ ///
+ public PluginCategory[] Categories { get; }
+
+ ///
+ /// The target groups of this plugin.
+ ///
+ public PluginTargetGroup[] TargetGroups { get; }
+
+ ///
+ /// True, when the plugin is maintained.
+ ///
+ public bool IsMaintained { get; }
+
+ ///
+ /// The message that should be displayed when the plugin is deprecated.
+ ///
+ public string DeprecationMessage { get; }
+
+ ///
+ /// True, when the plugin is AI Studio internal.
+ ///
+ public bool IsInternal { get; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs
new file mode 100644
index 00000000..1cbd8ca7
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs
@@ -0,0 +1,7 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum InternalPlugin
+{
+ LANGUAGE_EN_US,
+ LANGUAGE_DE_DE,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginData.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginData.cs
new file mode 100644
index 00000000..d7595f2a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginData.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public readonly record struct InternalPluginData(PluginType Type, Guid Id, string ShortName)
+{
+ public string ResourcePath => $"{this.Type.GetDirectory()}/{this.ShortName.ToLowerInvariant()}-{this.Id}";
+
+ public string ResourceName => $"{this.ShortName.ToLowerInvariant()}-{this.Id}";
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs
new file mode 100644
index 00000000..3a25aa18
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs
@@ -0,0 +1,12 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class InternalPluginExtensions
+{
+ public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch
+ {
+ InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
+ InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
+
+ _ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
new file mode 100644
index 00000000..d40d2237
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
@@ -0,0 +1,20 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// This Lua module loader does not load any modules.
+///
+public sealed class NoModuleLoader : ILuaModuleLoader
+{
+ #region Implementation of ILuaModuleLoader
+
+ public bool Exists(string moduleName) => false;
+
+ public ValueTask LoadAsync(string moduleName, CancellationToken cancellationToken = default)
+ {
+ return ValueTask.FromResult(new LuaModule());
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
new file mode 100644
index 00000000..3d9b74d1
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
@@ -0,0 +1,9 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a plugin that could not be loaded.
+///
+/// The error message that occurred while parsing the plugin.
+public sealed class NoPlugin(string parsingError) : PluginBase(false, LuaState.Create(), PluginType.NONE, parsingError);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs
new file mode 100644
index 00000000..5c6140c8
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs
@@ -0,0 +1,45 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public abstract partial class PluginBase
+{
+ private const string DEFAULT_ICON_SVG =
+ """
+
+ """;
+
+ #region Initialization-related methods
+
+ ///
+ /// Tries to initialize the icon of the plugin.
+ ///
+ ///
+ /// When no icon is specified, the default icon will be used.
+ ///
+ /// The error message, when the icon could not be read.
+ /// The read icon as SVG.
+ /// True, when the icon could be read successfully.
+
+ // ReSharper disable once OutParameterValueIsAlwaysDiscarded.Local
+ // ReSharper disable once UnusedMethodReturnValue.Local
+ private bool TryInitIconSVG(out string message, out string iconSVG)
+ {
+ if (!this.state.Environment["ICON_SVG"].TryRead(out iconSVG))
+ {
+ iconSVG = DEFAULT_ICON_SVG;
+ message = "The field ICON_SVG does not exist or is not a valid string.";
+ return true;
+ }
+
+ if (string.IsNullOrWhiteSpace(iconSVG))
+ {
+ iconSVG = DEFAULT_ICON_SVG;
+ message = "The field ICON_SVG is empty. The icon must be a non-empty string.";
+ return true;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
new file mode 100644
index 00000000..2674d4db
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
@@ -0,0 +1,495 @@
+using Lua;
+
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents the base of any AI Studio plugin.
+///
+public abstract partial class PluginBase : IPluginMetadata
+{
+ private readonly IReadOnlyCollection baseIssues;
+ protected readonly LuaState state;
+
+ protected readonly List pluginIssues = [];
+
+ ///
+ public string IconSVG { get; }
+
+ ///
+ public PluginType Type { get; }
+
+ ///
+ public Guid Id { get; }
+
+ ///
+ public string Name { get; } = string.Empty;
+
+ ///
+ public string Description { get; } = string.Empty;
+
+ ///
+ public PluginVersion Version { get; }
+
+ ///
+ public string[] Authors { get; } = [];
+
+ ///
+ public string SupportContact { get; } = string.Empty;
+
+ ///
+ public string SourceURL { get; } = string.Empty;
+
+ ///
+ public PluginCategory[] Categories { get; } = [];
+
+ ///
+ public PluginTargetGroup[] TargetGroups { get; } = [];
+
+ ///
+ public bool IsMaintained { get; }
+
+ ///
+ public string DeprecationMessage { get; } = string.Empty;
+
+ ///
+ public bool IsInternal { get; }
+
+ ///
+ /// The issues that occurred during the initialization of this plugin.
+ ///
+ public IEnumerable Issues => this.baseIssues.Concat(this.pluginIssues);
+
+ ///
+ /// True, when the plugin is valid.
+ ///
+ ///
+ /// False means that there were issues during the initialization of the plugin.
+ /// Please check the Issues property for more information.
+ ///
+ public bool IsValid => this is not NoPlugin && this.baseIssues.Count == 0 && this.pluginIssues.Count == 0;
+
+ protected PluginBase(bool isInternal, LuaState state, PluginType type, string parseError = "")
+ {
+ this.state = state;
+ this.Type = type;
+
+ var issues = new List();
+ if(!string.IsNullOrWhiteSpace(parseError))
+ issues.Add(parseError);
+
+ // Notice: when no icon is specified, the default icon will be used.
+ this.TryInitIconSVG(out _, out var iconSVG);
+ this.IconSVG = iconSVG;
+
+ if(this.TryInitId(out var issue, out var id))
+ {
+ this.Id = id;
+ this.IsInternal = isInternal;
+ }
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitName(out issue, out var name))
+ this.Name = name;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitDescription(out issue, out var description))
+ this.Description = description;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitVersion(out issue, out var version))
+ this.Version = version;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitAuthors(out issue, out var authors))
+ this.Authors = authors;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitSupportContact(out issue, out var contact))
+ this.SupportContact = contact;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitSourceURL(out issue, out var url))
+ this.SourceURL = url;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitCategories(out issue, out var categories))
+ this.Categories = categories;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitTargetGroups(out issue, out var targetGroups))
+ this.TargetGroups = targetGroups;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitIsMaintained(out issue, out var isMaintained))
+ this.IsMaintained = isMaintained;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitDeprecationMessage(out issue, out var deprecationMessage))
+ this.DeprecationMessage = deprecationMessage;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ this.baseIssues = issues;
+ }
+
+ #region Initialization-related methods
+
+ ///
+ /// Tries to read the ID of the plugin.
+ ///
+ /// The error message, when the ID could not be read.
+ /// The read ID.
+ /// True, when the ID could be read successfully.
+ private bool TryInitId(out string message, out Guid id)
+ {
+ if (!this.state.Environment["ID"].TryRead(out var idText))
+ {
+ message = "The field ID does not exist or is not a valid string.";
+ id = Guid.Empty;
+ return false;
+ }
+
+ if (!Guid.TryParse(idText, out id))
+ {
+ message = "The field ID is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).";
+ id = Guid.Empty;
+ return false;
+ }
+
+ if(id == Guid.Empty)
+ {
+ message = "The field ID is empty. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the name of the plugin.
+ ///
+ /// The error message, when the name could not be read.
+ /// The read name.
+ /// True, when the name could be read successfully.
+ private bool TryInitName(out string message, out string name)
+ {
+ if (!this.state.Environment["NAME"].TryRead(out name))
+ {
+ message = "The field NAME does not exist or is not a valid string.";
+ name = string.Empty;
+ return false;
+ }
+
+ if(string.IsNullOrWhiteSpace(name))
+ {
+ message = "The field NAME is empty. The name must be a non-empty string.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the description of the plugin.
+ ///
+ /// The error message, when the description could not be read.
+ /// The read description.
+ /// True, when the description could be read successfully.
+ private bool TryInitDescription(out string message, out string description)
+ {
+ if (!this.state.Environment["DESCRIPTION"].TryRead(out description))
+ {
+ message = "The field DESCRIPTION does not exist or is not a valid string.";
+ description = string.Empty;
+ return false;
+ }
+
+ if(string.IsNullOrWhiteSpace(description))
+ {
+ message = "The field DESCRIPTION is empty. The description must be a non-empty string.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the version of the plugin.
+ ///
+ /// The error message, when the version could not be read.
+ /// The read version.
+ /// True, when the version could be read successfully.
+ private bool TryInitVersion(out string message, out PluginVersion version)
+ {
+ if (!this.state.Environment["VERSION"].TryRead(out var versionText))
+ {
+ message = "The field VERSION does not exist or is not a valid string.";
+ version = PluginVersion.NONE;
+ return false;
+ }
+
+ if (!PluginVersion.TryParse(versionText, out version))
+ {
+ message = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).";
+ version = PluginVersion.NONE;
+ return false;
+ }
+
+ if(version == PluginVersion.NONE)
+ {
+ message = "The field VERSION is empty. The version number must be formatted as string in the major.minor.patch format (X.X.X).";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the authors of the plugin.
+ ///
+ /// The error message, when the authors could not be read.
+ /// The read authors.
+ /// True, when the authors could be read successfully.
+ private bool TryInitAuthors(out string message, out string[] authors)
+ {
+ if (!this.state.Environment["AUTHORS"].TryRead(out var authorsTable))
+ {
+ authors = [];
+ message = "The table AUTHORS does not exist or is using an invalid syntax.";
+ return false;
+ }
+
+ var authorList = new List();
+ foreach(var author in authorsTable.GetArraySpan())
+ if(author.TryRead(out var authorName))
+ authorList.Add(authorName);
+
+ authors = authorList.ToArray();
+ if(authorList.Count == 0)
+ {
+ message = "The table AUTHORS is empty. At least one author must be specified.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the support contact for the plugin.
+ ///
+ /// The error message, when the support contact could not be read.
+ /// The read support contact.
+ /// True, when the support contact could be read successfully.
+ private bool TryInitSupportContact(out string message, out string contact)
+ {
+ if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out contact))
+ {
+ contact = string.Empty;
+ message = "The field SUPPORT_CONTACT does not exist or is not a valid string.";
+ return false;
+ }
+
+ if(string.IsNullOrWhiteSpace(contact))
+ {
+ message = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Try to read the source URL of the plugin.
+ ///
+ /// The error message, when the source URL could not be read.
+ /// The read source URL.
+ /// True, when the source URL could be read successfully.
+ private bool TryInitSourceURL(out string message, out string url)
+ {
+ if (!this.state.Environment["SOURCE_URL"].TryRead(out url))
+ {
+ url = string.Empty;
+ message = "The field SOURCE_URL does not exist or is not a valid string.";
+ return false;
+ }
+
+ if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
+ {
+ url = string.Empty;
+ message = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the categories of the plugin.
+ ///
+ /// The error message, when the categories could not be read.
+ /// The read categories.
+ /// True, when the categories could be read successfully.
+ private bool TryInitCategories(out string message, out PluginCategory[] categories)
+ {
+ if (!this.state.Environment["CATEGORIES"].TryRead(out var categoriesTable))
+ {
+ categories = [];
+ message = "The table CATEGORIES does not exist or is using an invalid syntax.";
+ return false;
+ }
+
+ var categoryList = new List();
+ foreach(var luaCategory in categoriesTable.GetArraySpan())
+ if(luaCategory.TryRead(out var categoryName))
+ if(Enum.TryParse(categoryName, out var category) && category != PluginCategory.NONE)
+ categoryList.Add(category);
+
+ categories = categoryList.ToArray();
+ if(categoryList.Count == 0)
+ {
+ message = $"The table CATEGORIES is empty. At least one category is necessary. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the intended target groups for the plugin.
+ ///
+ /// The error message, when the target groups could not be read.
+ /// The read target groups.
+ /// True, when the target groups could be read successfully.
+ private bool TryInitTargetGroups(out string message, out PluginTargetGroup[] targetGroups)
+ {
+ if (!this.state.Environment["TARGET_GROUPS"].TryRead(out var targetGroupsTable))
+ {
+ targetGroups = [];
+ message = "The table TARGET_GROUPS does not exist or is using an invalid syntax.";
+ return false;
+ }
+
+ var targetGroupList = new List();
+ foreach(var luaTargetGroup in targetGroupsTable.GetArraySpan())
+ if(luaTargetGroup.TryRead(out var targetGroupName))
+ if(Enum.TryParse(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE)
+ targetGroupList.Add(targetGroup);
+
+ targetGroups = targetGroupList.ToArray();
+ if(targetGroups.Length == 0)
+ {
+ message = "The table TARGET_GROUPS is empty or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the maintenance status of the plugin.
+ ///
+ /// The error message, when the maintenance status could not be read.
+ /// The read maintenance status.
+ /// True, when the maintenance status could be read successfully.
+ private bool TryInitIsMaintained(out string message, out bool isMaintained)
+ {
+ if (!this.state.Environment["IS_MAINTAINED"].TryRead(out isMaintained))
+ {
+ isMaintained = false;
+ message = "The field IS_MAINTAINED does not exist or is not a valid boolean.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the deprecation message of the plugin.
+ ///
+ /// The error message, when the deprecation message could not be read.
+ /// The read deprecation message.
+ /// True, when the deprecation message could be read successfully.
+ private bool TryInitDeprecationMessage(out string message, out string deprecationMessage)
+ {
+ if (!this.state.Environment["DEPRECATION_MESSAGE"].TryRead(out deprecationMessage))
+ {
+ deprecationMessage = string.Empty;
+ message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This message is optional: use an empty string to indicate that the plugin is not deprecated.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to initialize the UI text content of the plugin.
+ ///
+ /// The error message, when the UI text content could not be read.
+ /// The read UI text content.
+ /// True, when the UI text content could be read successfully.
+ protected bool TryInitUITextContent(out string message, out Dictionary pluginContent)
+ {
+ if (!this.state.Environment["UI_TEXT_CONTENT"].TryRead(out var textTable))
+ {
+ message = "The UI_TEXT_CONTENT table does not exist or is not a valid table.";
+ pluginContent = [];
+ return false;
+ }
+
+ this.ReadTextTable("root", textTable, out pluginContent);
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Reads a flat or hierarchical text table.
+ ///
+ /// The parent key(s).
+ /// The table to read.
+ /// The read table content.
+ protected void ReadTextTable(string parent, LuaTable table, out Dictionary tableContent)
+ {
+ tableContent = [];
+ var lastKey = LuaValue.Nil;
+ while (table.TryGetNext(lastKey, out var pair))
+ {
+ var keyText = pair.Key.ToString();
+ if (pair.Value.TryRead(out var value))
+ tableContent[$"{parent}::{keyText}"] = value;
+
+ else if (pair.Value.TryRead(out var t))
+ {
+ this.ReadTextTable($"{parent}::{keyText}", t, out var subContent);
+ foreach (var (k, v) in subContent)
+ tableContent[k] = v;
+ }
+
+ lastKey = pair.Key;
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs
new file mode 100644
index 00000000..00afcd0e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs
@@ -0,0 +1,33 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginCategory
+{
+ NONE,
+ CORE,
+
+ BUSINESS,
+ INDUSTRY,
+ UTILITY,
+ SOFTWARE_DEVELOPMENT,
+ GAMING,
+ EDUCATION,
+ ENTERTAINMENT,
+ SOCIAL,
+ SHOPPING,
+ TRAVEL,
+ HEALTH,
+ FITNESS,
+ FOOD,
+ PARTY,
+ SPORTS,
+ NEWS,
+ WEATHER,
+ MUSIC,
+ POLITICAL,
+ SCIENCE,
+ TECHNOLOGY,
+ ART,
+ FICTION,
+ WRITING,
+ CONTENT_CREATION,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs
new file mode 100644
index 00000000..35303c06
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs
@@ -0,0 +1,38 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginCategoryExtensions
+{
+ public static string GetName(this PluginCategory type) => type switch
+ {
+ PluginCategory.NONE => "None",
+ PluginCategory.CORE => "AI Studio Core",
+
+ PluginCategory.BUSINESS => "Business",
+ PluginCategory.INDUSTRY => "Industry",
+ PluginCategory.UTILITY => "Utility",
+ PluginCategory.SOFTWARE_DEVELOPMENT => "Software Development",
+ PluginCategory.GAMING => "Gaming",
+ PluginCategory.EDUCATION => "Education",
+ PluginCategory.ENTERTAINMENT => "Entertainment",
+ PluginCategory.SOCIAL => "Social",
+ PluginCategory.SHOPPING => "Shopping",
+ PluginCategory.TRAVEL => "Travel",
+ PluginCategory.HEALTH => "Health",
+ PluginCategory.FITNESS => "Fitness",
+ PluginCategory.FOOD => "Food",
+ PluginCategory.PARTY => "Party",
+ PluginCategory.SPORTS => "Sports",
+ PluginCategory.NEWS => "News",
+ PluginCategory.WEATHER => "Weather",
+ PluginCategory.MUSIC => "Music",
+ PluginCategory.POLITICAL => "Political",
+ PluginCategory.SCIENCE => "Science",
+ PluginCategory.TECHNOLOGY => "Technology",
+ PluginCategory.ART => "Art",
+ PluginCategory.FICTION => "Fiction",
+ PluginCategory.WRITING => "Writing",
+ PluginCategory.CONTENT_CREATION => "Content Creation",
+
+ _ => "Unknown plugin category",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCheckResult.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCheckResult.cs
new file mode 100644
index 00000000..f390a47d
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCheckResult.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents the result of a plugin check.
+///
+/// In case the plugin is forbidden, this is true.
+/// The message that describes why the plugin is forbidden.
+public readonly record struct PluginCheckResult(bool IsForbidden, string? Message);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs
new file mode 100644
index 00000000..c2d75bf3
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs
@@ -0,0 +1,33 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static partial class PluginFactory
+{
+ public static void SetUpHotReloading()
+ {
+ LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'.");
+ try
+ {
+ var messageBus = Program.SERVICE_PROVIDER.GetRequiredService();
+
+ HOT_RELOAD_WATCHER.IncludeSubdirectories = true;
+ HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
+ HOT_RELOAD_WATCHER.Filter = "*.lua";
+ HOT_RELOAD_WATCHER.Changed += async (_, args) =>
+ {
+ LOG.LogInformation($"File changed: {args.FullPath}");
+ await LoadAll();
+ await messageBus.SendMessage(null, Event.PLUGINS_RELOADED);
+ };
+
+ HOT_RELOAD_WATCHER.EnableRaisingEvents = true;
+ }
+ catch (Exception e)
+ {
+ LOG.LogError(e, "Error while setting up hot reloading.");
+ }
+ finally
+ {
+ LOG.LogInformation("Hot reloading plugins set up.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
new file mode 100644
index 00000000..2c14adb6
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
@@ -0,0 +1,81 @@
+using Microsoft.Extensions.FileProviders;
+
+#if RELEASE
+using System.Reflection;
+#endif
+
+namespace AIStudio.Tools.PluginSystem;
+
+public static partial class PluginFactory
+{
+ public static async Task EnsureInternalPlugins()
+ {
+ LOG.LogInformation("Start ensuring internal plugins.");
+ foreach (var plugin in Enum.GetValues())
+ {
+ LOG.LogInformation($"Ensure plugin: {plugin}");
+ await EnsurePlugin(plugin);
+ }
+ }
+
+ private static async Task EnsurePlugin(InternalPlugin plugin)
+ {
+ try
+ {
+ #if DEBUG
+ var basePath = Path.Join(Environment.CurrentDirectory, "Plugins");
+ var resourceFileProvider = new PhysicalFileProvider(basePath);
+ #else
+ var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins");
+ #endif
+
+ var metaData = plugin.MetaData();
+ var mainResourcePath = $"{metaData.ResourcePath}/plugin.lua";
+ var resourceInfo = resourceFileProvider.GetFileInfo(mainResourcePath);
+
+ if(!resourceInfo.Exists)
+ {
+ LOG.LogError($"The plugin {plugin} does not exist. This should not happen, since the plugin is an integral part of AI Studio.");
+ return;
+ }
+
+ // Ensure that the additional resources exist:
+ foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
+ {
+ if(content.IsDirectory)
+ {
+ LOG.LogError("The plugin contains a directory. This is not allowed.");
+ continue;
+ }
+
+ await CopyInternalPluginFile(content, metaData);
+ }
+ }
+ catch
+ {
+ LOG.LogError($"Was not able to ensure the plugin: {plugin}");
+ }
+ }
+
+ private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData)
+ {
+ await using var inputStream = resourceInfo.CreateReadStream();
+
+ var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory());
+
+ if (!Directory.Exists(INTERNAL_PLUGINS_ROOT))
+ Directory.CreateDirectory(INTERNAL_PLUGINS_ROOT);
+
+ if (!Directory.Exists(pluginTypeBasePath))
+ Directory.CreateDirectory(pluginTypeBasePath);
+
+ var pluginPath = Path.Join(pluginTypeBasePath, metaData.ResourceName);
+ if (!Directory.Exists(pluginPath))
+ Directory.CreateDirectory(pluginPath);
+
+ var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name);
+
+ await using var outputStream = File.Create(pluginFilePath);
+ await inputStream.CopyToAsync(outputStream);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
new file mode 100644
index 00000000..0cb87178
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
@@ -0,0 +1,148 @@
+using System.Text;
+
+using AIStudio.Settings;
+
+using Lua;
+using Lua.Standard;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public static partial class PluginFactory
+{
+ private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory");
+
+ private static readonly string DATA_DIR = SettingsManager.DataDirectory!;
+
+ private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
+
+ private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
+
+ private static readonly FileSystemWatcher HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
+
+ private static readonly List AVAILABLE_PLUGINS = [];
+
+ ///
+ /// A list of all available plugins.
+ ///
+ public static IReadOnlyCollection AvailablePlugins => AVAILABLE_PLUGINS;
+
+ ///
+ /// Try to load all plugins from the plugins directory.
+ ///
+ ///
+ /// Loading plugins means:
+ /// - Parsing and checking the plugin code
+ /// - Check for forbidden plugins
+ /// - Creating a new instance of the allowed plugin
+ /// - Read the plugin metadata
+ ///
+ /// Loading a plugin does not mean to start the plugin, though.
+ ///
+ public static async Task LoadAll(CancellationToken cancellationToken = default)
+ {
+ LOG.LogInformation("Start loading plugins.");
+ if (!Directory.Exists(PLUGINS_ROOT))
+ {
+ LOG.LogInformation("No plugins found.");
+ return;
+ }
+
+ AVAILABLE_PLUGINS.Clear();
+
+ //
+ // The easiest way to load all plugins is to find all `plugin.lua` files and load them.
+ // By convention, each plugin is enforced to have a `plugin.lua` file.
+ //
+ var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories);
+ foreach (var pluginMainFile in pluginMainFiles)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ break;
+
+ LOG.LogInformation($"Try to load plugin: {pluginMainFile}");
+ var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken);
+ var pluginPath = Path.GetDirectoryName(pluginMainFile)!;
+ var plugin = await Load(pluginPath, code, cancellationToken);
+
+ switch (plugin)
+ {
+ case NoPlugin noPlugin when noPlugin.Issues.Any():
+ LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: {noPlugin.Issues.First()}");
+ continue;
+
+ case NoPlugin:
+ LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: Unknown.");
+ continue;
+
+ case { IsValid: false }:
+ LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix.");
+ #if DEBUG
+ foreach (var pluginIssue in plugin.Issues)
+ LOG.LogError($"Plugin issue: {pluginIssue}");
+ #endif
+ continue;
+
+ case { IsMaintained: false }:
+ LOG.LogWarning($"The plugin '{pluginMainFile}' is not maintained anymore. Please consider to disable it.");
+ break;
+ }
+
+ LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
+ AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin));
+ }
+ }
+
+ private static async Task Load(string pluginPath, string code, CancellationToken cancellationToken = default)
+ {
+ if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
+ return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
+
+ var state = LuaState.Create();
+
+ // Add the module loader so that the plugin can load other Lua modules:
+ state.ModuleLoader = new PluginLoader(pluginPath);
+
+ // Add some useful libraries:
+ state.OpenModuleLibrary();
+ state.OpenStringLibrary();
+ state.OpenTableLibrary();
+ state.OpenMathLibrary();
+ state.OpenBitwiseLibrary();
+ state.OpenCoroutineLibrary();
+
+ try
+ {
+ await state.DoStringAsync(code, cancellationToken: cancellationToken);
+ }
+ catch (LuaParseException e)
+ {
+ return new NoPlugin($"Was not able to parse the plugin: {e.Message}");
+ }
+ catch (LuaRuntimeException e)
+ {
+ return new NoPlugin($"Was not able to run the plugin: {e.Message}");
+ }
+
+ if (!state.Environment["TYPE"].TryRead(out var typeText))
+ return new NoPlugin("TYPE does not exist or is not a valid string.");
+
+ if (!Enum.TryParse(typeText, out var type))
+ return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}");
+
+ if(type is PluginType.NONE)
+ return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}");
+
+ var isInternal = pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase);
+ return type switch
+ {
+ PluginType.LANGUAGE => new PluginLanguage(isInternal, state, type),
+
+ _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.")
+ };
+ }
+
+ public static void Dispose()
+ {
+ HOT_RELOAD_WATCHER.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
new file mode 100644
index 00000000..4c8cf30a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
@@ -0,0 +1,48 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public sealed class PluginLanguage : PluginBase, ILanguagePlugin
+{
+ private readonly Dictionary content = [];
+
+ private ILanguagePlugin? baseLanguage;
+
+ public PluginLanguage(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type)
+ {
+ if (this.TryInitUITextContent(out var issue, out var readContent))
+ this.content = readContent;
+ else
+ this.pluginIssues.Add(issue);
+ }
+
+ ///
+ /// Sets the base language plugin. This plugin will be used to fill in missing keys.
+ ///
+ /// The base language plugin to use.
+ public void SetBaseLanguage(ILanguagePlugin baseLanguagePlugin) => this.baseLanguage = baseLanguagePlugin;
+
+ ///
+ /// Tries to get a text from the language plugin.
+ ///
+ ///
+ /// When the key neither in the base language nor in this language exist,
+ /// the value will be an empty string. Please note that the key is case-sensitive.
+ /// Furthermore, the keys are in the format "root::key". That means that
+ /// the keys are hierarchical and separated by "::".
+ ///
+ /// The key to use to get the text.
+ /// The desired text.
+ /// True if the key exists, false otherwise.
+ public bool TryGetText(string key, out string value)
+ {
+ if (this.content.TryGetValue(key, out value!))
+ return true;
+
+ if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
+ return true;
+
+ value = string.Empty;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs
new file mode 100644
index 00000000..ec81f73c
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs
@@ -0,0 +1,48 @@
+using System.Text;
+
+using AIStudio.Settings;
+
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Loads Lua modules from a plugin directory.
+///
+///
+/// Any plugin can load Lua modules from its own directory. This class is used to load these modules.
+/// Loading other modules outside the plugin directory is not allowed.
+///
+/// The directory where the plugin is located.
+public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader
+{
+ private static readonly string PLUGIN_BASE_PATH = Path.Join(SettingsManager.DataDirectory, "plugins");
+
+ #region Implementation of ILuaModuleLoader
+
+ ///
+ public bool Exists(string moduleName)
+ {
+ // Ensure that the user doesn't try to escape the plugin directory:
+ if (moduleName.Contains("..") || pluginDirectory.Contains(".."))
+ return false;
+
+ // Ensure that the plugin directory is nested in the plugin base path:
+ if (!pluginDirectory.StartsWith(PLUGIN_BASE_PATH, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
+ return File.Exists(path);
+ }
+
+ ///
+ public async ValueTask LoadAsync(string moduleName, CancellationToken cancellationToken = default)
+ {
+ var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
+ var code = await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
+
+ return new(moduleName, code);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
new file mode 100644
index 00000000..2bfdab1e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
@@ -0,0 +1,50 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
+{
+ #region Implementation of IPluginMetadata
+
+ ///
+ public string IconSVG { get; } = plugin.IconSVG;
+
+ ///
+ public PluginType Type { get; } = plugin.Type;
+
+ ///
+ public Guid Id { get; } = plugin.Id;
+
+ ///
+ public string Name { get; } = plugin.Name;
+
+ ///
+ public string Description { get; } = plugin.Description;
+
+ ///
+ public PluginVersion Version { get; } = plugin.Version;
+
+ ///
+ public string[] Authors { get; } = plugin.Authors;
+
+ ///
+ public string SupportContact { get; } = plugin.SupportContact;
+
+ ///
+ public string SourceURL { get; } = plugin.SourceURL;
+
+ ///
+ public PluginCategory[] Categories { get; } = plugin.Categories;
+
+ ///
+ public PluginTargetGroup[] TargetGroups { get; } = plugin.TargetGroups;
+
+ ///
+ public bool IsMaintained { get; } = plugin.IsMaintained;
+
+ ///
+ public string DeprecationMessage { get; } = plugin.DeprecationMessage;
+
+ ///
+ public bool IsInternal { get; } = plugin.IsInternal;
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs
new file mode 100644
index 00000000..102aa857
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs
@@ -0,0 +1,20 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginTargetGroup
+{
+ NONE,
+
+ EVERYONE,
+ CHILDREN,
+ TEENAGERS,
+ STUDENTS,
+ ADULTS,
+
+ INDUSTRIAL_WORKERS,
+ OFFICE_WORKERS,
+ BUSINESS_PROFESSIONALS,
+ SOFTWARE_DEVELOPERS,
+ SCIENTISTS,
+ TEACHERS,
+ ARTISTS,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs
new file mode 100644
index 00000000..7a14123f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs
@@ -0,0 +1,25 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginTargetGroupExtensions
+{
+ public static string Name(this PluginTargetGroup group) => group switch
+ {
+ PluginTargetGroup.NONE => "No target group",
+
+ PluginTargetGroup.EVERYONE => "Everyone",
+ PluginTargetGroup.CHILDREN => "Children",
+ PluginTargetGroup.TEENAGERS => "Teenagers",
+ PluginTargetGroup.STUDENTS => "Students",
+ PluginTargetGroup.ADULTS => "Adults",
+
+ PluginTargetGroup.INDUSTRIAL_WORKERS => "Industrial workers",
+ PluginTargetGroup.OFFICE_WORKERS => "Office workers",
+ PluginTargetGroup.BUSINESS_PROFESSIONALS => "Business professionals",
+ PluginTargetGroup.SOFTWARE_DEVELOPERS => "Software developers",
+ PluginTargetGroup.SCIENTISTS => "Scientists",
+ PluginTargetGroup.TEACHERS => "Teachers",
+ PluginTargetGroup.ARTISTS => "Artists",
+
+ _ => "Unknown target group",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs
new file mode 100644
index 00000000..5730e62f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs
@@ -0,0 +1,11 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginType
+{
+ NONE,
+
+ LANGUAGE,
+ ASSISTANT,
+ CONFIGURATION,
+ THEME,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs
new file mode 100644
index 00000000..b65eb502
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs
@@ -0,0 +1,24 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginTypeExtensions
+{
+ public static string GetName(this PluginType type) => type switch
+ {
+ PluginType.LANGUAGE => "Language plugin",
+ PluginType.ASSISTANT => "Assistant plugin",
+ PluginType.CONFIGURATION => "Configuration plugin",
+ PluginType.THEME => "Theme plugin",
+
+ _ => "Unknown plugin type",
+ };
+
+ public static string GetDirectory(this PluginType type) => type switch
+ {
+ PluginType.LANGUAGE => "languages",
+ PluginType.ASSISTANT => "assistants",
+ PluginType.CONFIGURATION => "configurations",
+ PluginType.THEME => "themes",
+
+ _ => "unknown",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs
new file mode 100644
index 00000000..d5507a56
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs
@@ -0,0 +1,90 @@
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a version number for a plugin.
+///
+/// The major version number.
+/// The minor version number.
+/// The patch version number.
+public readonly record struct PluginVersion(int Major, int Minor, int Patch) : IComparable
+{
+ ///
+ /// Represents no version number.
+ ///
+ public static readonly PluginVersion NONE = new(0, 0, 0);
+
+ ///
+ /// Tries to parse the input string as a plugin version number.
+ ///
+ /// The input string to parse.
+ /// The parsed version number.
+ /// True when the input string was successfully parsed; otherwise, false.
+ public static bool TryParse(string input, out PluginVersion version)
+ {
+ try
+ {
+ version = Parse(input);
+ return true;
+ }
+ catch
+ {
+ version = NONE;
+ return false;
+ }
+ }
+
+ ///
+ /// Parses the input string as a plugin version number.
+ ///
+ /// The input string to parse.
+ /// The parsed version number.
+ /// The input string is not in the correct format.
+ public static PluginVersion Parse(string input)
+ {
+ var segments = input.Split('.');
+ if (segments.Length != 3)
+ throw new FormatException("The input string must be in the format 'major.minor.patch'.");
+
+ var major = int.Parse(segments[0]);
+ var minor = int.Parse(segments[1]);
+ var patch = int.Parse(segments[2]);
+
+ if(major < 0 || minor < 0 || patch < 0)
+ throw new FormatException("The major, minor, and patch numbers must be greater than or equal to 0.");
+
+ return new PluginVersion(major, minor, patch);
+ }
+
+ ///
+ /// Converts the plugin version number to a string in the format 'major.minor.patch'.
+ ///
+ /// The plugin version number as a string.
+ public override string ToString() => $"{this.Major}.{this.Minor}.{this.Patch}";
+
+ ///
+ /// Compares the plugin version number to another plugin version number.
+ ///
+ /// The other plugin version number to compare to.
+ /// A value indicating the relative order of the plugin version numbers.
+ public int CompareTo(PluginVersion other)
+ {
+ var majorCompare = this.Major.CompareTo(other.Major);
+ if (majorCompare != 0)
+ return majorCompare;
+
+ var minorCompare = this.Minor.CompareTo(other.Minor);
+ if (minorCompare != 0)
+ return minorCompare;
+
+ return this.Patch.CompareTo(other.Patch);
+ }
+
+ public static bool operator >(PluginVersion left, PluginVersion right) => left.CompareTo(right) > 0;
+
+ public static bool operator <(PluginVersion left, PluginVersion right) => left.CompareTo(right) < 0;
+
+ public static bool operator >=(PluginVersion left, PluginVersion right) => left.CompareTo(right) >= 0;
+
+ public static bool operator <=(PluginVersion left, PluginVersion right) => left.CompareTo(right) <= 0;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs b/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs
new file mode 100644
index 00000000..fff91251
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs
@@ -0,0 +1,75 @@
+using System.Text;
+
+using AIStudio.Agents;
+using AIStudio.Chat;
+using AIStudio.Provider;
+using AIStudio.Settings;
+
+namespace AIStudio.Tools.RAG.AugmentationProcesses;
+
+public sealed class AugmentationOne : IAugmentationProcess
+{
+ #region Implementation of IAugmentationProcess
+
+ ///
+ public string TechnicalName => "AugmentationOne";
+
+ ///
+ public string UIName => "Standard augmentation process";
+
+ ///
+ public string Description => "This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread.";
+
+ ///
+ public async Task ProcessAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, IReadOnlyList retrievalContexts, CancellationToken token = default)
+ {
+ var logger = Program.SERVICE_PROVIDER.GetService>()!;
+ var settings = Program.SERVICE_PROVIDER.GetService()!;
+
+ if(retrievalContexts.Count == 0)
+ {
+ logger.LogWarning("No retrieval contexts were issued. Skipping the augmentation process.");
+ return chatThread;
+ }
+
+ var numTotalRetrievalContexts = retrievalContexts.Count;
+
+ // Want the user to validate all retrieval contexts?
+ if (settings.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation && chatThread.DataSourceOptions.AutomaticValidation)
+ {
+ // Let's get the validation agent & set up its provider:
+ var validationAgent = Program.SERVICE_PROVIDER.GetService()!;
+ validationAgent.SetLLMProvider(provider);
+
+ // Let's validate all retrieval contexts:
+ var validationResults = await validationAgent.ValidateRetrievalContextsAsync(lastPrompt, chatThread, retrievalContexts, token);
+
+ //
+ // Now, filter the retrieval contexts to the most relevant ones:
+ //
+ var targetWindow = validationResults.DetermineTargetWindow(TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING);
+ var threshold = validationResults.GetConfidenceThreshold(targetWindow);
+
+ // Filter the retrieval contexts:
+ retrievalContexts = validationResults.Where(x => x.RetrievalContext is not null && x.Confidence >= threshold).Select(x => x.RetrievalContext!).ToList();
+ }
+
+ logger.LogInformation($"Starting the augmentation process over {numTotalRetrievalContexts:###,###,###,###} retrieval contexts.");
+
+ //
+ // We build a huge prompt from all retrieval contexts:
+ //
+ var sb = new StringBuilder();
+ sb.AppendLine("The following useful information will help you in processing the user prompt:");
+ sb.AppendLine();
+
+ // Let's convert all retrieval contexts to Markdown:
+ await retrievalContexts.AsMarkdown(sb, token);
+
+ // Add the augmented data to the chat thread:
+ chatThread.AugmentedData = sb.ToString();
+ return chatThread;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/RAG/DataSelectionResult.cs b/app/MindWork AI Studio/Tools/RAG/DataSelectionResult.cs
new file mode 100644
index 00000000..6508b76a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/RAG/DataSelectionResult.cs
@@ -0,0 +1,10 @@
+using AIStudio.Settings;
+
+namespace AIStudio.Tools.RAG;
+
+///