diff --git a/app/MindWork AI Studio/Components/CommonDialogs/ConfirmDialog.razor.cs b/app/MindWork AI Studio/Components/CommonDialogs/ConfirmDialog.razor.cs
index 201f906..5e367e1 100644
--- a/app/MindWork AI Studio/Components/CommonDialogs/ConfirmDialog.razor.cs
+++ b/app/MindWork AI Studio/Components/CommonDialogs/ConfirmDialog.razor.cs
@@ -4,6 +4,9 @@ using MudBlazor;
namespace AIStudio.Components.CommonDialogs;
+///
+/// A confirmation dialog that can be used to ask the user for confirmation.
+///
public partial class ConfirmDialog : ComponentBase
{
[CascadingParameter]
diff --git a/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs
index 67050cc..819c370 100644
--- a/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs
@@ -13,8 +13,14 @@ public partial class MainLayout : LayoutComponentBase
protected override async Task OnInitializedAsync()
{
+ //
+ // We use the Tauri API (Rust) to get the data and config directories
+ // for this app.
+ //
var dataDir = await this.JsRuntime.InvokeAsync("window.__TAURI__.path.appLocalDataDir");
var configDir = await this.JsRuntime.InvokeAsync("window.__TAURI__.path.appConfigDir");
+
+ // Store the directories in the settings manager:
SettingsManager.ConfigDirectory = configDir;
SettingsManager.DataDirectory = dataDir;
diff --git a/app/MindWork AI Studio/Components/Pages/Chat.razor.cs b/app/MindWork AI Studio/Components/Pages/Chat.razor.cs
index 83f6fbc..5dad1c7 100644
--- a/app/MindWork AI Studio/Components/Pages/Chat.razor.cs
+++ b/app/MindWork AI Studio/Components/Pages/Chat.razor.cs
@@ -2,6 +2,9 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages;
+///
+/// The chat page.
+///
public partial class Chat : ComponentBase
{
private async Task SendMessage()
diff --git a/app/MindWork AI Studio/Components/Pages/Settings.razor.cs b/app/MindWork AI Studio/Components/Pages/Settings.razor.cs
index cabf3dd..5b7803a 100644
--- a/app/MindWork AI Studio/Components/Pages/Settings.razor.cs
+++ b/app/MindWork AI Studio/Components/Pages/Settings.razor.cs
@@ -37,6 +37,8 @@ public partial class Settings : ComponentBase
#endregion
+ #region Provider related
+
private async Task AddProvider()
{
var dialogParameters = new DialogParameters
@@ -96,4 +98,6 @@ public partial class Settings : ComponentBase
await this.SettingsManager.StoreSettings();
}
}
+
+ #endregion
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs
index db138f7..38b90ef 100644
--- a/app/MindWork AI Studio/Provider/IProvider.cs
+++ b/app/MindWork AI Studio/Provider/IProvider.cs
@@ -3,10 +3,20 @@ using Microsoft.JSInterop;
namespace AIStudio.Provider;
+///
+/// A common interface for all providers.
+///
public interface IProvider
{
+ ///
+ /// The provider's ID.
+ ///
public string Id { get; }
+ ///
+ /// The provider's instance name. Useful for multiple instances of the same provider,
+ /// e.g., to distinguish between different OpenAI API keys.
+ ///
public string InstanceName { get; set; }
public IAsyncEnumerable GetChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, Thread chatThread);
diff --git a/app/MindWork AI Studio/Provider/Providers.cs b/app/MindWork AI Studio/Provider/Providers.cs
index 22219d4..83d3ad9 100644
--- a/app/MindWork AI Studio/Provider/Providers.cs
+++ b/app/MindWork AI Studio/Provider/Providers.cs
@@ -2,14 +2,25 @@ using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider;
+///
+/// Enum for all available providers.
+///
public enum Providers
{
NONE,
OPEN_AI,
}
+///
+/// Extension methods for the provider enum.
+///
public static class ExtensionsProvider
{
+ ///
+ /// Returns the human-readable name of the provider.
+ ///
+ /// The provider.
+ /// The human-readable name of the provider.
public static string ToName(this Providers provider) => provider switch
{
Providers.OPEN_AI => "OpenAI",
@@ -17,6 +28,11 @@ public static class ExtensionsProvider
_ => "Unknown",
};
+ ///
+ /// Creates a new provider instance based on the provider value.
+ ///
+ /// The provider value.
+ /// The provider instance.
public static IProvider CreateProvider(this Providers provider) => provider switch
{
Providers.OPEN_AI => new ProviderOpenAI(),
diff --git a/app/MindWork AI Studio/Settings/Data.cs b/app/MindWork AI Studio/Settings/Data.cs
index 16c1ffa..1cbeca3 100644
--- a/app/MindWork AI Studio/Settings/Data.cs
+++ b/app/MindWork AI Studio/Settings/Data.cs
@@ -1,8 +1,18 @@
namespace AIStudio.Settings;
+///
+/// The data model for the settings file.
+///
public sealed class Data
{
- public Version Version { get; init; }
+ ///
+ /// The version of the settings file. Allows us to upgrade the settings,
+ /// when a new version is available.
+ ///
+ public Version Version { get; init; } = Version.V1;
+ ///
+ /// List of configured providers.
+ ///
public List Providers { get; init; } = new();
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs
index eb7fdc8..4d32f01 100644
--- a/app/MindWork AI Studio/Settings/Provider.cs
+++ b/app/MindWork AI Studio/Settings/Provider.cs
@@ -2,4 +2,25 @@ using AIStudio.Provider;
namespace AIStudio.Settings;
-public readonly record struct Provider(string Id, string InstanceName, Providers UsedProvider);
\ No newline at end of file
+///
+/// Data model for configured providers.
+///
+/// The provider's ID.
+/// The provider's instance name. Useful for multiple instances of the same provider, e.g., to distinguish between different OpenAI API keys.
+/// The provider used.
+public readonly record struct Provider(string Id, string InstanceName, Providers UsedProvider)
+{
+ #region Overrides of ValueType
+
+ ///
+ /// Returns a string that represents the current provider in a human-readable format.
+ /// We use this to display the provider in the chat UI.
+ ///
+ /// A string that represents the current provider in a human-readable format.
+ public override string ToString()
+ {
+ return $"{this.InstanceName} ({this.UsedProvider.ToName()})";
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs
index b8c5fa8..51e602f 100644
--- a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs
@@ -9,20 +9,35 @@ using MudBlazor;
namespace AIStudio.Settings;
+///
+/// The provider settings dialog.
+///
public partial class ProviderDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
+ ///
+ /// The provider's ID.
+ ///
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
+ ///
+ /// The user chosen instance name.
+ ///
[Parameter]
public string DataInstanceName { get; set; } = string.Empty;
+ ///
+ /// The provider to use.
+ ///
[Parameter]
public Providers DataProvider { get; set; } = Providers.NONE;
-
+
+ ///
+ /// Should the dialog be in editing mode?
+ ///
[Parameter]
public bool IsEditing { get; init; }
@@ -32,6 +47,9 @@ public partial class ProviderDialog : ComponentBase
[Inject]
private IJSRuntime JsRuntime { get; set; } = null!;
+ ///
+ /// The list of used instance names. We need this to check for uniqueness.
+ ///
private List usedInstanceNames { get; set; } = [];
private bool dataIsValid;
@@ -40,14 +58,17 @@ public partial class ProviderDialog : ComponentBase
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
+ // We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
+ // Load the used instance names:
this.usedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
+ // When editing, we need to load the data:
if(this.IsEditing)
{
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
@@ -57,6 +78,7 @@ public partial class ProviderDialog : ComponentBase
provider.InstanceName = this.DataInstanceName;
+ // Load the API key:
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
if(requestedSecret.Success)
this.dataAPIKey = requestedSecret.Secret;
@@ -72,6 +94,8 @@ public partial class ProviderDialog : ComponentBase
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();
@@ -86,9 +110,12 @@ public partial class ProviderDialog : ComponentBase
if (!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue))
this.dataAPIKeyStorageIssue = string.Empty;
+ // When the data is not valid, we don't store it:
if (!this.dataIsValid)
return;
+ // Use the data model to store the provider.
+ // We just return this data to the parent component:
var addedProvider = new Provider
{
Id = this.DataId,
@@ -96,9 +123,11 @@ public partial class ProviderDialog : ComponentBase
UsedProvider = this.DataProvider,
};
+ // We need to instantiate the provider to store the API key:
var provider = this.DataProvider.CreateProvider();
provider.InstanceName = this.DataInstanceName;
+ // Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
if (!storeResponse.Success)
{
diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs
index 3936e4c..af6adf5 100644
--- a/app/MindWork AI Studio/Settings/SettingsManager.cs
+++ b/app/MindWork AI Studio/Settings/SettingsManager.cs
@@ -6,14 +6,26 @@ using Microsoft.JSInterop;
namespace AIStudio.Settings;
+///
+/// The settings manager.
+///
public sealed class SettingsManager
{
private const string SETTINGS_FILENAME = "settings.json";
+ ///
+ /// The directory where the configuration files are stored.
+ ///
public static string? ConfigDirectory { get; set; }
+ ///
+ /// The directory where the data files are stored.
+ ///
public static string? DataDirectory { get; set; }
+ ///
+ /// The configuration data.
+ ///
public Data ConfigurationData { get; private set; } = new();
private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory);
@@ -22,24 +34,62 @@ public sealed class SettingsManager
private readonly record struct GetSecretRequest(string Destination, string UserName);
+ ///
+ /// Data structure for any requested secret.
+ ///
+ /// True, when the secret was successfully retrieved.
+ /// The secret, e.g., API key.
+ /// The issue, when the secret could not be retrieved.
public readonly record struct RequestedSecret(bool Success, string Secret, string Issue);
+ ///
+ /// Try to get the API key for the given provider.
+ ///
+ /// The JS runtime to access the Rust code.
+ /// The provider to get the API key for.
+ /// The requested secret.
public async Task GetAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync("window.__TAURI__.invoke", "get_secret", new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
private readonly record struct StoreSecretRequest(string Destination, string UserName, string Secret);
+ ///
+ /// Data structure for storing a secret response.
+ ///
+ /// True, when the secret was successfully stored.
+ /// The issue, when the secret could not be stored.
public readonly record struct StoreSecretResponse(bool Success, string Issue);
+ ///
+ /// Try to store the API key for the given provider.
+ ///
+ /// The JS runtime to access the Rust code.
+ /// The provider to store the API key for.
+ /// The API key to store.
+ /// The store secret response.
public async Task SetAPIKey(IJSRuntime jsRuntime, IProvider provider, string key) => await jsRuntime.InvokeAsync("window.__TAURI__.invoke", "store_secret", new StoreSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, key));
private readonly record struct DeleteSecretRequest(string Destination, string UserName);
+ ///
+ /// Data structure for deleting a secret response.
+ ///
+ /// True, when the secret was successfully deleted.
+ /// The issue, when the secret could not be deleted.
public readonly record struct DeleteSecretResponse(bool Success, string Issue);
+ ///
+ /// Tries to delete the API key for the given provider.
+ ///
+ /// The JS runtime to access the Rust code.
+ /// The provider to delete the API key for.
+ /// The delete secret response.
public async Task DeleteAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync("window.__TAURI__.invoke", "delete_secret", new DeleteSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
#endregion
+ ///
+ /// Loads the settings from the file system.
+ ///
public async Task LoadSettings()
{
if(!this.IsSetUp)
@@ -57,6 +107,9 @@ public sealed class SettingsManager
this.ConfigurationData = loadedConfiguration;
}
+ ///
+ /// Stores the settings to the file system.
+ ///
public async Task StoreSettings()
{
if(!this.IsSetUp)
diff --git a/app/MindWork AI Studio/Settings/Version.cs b/app/MindWork AI Studio/Settings/Version.cs
index 0ce7765..047289e 100644
--- a/app/MindWork AI Studio/Settings/Version.cs
+++ b/app/MindWork AI Studio/Settings/Version.cs
@@ -1,5 +1,9 @@
namespace AIStudio.Settings;
+///
+/// The version of the settings file. Allows us to upgrade the settings,
+/// in case a new version is available.
+///
public enum Version
{
UNKNOWN,