mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-10 20:49:06 +00:00
Added documentation
This commit is contained in:
parent
0859b1f2ec
commit
418f458118
@ -4,6 +4,9 @@ using MudBlazor;
|
|||||||
|
|
||||||
namespace AIStudio.Components.CommonDialogs;
|
namespace AIStudio.Components.CommonDialogs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A confirmation dialog that can be used to ask the user for confirmation.
|
||||||
|
/// </summary>
|
||||||
public partial class ConfirmDialog : ComponentBase
|
public partial class ConfirmDialog : ComponentBase
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
|
@ -13,8 +13,14 @@ public partial class MainLayout : LayoutComponentBase
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
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 dataDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appLocalDataDir");
|
||||||
var configDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appConfigDir");
|
var configDir = await this.JsRuntime.InvokeAsync<string>("window.__TAURI__.path.appConfigDir");
|
||||||
|
|
||||||
|
// Store the directories in the settings manager:
|
||||||
SettingsManager.ConfigDirectory = configDir;
|
SettingsManager.ConfigDirectory = configDir;
|
||||||
SettingsManager.DataDirectory = dataDir;
|
SettingsManager.DataDirectory = dataDir;
|
||||||
|
|
||||||
|
@ -2,6 +2,9 @@ using Microsoft.AspNetCore.Components;
|
|||||||
|
|
||||||
namespace AIStudio.Components.Pages;
|
namespace AIStudio.Components.Pages;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The chat page.
|
||||||
|
/// </summary>
|
||||||
public partial class Chat : ComponentBase
|
public partial class Chat : ComponentBase
|
||||||
{
|
{
|
||||||
private async Task SendMessage()
|
private async Task SendMessage()
|
||||||
|
@ -37,6 +37,8 @@ public partial class Settings : ComponentBase
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Provider related
|
||||||
|
|
||||||
private async Task AddProvider()
|
private async Task AddProvider()
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<ProviderDialog>
|
var dialogParameters = new DialogParameters<ProviderDialog>
|
||||||
@ -96,4 +98,6 @@ public partial class Settings : ComponentBase
|
|||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
@ -3,10 +3,20 @@ using Microsoft.JSInterop;
|
|||||||
|
|
||||||
namespace AIStudio.Provider;
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A common interface for all providers.
|
||||||
|
/// </summary>
|
||||||
public interface IProvider
|
public interface IProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The provider's ID.
|
||||||
|
/// </summary>
|
||||||
public string Id { get; }
|
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 string InstanceName { get; set; }
|
||||||
|
|
||||||
public IAsyncEnumerable<string> GetChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, Thread chatThread);
|
public IAsyncEnumerable<string> GetChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, Thread chatThread);
|
||||||
|
@ -2,14 +2,25 @@ using AIStudio.Provider.OpenAI;
|
|||||||
|
|
||||||
namespace AIStudio.Provider;
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enum for all available providers.
|
||||||
|
/// </summary>
|
||||||
public enum Providers
|
public enum Providers
|
||||||
{
|
{
|
||||||
NONE,
|
NONE,
|
||||||
OPEN_AI,
|
OPEN_AI,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for the provider enum.
|
||||||
|
/// </summary>
|
||||||
public static class ExtensionsProvider
|
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
|
public static string ToName(this Providers provider) => provider switch
|
||||||
{
|
{
|
||||||
Providers.OPEN_AI => "OpenAI",
|
Providers.OPEN_AI => "OpenAI",
|
||||||
@ -17,6 +28,11 @@ public static class ExtensionsProvider
|
|||||||
_ => "Unknown",
|
_ => "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
|
public static IProvider CreateProvider(this Providers provider) => provider switch
|
||||||
{
|
{
|
||||||
Providers.OPEN_AI => new ProviderOpenAI(),
|
Providers.OPEN_AI => new ProviderOpenAI(),
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data model for the settings file.
|
||||||
|
/// </summary>
|
||||||
public sealed class Data
|
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();
|
public List<Provider> Providers { get; init; } = new();
|
||||||
}
|
}
|
@ -2,4 +2,25 @@ using AIStudio.Provider;
|
|||||||
|
|
||||||
namespace AIStudio.Settings;
|
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
|
||||||
|
}
|
@ -9,20 +9,35 @@ using MudBlazor;
|
|||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The provider settings dialog.
|
||||||
|
/// </summary>
|
||||||
public partial class ProviderDialog : ComponentBase
|
public partial class ProviderDialog : ComponentBase
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The provider's ID.
|
||||||
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string DataId { get; set; } = Guid.NewGuid().ToString();
|
public string DataId { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user chosen instance name.
|
||||||
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string DataInstanceName { get; set; } = string.Empty;
|
public string DataInstanceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The provider to use.
|
||||||
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Providers DataProvider { get; set; } = Providers.NONE;
|
public Providers DataProvider { get; set; } = Providers.NONE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the dialog be in editing mode?
|
||||||
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsEditing { get; init; }
|
public bool IsEditing { get; init; }
|
||||||
|
|
||||||
@ -32,6 +47,9 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private IJSRuntime JsRuntime { get; set; } = null!;
|
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 List<string> usedInstanceNames { get; set; } = [];
|
||||||
|
|
||||||
private bool dataIsValid;
|
private bool dataIsValid;
|
||||||
@ -40,14 +58,17 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
private string dataAPIKeyStorageIssue = string.Empty;
|
private string dataAPIKeyStorageIssue = string.Empty;
|
||||||
private string dataEditingPreviousInstanceName = string.Empty;
|
private string dataEditingPreviousInstanceName = string.Empty;
|
||||||
|
|
||||||
|
// We get the form reference from Blazor code to validate it manually:
|
||||||
private MudForm form = null!;
|
private MudForm form = null!;
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
// Load the used instance names:
|
||||||
this.usedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
|
this.usedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
|
||||||
|
|
||||||
|
// When editing, we need to load the data:
|
||||||
if(this.IsEditing)
|
if(this.IsEditing)
|
||||||
{
|
{
|
||||||
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
|
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
|
||||||
@ -57,6 +78,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
|
|
||||||
provider.InstanceName = this.DataInstanceName;
|
provider.InstanceName = this.DataInstanceName;
|
||||||
|
|
||||||
|
// Load the API key:
|
||||||
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
|
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
|
||||||
if(requestedSecret.Success)
|
if(requestedSecret.Success)
|
||||||
this.dataAPIKey = requestedSecret.Secret;
|
this.dataAPIKey = requestedSecret.Secret;
|
||||||
@ -72,6 +94,8 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
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)
|
if(!this.IsEditing && firstRender)
|
||||||
this.form.ResetValidation();
|
this.form.ResetValidation();
|
||||||
|
|
||||||
@ -86,9 +110,12 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
if (!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue))
|
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)
|
if (!this.dataIsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Use the data model to store the provider.
|
||||||
|
// We just return this data to the parent component:
|
||||||
var addedProvider = new Provider
|
var addedProvider = new Provider
|
||||||
{
|
{
|
||||||
Id = this.DataId,
|
Id = this.DataId,
|
||||||
@ -96,9 +123,11 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
UsedProvider = this.DataProvider,
|
UsedProvider = this.DataProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We need to instantiate the provider to store the API key:
|
||||||
var provider = this.DataProvider.CreateProvider();
|
var provider = this.DataProvider.CreateProvider();
|
||||||
provider.InstanceName = this.DataInstanceName;
|
provider.InstanceName = this.DataInstanceName;
|
||||||
|
|
||||||
|
// Store the API key in the OS secure storage:
|
||||||
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
|
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
|
||||||
if (!storeResponse.Success)
|
if (!storeResponse.Success)
|
||||||
{
|
{
|
||||||
|
@ -6,14 +6,26 @@ using Microsoft.JSInterop;
|
|||||||
|
|
||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The settings manager.
|
||||||
|
/// </summary>
|
||||||
public sealed class SettingsManager
|
public sealed class SettingsManager
|
||||||
{
|
{
|
||||||
private const string SETTINGS_FILENAME = "settings.json";
|
private const string SETTINGS_FILENAME = "settings.json";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The directory where the configuration files are stored.
|
||||||
|
/// </summary>
|
||||||
public static string? ConfigDirectory { get; set; }
|
public static string? ConfigDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The directory where the data files are stored.
|
||||||
|
/// </summary>
|
||||||
public static string? DataDirectory { get; set; }
|
public static string? DataDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration data.
|
||||||
|
/// </summary>
|
||||||
public Data ConfigurationData { get; private set; } = new();
|
public Data ConfigurationData { get; private set; } = new();
|
||||||
|
|
||||||
private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory);
|
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);
|
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);
|
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));
|
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);
|
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);
|
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));
|
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);
|
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);
|
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));
|
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
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the settings from the file system.
|
||||||
|
/// </summary>
|
||||||
public async Task LoadSettings()
|
public async Task LoadSettings()
|
||||||
{
|
{
|
||||||
if(!this.IsSetUp)
|
if(!this.IsSetUp)
|
||||||
@ -57,6 +107,9 @@ public sealed class SettingsManager
|
|||||||
this.ConfigurationData = loadedConfiguration;
|
this.ConfigurationData = loadedConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the settings to the file system.
|
||||||
|
/// </summary>
|
||||||
public async Task StoreSettings()
|
public async Task StoreSettings()
|
||||||
{
|
{
|
||||||
if(!this.IsSetUp)
|
if(!this.IsSetUp)
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
namespace AIStudio.Settings;
|
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
|
public enum Version
|
||||||
{
|
{
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
|
Loading…
Reference in New Issue
Block a user