From 29263660fc0ee56111a839ad432bd136e30956b3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 3 Jul 2024 20:31:04 +0200 Subject: [PATCH] Implement support for self-hosted and local LLMs (#20) --- .../Components/Blocks/Changelog.Logs.cs | 1 + .../Components/Pages/Chat.razor.cs | 2 +- .../Components/Pages/Settings.razor | 17 +- .../Components/Pages/Settings.razor.cs | 13 +- app/MindWork AI Studio/Provider/Providers.cs | 14 +- .../Provider/SelfHosted/ChatRequest.cs | 16 ++ .../Provider/SelfHosted/Message.cs | 8 + .../Provider/SelfHosted/ModelsResponse.cs | 5 + .../Provider/SelfHosted/ProviderSelfHosted.cs | 162 ++++++++++++++++++ app/MindWork AI Studio/Settings/Data.cs | 2 +- app/MindWork AI Studio/Settings/Provider.cs | 7 +- .../Settings/ProviderDialog.razor | 58 ++++--- .../Settings/ProviderDialog.razor.cs | 90 ++++++++-- .../Settings/SettingsManager.cs | 2 +- .../Settings/SettingsMigrations.cs | 39 +++++ app/MindWork AI Studio/Settings/Version.cs | 2 + .../wwwroot/changelog/v0.6.3.md | 8 + metadata.txt | 8 +- runtime/Cargo.lock | 2 +- runtime/Cargo.toml | 2 +- runtime/tauri.conf.json | 2 +- 21 files changed, 408 insertions(+), 52 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs create mode 100644 app/MindWork AI Studio/Provider/SelfHosted/Message.cs create mode 100644 app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs create mode 100644 app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs create mode 100644 app/MindWork AI Studio/Settings/SettingsMigrations.cs create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v0.6.3.md diff --git a/app/MindWork AI Studio/Components/Blocks/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Blocks/Changelog.Logs.cs index 775386d1..0a166d69 100644 --- a/app/MindWork AI Studio/Components/Blocks/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Blocks/Changelog.Logs.cs @@ -13,6 +13,7 @@ public readonly record struct Log(int Build, string Display, string Filename) public static readonly Log[] LOGS = [ + new (159, "v0.6.3, build 159 (2024-07-03 18:26 UTC)", "v0.6.3.md"), new (158, "v0.6.2, build 158 (2024-07-01 18:03 UTC)", "v0.6.2.md"), new (157, "v0.6.1, build 157 (2024-06-30 19:00 UTC)", "v0.6.1.md"), new (156, "v0.6.0, build 156 (2024-06-30 12:49 UTC)", "v0.6.0.md"), diff --git a/app/MindWork AI Studio/Components/Pages/Chat.razor.cs b/app/MindWork AI Studio/Components/Pages/Chat.razor.cs index 53c22aa0..08275bd1 100644 --- a/app/MindWork AI Studio/Components/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Components/Pages/Chat.razor.cs @@ -101,7 +101,7 @@ private async Task SendMessage() // Use the selected provider to get the AI response. // By awaiting this line, we wait for the entire // content to be streamed. - await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread); + await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread); // Disable the stream state: this.isStreaming = false; diff --git a/app/MindWork AI Studio/Components/Pages/Settings.razor b/app/MindWork AI Studio/Components/Pages/Settings.razor index fdc51e4b..2a9414e4 100644 --- a/app/MindWork AI Studio/Components/Pages/Settings.razor +++ b/app/MindWork AI Studio/Components/Pages/Settings.razor @@ -1,4 +1,5 @@ @page "/settings" +@using AIStudio.Provider Settings @@ -11,7 +12,7 @@ - + # @@ -24,12 +25,20 @@ @context.Num @context.InstanceName @context.UsedProvider - @context.Model + + @if(context.UsedProvider is not Providers.SELF_HOSTED) + @context.Model + else + @("as selected by provider") + - + + Open Dashboard + + Edit - + Delete diff --git a/app/MindWork AI Studio/Components/Pages/Settings.razor.cs b/app/MindWork AI Studio/Components/Pages/Settings.razor.cs index 13b06844..e6df7480 100644 --- a/app/MindWork AI Studio/Components/Pages/Settings.razor.cs +++ b/app/MindWork AI Studio/Components/Pages/Settings.razor.cs @@ -50,6 +50,8 @@ private async Task EditProvider(AIStudio.Settings.Provider provider) { x => x.DataInstanceName, provider.InstanceName }, { x => x.DataProvider, provider.UsedProvider }, { x => x.DataModel, provider.Model }, + { x => x.DataHostname, provider.Hostname }, + { x => x.IsSelfHosted, provider.IsSelfHosted }, { x => x.IsEditing, true }, }; @@ -81,7 +83,7 @@ private async Task DeleteProvider(AIStudio.Settings.Provider provider) if (dialogResult.Canceled) return; - var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName); + var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName, provider.Hostname); var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance); if(deleteSecretResponse.Success) { @@ -89,6 +91,15 @@ private async Task DeleteProvider(AIStudio.Settings.Provider provider) await this.SettingsManager.StoreSettings(); } } + + private string GetProviderDashboardURL(Providers provider) => provider switch + { + Providers.OPEN_AI => "https://platform.openai.com/usage", + Providers.MISTRAL => "https://console.mistral.ai/usage/", + Providers.ANTHROPIC => "https://console.anthropic.com/settings/plans", + + _ => string.Empty, + }; #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Providers.cs b/app/MindWork AI Studio/Provider/Providers.cs index 6c8326f2..db99cc3e 100644 --- a/app/MindWork AI Studio/Provider/Providers.cs +++ b/app/MindWork AI Studio/Provider/Providers.cs @@ -1,6 +1,7 @@ using AIStudio.Provider.Anthropic; using AIStudio.Provider.Mistral; using AIStudio.Provider.OpenAI; +using AIStudio.Provider.SelfHosted; namespace AIStudio.Provider; @@ -10,9 +11,12 @@ namespace AIStudio.Provider; public enum Providers { NONE, + OPEN_AI, ANTHROPIC, MISTRAL, + + SELF_HOSTED, } /// @@ -27,11 +31,14 @@ public static class ExtensionsProvider /// The human-readable name of the provider. public static string ToName(this Providers provider) => provider switch { + Providers.NONE => "No provider selected", + Providers.OPEN_AI => "OpenAI", Providers.ANTHROPIC => "Anthropic", Providers.MISTRAL => "Mistral", - Providers.NONE => "No provider selected", + Providers.SELF_HOSTED => "Self-hosted", + _ => "Unknown", }; @@ -40,13 +47,16 @@ public static class ExtensionsProvider /// /// The provider value. /// The used instance name. + /// The hostname of the provider. /// The provider instance. - public static IProvider CreateProvider(this Providers provider, string instanceName) => provider switch + public static IProvider CreateProvider(this Providers provider, string instanceName, string hostname = "http://localhost:1234") => provider switch { Providers.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName }, Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName }, Providers.MISTRAL => new ProviderMistral { InstanceName = instanceName }, + Providers.SELF_HOSTED => new ProviderSelfHosted(hostname) { InstanceName = instanceName }, + _ => new NoProvider(), }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs b/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs new file mode 100644 index 00000000..74b3f089 --- /dev/null +++ b/app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Provider.SelfHosted; + +/// +/// The chat request model. +/// +/// Which model to use for chat completion. +/// The chat messages. +/// Whether to stream the chat completion. +/// The maximum number of tokens to generate. +public readonly record struct ChatRequest( + string Model, + IList Messages, + bool Stream, + + int MaxTokens +); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/Message.cs b/app/MindWork AI Studio/Provider/SelfHosted/Message.cs new file mode 100644 index 00000000..e4ecc70a --- /dev/null +++ b/app/MindWork AI Studio/Provider/SelfHosted/Message.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Provider.SelfHosted; + +/// +/// Chat message model. +/// +/// The text content of the message. +/// The role of the message. +public readonly record struct Message(string Content, string Role); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs b/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs new file mode 100644 index 00000000..8ea8fb57 --- /dev/null +++ b/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs @@ -0,0 +1,5 @@ +namespace AIStudio.Provider.SelfHosted; + +public readonly record struct ModelsResponse(string Object, Model[] Data); + +public readonly record struct Model(string Id, string Object, string OwnedBy); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs new file mode 100644 index 00000000..82a458e3 --- /dev/null +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -0,0 +1,162 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.SelfHosted; + +public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostname}/v1/"), IProvider +{ + private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + #region Implementation of IProvider + + public string Id => "Self-hosted"; + + public string InstanceName { get; set; } = "Self-hosted"; + + public async IAsyncEnumerable StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) + { + // Prepare the system prompt: + var systemPrompt = new Message + { + Role = "system", + Content = chatThread.SystemPrompt, + }; + + // Prepare the OpenAI HTTP chat request: + var providerChatRequest = JsonSerializer.Serialize(new ChatRequest + { + Model = (await this.GetTextModels(jsRuntime, settings, token: token)).First().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.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => text.Text, + _ => string.Empty, + } + }).ToList()], + + // Right now, we only support streaming completions: + Stream = true, + MaxTokens = -1, + }, JSON_SERIALIZER_OPTIONS); + + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the content: + request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); + + // 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. + var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + + // Open the response stream: + var providerStream = await response.Content.ReadAsStreamAsync(token); + + // Add a stream reader to read the stream, line by line: + var streamReader = new StreamReader(providerStream); + + // Read the stream, line by line: + while(!streamReader.EndOfStream) + { + // Check if the token is cancelled: + if(token.IsCancellationRequested) + yield break; + + // Read the next line: + var line = await streamReader.ReadLineAsync(token); + + // Skip empty lines: + if(string.IsNullOrWhiteSpace(line)) + continue; + + // Skip lines that do not start with "data: ". Regard + // to the specification, we only want to read the data lines: + if(!line.StartsWith("data: ", StringComparison.InvariantCulture)) + continue; + + // Check if the line is the end of the stream: + if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture)) + yield break; + + ResponseStreamLine providerResponse; + try + { + // We know that the line starts with "data: ". Hence, we can + // skip the first 6 characters to get the JSON data after that. + var jsonData = line[6..]; + + // Deserialize the JSON data: + providerResponse = JsonSerializer.Deserialize(jsonData, JSON_SERIALIZER_OPTIONS); + } + catch + { + // Skip invalid JSON data: + continue; + } + + // Skip empty responses: + if(providerResponse == default || providerResponse.Choices.Count == 0) + continue; + + // Yield the response: + yield return providerResponse.Choices[0].Delta.Content; + } + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public async IAsyncEnumerable StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.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 async Task> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, "models"); + var response = await this.httpClient.SendAsync(request, token); + if(!response.IsSuccessStatusCode) + return []; + + var modelResponse = await response.Content.ReadFromJsonAsync(token); + if (modelResponse.Data.Length > 1) + Console.WriteLine("Warning: multiple models found; using the first one."); + + var firstModel = modelResponse.Data.First(); + return [ new Provider.Model(firstModel.Id) ]; + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public Task> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(Enumerable.Empty()); + } + #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/Data.cs b/app/MindWork AI Studio/Settings/Data.cs index 8052c271..8ce5120b 100644 --- a/app/MindWork AI Studio/Settings/Data.cs +++ b/app/MindWork AI Studio/Settings/Data.cs @@ -9,7 +9,7 @@ public sealed class Data /// 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; + public Version Version { get; init; } = Version.V2; /// /// List of configured providers. diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index d1f6194a..48133267 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -9,8 +9,10 @@ namespace AIStudio.Settings; /// 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. +/// Whether the provider is self-hosted. +/// The hostname of the provider. Useful for self-hosted providers. /// The LLM model to use for chat. -public readonly record struct Provider(uint Num, string Id, string InstanceName, Providers UsedProvider, Model Model) +public readonly record struct Provider(uint Num, string Id, string InstanceName, Providers UsedProvider, Model Model, bool IsSelfHosted = false, string Hostname = "http://localhost:1234") { #region Overrides of ValueType @@ -21,6 +23,9 @@ public readonly record struct Provider(uint Num, string Id, string InstanceName, /// A string that represents the current provider in a human-readable format. public override string ToString() { + if(this.IsSelfHosted) + return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Hostname}, {this.Model})"; + return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Model})"; } diff --git a/app/MindWork AI Studio/Settings/ProviderDialog.razor b/app/MindWork AI Studio/Settings/ProviderDialog.razor index 62fdad3b..82bb5fc1 100644 --- a/app/MindWork AI Studio/Settings/ProviderDialog.razor +++ b/app/MindWork AI Studio/Settings/ProviderDialog.razor @@ -4,49 +4,65 @@ + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + @foreach (Providers provider in Enum.GetValues(typeof(Providers))) + { + @provider + } + + Create account + + @* ReSharper disable once CSharpWarnings::CS8974 *@ - @* ReSharper disable once CSharpWarnings::CS8974 *@ - - @foreach (Providers provider in Enum.GetValues(typeof(Providers))) - { - @provider - } - - - @* ReSharper disable once CSharpWarnings::CS8974 *@ - Reload - + Load + @foreach (var model in this.availableModels) { @model } + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + diff --git a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs index 012930b5..3febabcb 100644 --- a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs @@ -32,6 +32,18 @@ public partial class ProviderDialog : ComponentBase [Parameter] public string DataInstanceName { get; set; } = string.Empty; + /// + /// The chosen hostname for self-hosted providers. + /// + [Parameter] + public string DataHostname { get; set; } = string.Empty; + + /// + /// Is this provider self-hosted? + /// + [Parameter] + public bool IsSelfHosted { get; set; } + /// /// The provider to use. /// @@ -99,7 +111,8 @@ protected override async Task OnInitializedAsync() this.dataAPIKey = requestedSecret.Secret; // Now, we try to load the list of available models: - await this.ReloadModels(); + if(this.DataProvider is not Providers.SELF_HOSTED) + await this.ReloadModels(); } else { @@ -142,6 +155,8 @@ private async Task Store() InstanceName = this.DataInstanceName, UsedProvider = this.DataProvider, Model = this.DataModel, + IsSelfHosted = this.DataProvider is Providers.SELF_HOSTED, + Hostname = this.DataHostname, }; // We need to instantiate the provider to store the API key: @@ -169,33 +184,49 @@ private async Task Store() private string? ValidatingModel(Model model) { + if(this.DataProvider is Providers.SELF_HOSTED) + return null; + if (model == default) return "Please select a model."; return null; } - [GeneratedRegex("^[a-zA-Z0-9 ]+$")] + [GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")] private static partial Regex InstanceNameRegex(); + private static readonly string[] RESERVED_NAMES = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; + private string? ValidatingInstanceName(string instanceName) { - if(string.IsNullOrWhiteSpace(instanceName)) + if (string.IsNullOrWhiteSpace(instanceName)) return "Please enter an instance name."; - if(instanceName.StartsWith(' ')) - return "The instance name must not start with a space."; + if (instanceName.StartsWith(' ') || instanceName.StartsWith('.')) + return "The instance name must not start with a space or a dot."; + + if (instanceName.EndsWith(' ') || instanceName.EndsWith('.')) + return "The instance name must not end with a space or a dot."; + + if (instanceName.StartsWith('-') || instanceName.StartsWith('_')) + return "The instance name must not start with a hyphen or an underscore."; - if(instanceName.EndsWith(' ')) - return "The instance name must not end with a space."; + if (instanceName.Length > 255) + return "The instance name must not exceed 255 characters."; - // The instance name must only contain letters, numbers, and spaces: if (!InstanceNameRegex().IsMatch(instanceName)) - return "The instance name must only contain letters, numbers, and spaces."; + return "The instance name must only contain letters, numbers, spaces, hyphens, underscores, and dots."; - if(instanceName.Contains(" ")) + if (instanceName.Contains(" ")) return "The instance name must not contain consecutive spaces."; + if (RESERVED_NAMES.Contains(instanceName.ToUpperInvariant())) + return "This name is reserved and cannot be used."; + + if (instanceName.Any(c => Path.GetInvalidFileNameChars().Contains(c))) + return "The instance name contains invalid characters."; + // The instance name must be unique: var lowerInstanceName = instanceName.ToLowerInvariant(); if (lowerInstanceName != this.dataEditingPreviousInstanceName && this.UsedInstanceNames.Contains(lowerInstanceName)) @@ -206,6 +237,9 @@ private async Task Store() private string? ValidatingAPIKey(string apiKey) { + if(this.DataProvider is Providers.SELF_HOSTED) + return null; + if(!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue)) return this.dataAPIKeyStorageIssue; @@ -215,13 +249,28 @@ private async Task Store() return null; } - private void Cancel() => this.MudDialog.Cancel(); + private string? ValidatingHostname(string hostname) + { + if(this.DataProvider != Providers.SELF_HOSTED) + return null; + + if(string.IsNullOrWhiteSpace(hostname)) + return "Please enter a hostname, e.g., http://localhost:1234"; + + if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) + return "The hostname must start with either http:// or https://"; + + if(!Uri.TryCreate(hostname, UriKind.Absolute, out _)) + return "The hostname is not a valid HTTP(S) URL."; + + return null; + } - private bool CanLoadModels => !string.IsNullOrWhiteSpace(this.dataAPIKey) && this.DataProvider != Providers.NONE && !string.IsNullOrWhiteSpace(this.DataInstanceName); + private void Cancel() => this.MudDialog.Cancel(); private async Task ReloadModels() { - var provider = this.DataProvider.CreateProvider(this.DataInstanceName); + var provider = this.DataProvider.CreateProvider("temp"); if(provider is NoProvider) return; @@ -233,4 +282,19 @@ private async Task ReloadModels() this.availableModels.Clear(); this.availableModels.AddRange(orderedModels); } + + private bool CanLoadModels => !string.IsNullOrWhiteSpace(this.dataAPIKey) && this.DataProvider != Providers.NONE && this.DataProvider != Providers.SELF_HOSTED; + + private bool IsCloudProvider => this.DataProvider is not Providers.SELF_HOSTED; + + private bool IsSelfHostedOrNone => this.DataProvider is Providers.SELF_HOSTED or Providers.NONE; + + private string GetProviderCreationURL() => this.DataProvider switch + { + Providers.OPEN_AI => "https://platform.openai.com/signup", + Providers.MISTRAL => "https://console.mistral.ai/", + Providers.ANTHROPIC => "https://console.anthropic.com/dashboard", + + _ => string.Empty, + }; } \ 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 4353056e..4a5d2991 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -103,7 +103,7 @@ public async Task LoadSettings() if(loadedConfiguration is null) return; - this.ConfigurationData = loadedConfiguration; + this.ConfigurationData = SettingsMigrations.Migrate(loadedConfiguration); } /// diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs new file mode 100644 index 00000000..e5783787 --- /dev/null +++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs @@ -0,0 +1,39 @@ +namespace AIStudio.Settings; + +public static class SettingsMigrations +{ + public static Data Migrate(Data previousData) + { + switch (previousData.Version) + { + case Version.V1: + return MigrateFromV1(previousData); + + default: + Console.WriteLine("No migration needed."); + return previousData; + } + } + + private static Data MigrateFromV1(Data previousData) + { + // + // Summary: + // In v1 we had no self-hosted providers. Thus, we had no hostnames. + // + + Console.WriteLine("Migrating from v1 to v2..."); + return new() + { + Version = Version.V2, + + Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = "" }).ToList(), + + EnableSpellchecking = previousData.EnableSpellchecking, + IsSavingEnergy = previousData.IsSavingEnergy, + NextProviderNum = previousData.NextProviderNum, + ShortcutSendBehavior = previousData.ShortcutSendBehavior, + UpdateBehavior = previousData.UpdateBehavior, + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/Version.cs b/app/MindWork AI Studio/Settings/Version.cs index 047289ea..04e54efd 100644 --- a/app/MindWork AI Studio/Settings/Version.cs +++ b/app/MindWork AI Studio/Settings/Version.cs @@ -7,5 +7,7 @@ namespace AIStudio.Settings; public enum Version { UNKNOWN, + V1, + V2, } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.6.3.md b/app/MindWork AI Studio/wwwroot/changelog/v0.6.3.md new file mode 100644 index 00000000..71c29aec --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.6.3.md @@ -0,0 +1,8 @@ +# v0.6.3, build 159 (2024-07-03 18:26 UTC) +- Added possibility to configure a self-hosted or local provider. +- Added settings migration processor to handle settings migration from previous versions. +- Added links to create an account for the selected provider in the provider dialog. +- Added links to each provider's dashboard to the settings page. +- Added self-hosted and local providers. +- Improved instance name validation. +- Optimized the provider dialog: changed the layout to improve usability. \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 887fb2dc..6d8edecc 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,9 +1,9 @@ -0.6.2 -2024-07-01 18:08:01 UTC -158 +0.6.3 +2024-07-03 18:26:31 UTC +159 8.0.206 (commit bb12410699) 8.0.6 (commit 3b8b000a0e) 1.79.0 (commit 129f3b996) 6.20.0 1.6.1 -c86a9e32c12, release +ac6748e9eb5, release diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index a60a30a8..24bdacd5 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2313,7 +2313,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "0.6.2" +version = "0.6.3" dependencies = [ "arboard", "flexi_logger", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index c4da1020..cb4728b8 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindwork-ai-studio" -version = "0.6.2" +version = "0.6.3" edition = "2021" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 590d1c7c..07af8486 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -6,7 +6,7 @@ }, "package": { "productName": "MindWork AI Studio", - "version": "0.6.2" + "version": "0.6.3" }, "tauri": { "allowlist": {