Added documentation

This commit is contained in:
Thorsten Sommer 2024-05-04 10:55:00 +02:00
parent 0859b1f2ec
commit 418f458118
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
11 changed files with 162 additions and 3 deletions

View File

@ -4,6 +4,9 @@ using MudBlazor;
namespace AIStudio.Components.CommonDialogs;
/// <summary>
/// A confirmation dialog that can be used to ask the user for confirmation.
/// </summary>
public partial class ConfirmDialog : ComponentBase
{
[CascadingParameter]

View File

@ -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<string>("window.__TAURI__.path.appLocalDataDir");
var configDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appConfigDir");
// Store the directories in the settings manager:
SettingsManager.ConfigDirectory = configDir;
SettingsManager.DataDirectory = dataDir;

View File

@ -2,6 +2,9 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages;
/// <summary>
/// The chat page.
/// </summary>
public partial class Chat : ComponentBase
{
private async Task SendMessage()

View File

@ -37,6 +37,8 @@ public partial class Settings : ComponentBase
#endregion
#region Provider related
private async Task AddProvider()
{
var dialogParameters = new DialogParameters<ProviderDialog>
@ -96,4 +98,6 @@ public partial class Settings : ComponentBase
await this.SettingsManager.StoreSettings();
}
}
#endregion
}

View File

@ -3,10 +3,20 @@ using Microsoft.JSInterop;
namespace AIStudio.Provider;
/// <summary>
/// A common interface for all providers.
/// </summary>
public interface IProvider
{
/// <summary>
/// The provider's ID.
/// </summary>
public string Id { get; }
/// <summary>
/// The provider's instance name. Useful for multiple instances of the same provider,
/// e.g., to distinguish between different OpenAI API keys.
/// </summary>
public string InstanceName { get; set; }
public IAsyncEnumerable<string> GetChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, Thread chatThread);

View File

@ -2,14 +2,25 @@ using AIStudio.Provider.OpenAI;
namespace AIStudio.Provider;
/// <summary>
/// Enum for all available providers.
/// </summary>
public enum Providers
{
NONE,
OPEN_AI,
}
/// <summary>
/// Extension methods for the provider enum.
/// </summary>
public static class ExtensionsProvider
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this Providers provider) => provider switch
{
Providers.OPEN_AI => "OpenAI",
@ -17,6 +28,11 @@ public static class ExtensionsProvider
_ => "Unknown",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="provider">The provider value.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Providers provider) => provider switch
{
Providers.OPEN_AI => new ProviderOpenAI(),

View File

@ -1,8 +1,18 @@
namespace AIStudio.Settings;
/// <summary>
/// The data model for the settings file.
/// </summary>
public sealed class Data
{
public Version Version { get; init; }
/// <summary>
/// The version of the settings file. Allows us to upgrade the settings,
/// when a new version is available.
/// </summary>
public Version Version { get; init; } = Version.V1;
/// <summary>
/// List of configured providers.
/// </summary>
public List<Provider> Providers { get; init; } = new();
}

View File

@ -2,4 +2,25 @@ using AIStudio.Provider;
namespace AIStudio.Settings;
public readonly record struct Provider(string Id, string InstanceName, Providers UsedProvider);
/// <summary>
/// Data model for configured providers.
/// </summary>
/// <param name="Id">The provider's ID.</param>
/// <param name="InstanceName">The provider's instance name. Useful for multiple instances of the same provider, e.g., to distinguish between different OpenAI API keys.</param>
/// <param name="UsedProvider">The provider used.</param>
public readonly record struct Provider(string Id, string InstanceName, Providers UsedProvider)
{
#region Overrides of ValueType
/// <summary>
/// Returns a string that represents the current provider in a human-readable format.
/// We use this to display the provider in the chat UI.
/// </summary>
/// <returns>A string that represents the current provider in a human-readable format.</returns>
public override string ToString()
{
return $"{this.InstanceName} ({this.UsedProvider.ToName()})";
}
#endregion
}

View File

@ -9,20 +9,35 @@ using MudBlazor;
namespace AIStudio.Settings;
/// <summary>
/// The provider settings dialog.
/// </summary>
public partial class ProviderDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The provider's ID.
/// </summary>
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The user chosen instance name.
/// </summary>
[Parameter]
public string DataInstanceName { get; set; } = string.Empty;
/// <summary>
/// The provider to use.
/// </summary>
[Parameter]
public Providers DataProvider { get; set; } = Providers.NONE;
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
@ -32,6 +47,9 @@ public partial class ProviderDialog : ComponentBase
[Inject]
private IJSRuntime JsRuntime { get; set; } = null!;
/// <summary>
/// The list of used instance names. We need this to check for uniqueness.
/// </summary>
private List<string> 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)
{

View File

@ -6,14 +6,26 @@ using Microsoft.JSInterop;
namespace AIStudio.Settings;
/// <summary>
/// The settings manager.
/// </summary>
public sealed class SettingsManager
{
private const string SETTINGS_FILENAME = "settings.json";
/// <summary>
/// The directory where the configuration files are stored.
/// </summary>
public static string? ConfigDirectory { get; set; }
/// <summary>
/// The directory where the data files are stored.
/// </summary>
public static string? DataDirectory { get; set; }
/// <summary>
/// The configuration data.
/// </summary>
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);
/// <summary>
/// Data structure for any requested secret.
/// </summary>
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, string Secret, string Issue);
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to get the API key for.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync<RequestedSecret>("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);
/// <summary>
/// Data structure for storing a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);
/// <summary>
/// Try to store the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to store the API key for.</param>
/// <param name="key">The API key to store.</param>
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(IJSRuntime jsRuntime, IProvider provider, string key) => await jsRuntime.InvokeAsync<StoreSecretResponse>("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);
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue);
/// <summary>
/// Tries to delete the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to delete the API key for.</param>
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync<DeleteSecretResponse>("window.__TAURI__.invoke", "delete_secret", new DeleteSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
#endregion
/// <summary>
/// Loads the settings from the file system.
/// </summary>
public async Task LoadSettings()
{
if(!this.IsSetUp)
@ -57,6 +107,9 @@ public sealed class SettingsManager
this.ConfigurationData = loadedConfiguration;
}
/// <summary>
/// Stores the settings to the file system.
/// </summary>
public async Task StoreSettings()
{
if(!this.IsSetUp)

View File

@ -1,5 +1,9 @@
namespace AIStudio.Settings;
/// <summary>
/// The version of the settings file. Allows us to upgrade the settings,
/// in case a new version is available.
/// </summary>
public enum Version
{
UNKNOWN,