diff --git a/app/MindWork AI Studio/Agents/AgentBase.cs b/app/MindWork AI Studio/Agents/AgentBase.cs index 456f0437..0507d8c7 100644 --- a/app/MindWork AI Studio/Agents/AgentBase.cs +++ b/app/MindWork AI Studio/Agents/AgentBase.cs @@ -1,13 +1,14 @@ using AIStudio.Chat; using AIStudio.Provider; using AIStudio.Settings; -using AIStudio.Tools; + +using RustService = AIStudio.Tools.RustService; // ReSharper disable MemberCanBePrivate.Global namespace AIStudio.Agents; -public abstract class AgentBase(ILogger logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent +public abstract class AgentBase(ILogger logger, RustService rustService, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent { protected SettingsManager SettingsManager { get; init; } = settingsManager; @@ -17,6 +18,8 @@ public abstract class AgentBase(ILogger logger, SettingsManager setti protected ILogger Logger { get; init; } = logger; + protected RustService RustService { get; init; } = rustService; + /// /// Represents the type or category of this agent. /// @@ -106,6 +109,6 @@ public abstract class AgentBase(ILogger logger, SettingsManager setti // 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(providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread); + await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger, this.RustService), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs b/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs index 62172be6..6a27b731 100644 --- a/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs +++ b/app/MindWork AI Studio/Agents/AgentTextContentCleaner.cs @@ -1,10 +1,11 @@ using AIStudio.Chat; using AIStudio.Settings; -using AIStudio.Tools; + +using RustService = AIStudio.Tools.RustService; namespace AIStudio.Agents; -public sealed class AgentTextContentCleaner(ILogger logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng) +public sealed class AgentTextContentCleaner(ILogger logger, RustService rustService, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, rustService, settingsManager, jsRuntime, rng) { private static readonly ContentBlock EMPTY_BLOCK = new() { diff --git a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs index b9f95a87..16e1f7e0 100644 --- a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs +++ b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs @@ -1,7 +1,6 @@ using System.Text; using AIStudio.Chat; -using AIStudio.Tools; namespace AIStudio.Assistants.Agenda; diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 83083278..bee2aa07 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -1,10 +1,11 @@ using AIStudio.Chat; using AIStudio.Provider; using AIStudio.Settings; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; +using RustService = AIStudio.Tools.RustService; + namespace AIStudio.Assistants; public abstract partial class AssistantBase : ComponentBase @@ -22,7 +23,7 @@ public abstract partial class AssistantBase : ComponentBase protected ISnackbar Snackbar { get; init; } = null!; [Inject] - protected Rust Rust { get; init; } = null!; + protected RustService RustService { get; init; } = null!; [Inject] protected NavigationManager NavigationManager { get; init; } = null!; @@ -154,7 +155,7 @@ public abstract partial class AssistantBase : ComponentBase // 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.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); + await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger, this.RustService), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); this.isProcessing = false; this.StateHasChanged(); @@ -165,7 +166,7 @@ public abstract partial class AssistantBase : ComponentBase protected async Task CopyToClipboard() { - await this.Rust.CopyText2Clipboard(this.Snackbar, this.Result2Copy()); + await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy()); } private static string? GetButtonIcon(string icon) diff --git a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs index 52c10e40..4f9ba4bc 100644 --- a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs +++ b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs @@ -1,7 +1,5 @@ using System.Text; -using AIStudio.Tools; - namespace AIStudio.Assistants.Coding; public partial class AssistantCoding : AssistantBaseCore diff --git a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs index 686b9282..23151e94 100644 --- a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs +++ b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs @@ -1,7 +1,6 @@ using System.Text; using AIStudio.Chat; -using AIStudio.Tools; namespace AIStudio.Assistants.EMail; diff --git a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs index 1821aa37..7669be27 100644 --- a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs +++ b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Chat; -using AIStudio.Tools; namespace AIStudio.Assistants.GrammarSpelling; diff --git a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs index c45dce41..458c5f0c 100644 --- a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs +++ b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs @@ -1,5 +1,3 @@ -using AIStudio.Tools; - namespace AIStudio.Assistants.IconFinder; public partial class AssistantIconFinder : AssistantBaseCore diff --git a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs index af2138e5..e87fb624 100644 --- a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs +++ b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs @@ -1,5 +1,3 @@ -using AIStudio.Tools; - namespace AIStudio.Assistants.LegalCheck; public partial class AssistantLegalCheck : AssistantBaseCore diff --git a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs index 303a127a..bbadd4e8 100644 --- a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs +++ b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Chat; -using AIStudio.Tools; namespace AIStudio.Assistants.RewriteImprove; diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs index 5d120c86..d2d363ac 100644 --- a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Chat; -using AIStudio.Tools; namespace AIStudio.Assistants.TextSummarizer; diff --git a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs index 97b3ed3b..653dba94 100644 --- a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs +++ b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Chat; -using AIStudio.Tools; namespace AIStudio.Assistants.Translation; diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 8bb48b4a..1b6144f2 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -1,7 +1,7 @@ -using AIStudio.Tools; - using Microsoft.AspNetCore.Components; +using RustService = AIStudio.Tools.RustService; + namespace AIStudio.Chat; /// @@ -40,7 +40,7 @@ public partial class ContentBlockComponent : ComponentBase public string Class { get; set; } = string.Empty; [Inject] - private Rust Rust { get; init; } = null!; + private RustService RustService { get; init; } = null!; [Inject] private IJSRuntime JsRuntime { get; init; } = null!; @@ -100,7 +100,7 @@ public partial class ContentBlockComponent : ComponentBase { case ContentType.TEXT: var textContent = (ContentText) this.Content; - await this.Rust.CopyText2Clipboard(this.Snackbar, textContent.Text); + await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text); break; default: diff --git a/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs b/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs index bad94f67..6951c5e0 100644 --- a/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Settings; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index 59213204..5c0ca716 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Settings; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; diff --git a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs index 42d75a06..6a3c057b 100644 --- a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs +++ b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Layout; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; diff --git a/app/MindWork AI Studio/Components/MSGComponentBase.cs b/app/MindWork AI Studio/Components/MSGComponentBase.cs index c1606ba8..ae88bbd5 100644 --- a/app/MindWork AI Studio/Components/MSGComponentBase.cs +++ b/app/MindWork AI Studio/Components/MSGComponentBase.cs @@ -1,5 +1,3 @@ -using AIStudio.Tools; - using Microsoft.AspNetCore.Components; namespace AIStudio.Components; diff --git a/app/MindWork AI Studio/Components/ProcessComponent.razor.cs b/app/MindWork AI Studio/Components/ProcessComponent.razor.cs index bbcd6e5d..297cdbca 100644 --- a/app/MindWork AI Studio/Components/ProcessComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ProcessComponent.razor.cs @@ -1,5 +1,3 @@ -using AIStudio.Tools; - using Microsoft.AspNetCore.Components; namespace AIStudio.Components; diff --git a/app/MindWork AI Studio/Components/ReadWebContent.razor.cs b/app/MindWork AI Studio/Components/ReadWebContent.razor.cs index c681dc12..b1511b3f 100644 --- a/app/MindWork AI Studio/Components/ReadWebContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadWebContent.razor.cs @@ -1,7 +1,6 @@ using AIStudio.Agents; using AIStudio.Chat; using AIStudio.Settings; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index c57d1aa1..edf0d280 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -5,7 +5,6 @@ using System.Text.Json.Serialization; using AIStudio.Chat; using AIStudio.Dialogs; using AIStudio.Settings; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; diff --git a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs index dccad89c..aefd5518 100644 --- a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor.cs @@ -1,6 +1,6 @@ using System.Reflection; -using AIStudio.Tools; +using AIStudio.Tools.Rust; using Microsoft.AspNetCore.Components; diff --git a/app/MindWork AI Studio/GlobalUsings.cs b/app/MindWork AI Studio/GlobalUsings.cs index 8cd95b6e..fe809fa7 100644 --- a/app/MindWork AI Studio/GlobalUsings.cs +++ b/app/MindWork AI Studio/GlobalUsings.cs @@ -1,5 +1,7 @@ // Global using directives +global using AIStudio.Tools; + global using Microsoft.JSInterop; global using MudBlazor; \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 5e5df417..59dd785c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -1,13 +1,14 @@ using AIStudio.Dialogs; using AIStudio.Settings; using AIStudio.Settings.DataModel; -using AIStudio.Tools; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using DialogOptions = AIStudio.Dialogs.DialogOptions; +using RustService = AIStudio.Tools.RustService; namespace AIStudio.Layout; @@ -26,7 +27,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis private IDialogService DialogService { get; init; } = null!; [Inject] - private Rust Rust { get; init; } = null!; + private RustService RustService { get; init; } = null!; [Inject] private ISnackbar Snackbar { get; init; } = null!; @@ -189,7 +190,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis this.performingUpdate = true; this.StateHasChanged(); - await this.Rust.InstallUpdate(this.JsRuntime); + await this.RustService.InstallUpdate(); } private async ValueTask OnLocationChanging(LocationChangingContext context) diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index 5c31a69a..96b4f9ab 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -1,7 +1,5 @@ using System.Reflection; -using AIStudio.Tools; - using Microsoft.AspNetCore.Components; namespace AIStudio.Pages; diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index fef3287b..6ac863cc 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -4,12 +4,12 @@ using AIStudio.Dialogs; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Settings.DataModel; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using DialogOptions = AIStudio.Dialogs.DialogOptions; +using RustService = AIStudio.Tools.RustService; namespace AIStudio.Pages; @@ -33,6 +33,9 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable [Inject] private ILogger Logger { get; init; } = null!; + [Inject] + private RustService RustService { get; init; } = null!; + private InnerScrolling scrollingArea = null!; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom; @@ -192,7 +195,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable // 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.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); + await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger, this.RustService), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread); // Save the chat: if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs index debf62a9..6678ed17 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor.cs +++ b/app/MindWork AI Studio/Pages/Settings.razor.cs @@ -1,11 +1,11 @@ using AIStudio.Dialogs; using AIStudio.Provider; using AIStudio.Settings; -using AIStudio.Tools; using Microsoft.AspNetCore.Components; using DialogOptions = AIStudio.Dialogs.DialogOptions; +using RustService = AIStudio.Tools.RustService; // ReSharper disable ClassNeverInstantiated.Global @@ -28,6 +28,9 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable [Inject] private ILogger Logger { get; init; } = null!; + [Inject] + private RustService RustService { get; init; } = null!; + private readonly List> availableProviders = new(); #region Overrides of ComponentBase @@ -114,7 +117,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable if (dialogResult is null || dialogResult.Canceled) return; - var providerInstance = provider.CreateProvider(this.Logger); + var providerInstance = provider.CreateProvider(this.Logger, this.RustService); var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance); if(deleteSecretResponse.Success) { diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index a4db1d08..85e96cfb 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -1,7 +1,5 @@ -using AIStudio; using AIStudio.Agents; using AIStudio.Settings; -using AIStudio.Tools; using AIStudio.Tools.Services; using Microsoft.Extensions.Logging.Console; @@ -13,113 +11,125 @@ using System.Reflection; using Microsoft.Extensions.FileProviders; #endif -if(args.Length == 0) +namespace AIStudio; + +internal sealed class Program { - Console.WriteLine("Please provide the port of the runtime API."); - return; -} - -var rustApiPort = args[0]; -using var rust = new Rust(rustApiPort); -var appPort = await rust.GetAppPort(); -if(appPort == 0) -{ - Console.WriteLine("Failed to get the app port from Rust."); - return; -} - -// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable: -var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD"); -if(string.IsNullOrWhiteSpace(secretPasswordEncoded)) -{ - Console.WriteLine("The AI_STUDIO_SECRET_PASSWORD environment variable is not set."); - return; -} - -var secretPassword = Convert.FromBase64String(secretPasswordEncoded); -var secretKeySaltEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY_SALT"); -if(string.IsNullOrWhiteSpace(secretKeySaltEncoded)) -{ - Console.WriteLine("The AI_STUDIO_SECRET_KEY_SALT environment variable is not set."); - return; -} - -var secretKeySalt = Convert.FromBase64String(secretKeySaltEncoded); - -var builder = WebApplication.CreateBuilder(); -builder.Logging.ClearProviders(); -builder.Logging.SetMinimumLevel(LogLevel.Debug); -builder.Logging.AddFilter("Microsoft", LogLevel.Information); -builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); -builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Warning); -builder.Logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning); -builder.Logging.AddFilter("MudBlazor", LogLevel.Information); -builder.Logging.AddConsole(options => -{ - options.FormatterName = TerminalLogger.FORMATTER_NAME; -}).AddConsoleFormatter(); - -builder.Services.AddMudServices(config => -{ - config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; - config.SnackbarConfiguration.PreventDuplicates = false; - config.SnackbarConfiguration.NewestOnTop = false; - config.SnackbarConfiguration.ShowCloseIcon = true; - config.SnackbarConfiguration.VisibleStateDuration = 6_000; //milliseconds aka 6 seconds - config.SnackbarConfiguration.HideTransitionDuration = 500; - config.SnackbarConfiguration.ShowTransitionDuration = 500; - config.SnackbarConfiguration.SnackbarVariant = Variant.Outlined; -}); - -builder.Services.AddMudMarkdownServices(); -builder.Services.AddSingleton(MessageBus.INSTANCE); -builder.Services.AddSingleton(rust); -builder.Services.AddMudMarkdownClipboardService(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddHostedService(); -builder.Services.AddHostedService(); -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents() - .AddHubOptions(options => + public static RustService RUST_SERVICE = null!; + public static Encryption ENCRYPTION = null!; + + public static async Task Main(string[] args) { - options.MaximumReceiveMessageSize = null; - options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200); - options.HandshakeTimeout = TimeSpan.FromSeconds(30); - }); + if(args.Length == 0) + { + Console.WriteLine("Please provide the port of the runtime API."); + return; + } -builder.Services.AddSingleton(new HttpClient -{ - BaseAddress = new Uri($"http://localhost:{appPort}") -}); + var rustApiPort = args[0]; + using var rust = new RustService(rustApiPort); + var appPort = await rust.GetAppPort(); + if(appPort == 0) + { + Console.WriteLine("Failed to get the app port from Rust."); + return; + } -builder.WebHost.UseUrls($"http://localhost:{appPort}"); + // Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable: + var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD"); + if(string.IsNullOrWhiteSpace(secretPasswordEncoded)) + { + Console.WriteLine("The AI_STUDIO_SECRET_PASSWORD environment variable is not set."); + return; + } + + var secretPassword = Convert.FromBase64String(secretPasswordEncoded); + var secretKeySaltEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY_SALT"); + if(string.IsNullOrWhiteSpace(secretKeySaltEncoded)) + { + Console.WriteLine("The AI_STUDIO_SECRET_KEY_SALT environment variable is not set."); + return; + } + + var secretKeySalt = Convert.FromBase64String(secretKeySaltEncoded); + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + builder.Logging.SetMinimumLevel(LogLevel.Debug); + builder.Logging.AddFilter("Microsoft", LogLevel.Information); + builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); + builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Warning); + builder.Logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning); + builder.Logging.AddFilter("MudBlazor", LogLevel.Information); + builder.Logging.AddConsole(options => + { + options.FormatterName = TerminalLogger.FORMATTER_NAME; + }).AddConsoleFormatter(); + + builder.Services.AddMudServices(config => + { + config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; + config.SnackbarConfiguration.PreventDuplicates = false; + config.SnackbarConfiguration.NewestOnTop = false; + config.SnackbarConfiguration.ShowCloseIcon = true; + config.SnackbarConfiguration.VisibleStateDuration = 6_000; //milliseconds aka 6 seconds + config.SnackbarConfiguration.HideTransitionDuration = 500; + config.SnackbarConfiguration.ShowTransitionDuration = 500; + config.SnackbarConfiguration.SnackbarVariant = Variant.Outlined; + }); + + builder.Services.AddMudMarkdownServices(); + builder.Services.AddSingleton(MessageBus.INSTANCE); + builder.Services.AddSingleton(rust); + builder.Services.AddMudMarkdownClipboardService(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddHubOptions(options => + { + options.MaximumReceiveMessageSize = null; + options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200); + options.HandshakeTimeout = TimeSpan.FromSeconds(30); + }); + + builder.Services.AddSingleton(new HttpClient + { + BaseAddress = new Uri($"http://localhost:{appPort}") + }); + + builder.WebHost.UseUrls($"http://localhost:{appPort}"); + + #if DEBUG + builder.WebHost.UseWebRoot("wwwroot"); + builder.WebHost.UseStaticWebAssets(); + #endif + + // Execute the builder to get the app: + var app = builder.Build(); + + // Initialize 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: + var rustLogger = app.Services.GetRequiredService>(); + rust.SetLogger(rustLogger); + rust.SetEncryptor(encryption); + + RUST_SERVICE = rust; + ENCRYPTION = encryption; + + app.Use(Redirect.HandlerContentAsync); #if DEBUG -builder.WebHost.UseWebRoot("wwwroot"); -builder.WebHost.UseStaticWebAssets(); -#endif - -// Execute the builder to get the app: -var app = builder.Build(); - -// Initialize 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: -var rustLogger = app.Services.GetRequiredService>(); -rust.SetLogger(rustLogger); -rust.SetEncryptor(encryption); - -app.Use(Redirect.HandlerContentAsync); - -#if DEBUG -app.UseStaticFiles(); -app.UseDeveloperExceptionPage(); + app.UseStaticFiles(); + app.UseDeveloperExceptionPage(); #else var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot"); @@ -131,12 +141,14 @@ app.UseStaticFiles(new StaticFileOptions #endif -app.UseAntiforgery(); -app.MapRazorComponents() - .AddInteractiveServerRenderMode(); + app.UseAntiforgery(); + app.MapRazorComponents() + .AddInteractiveServerRenderMode(); -var serverTask = app.RunAsync(); + var serverTask = app.RunAsync(); -await encryptionInitializer; -await rust.AppIsReady(); -await serverTask; \ No newline at end of file + await encryptionInitializer; + await rust.AppIsReady(); + await serverTask; + } +} \ 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 642bbffb..0f561f19 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -25,7 +25,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap public async IAsyncEnumerable StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: - var requestedSecret = await settings.GetAPIKey(jsRuntime, this); + var requestedSecret = await RUST_SERVICE.GetAPIKey(this); if(!requestedSecret.Success) yield break; @@ -64,7 +64,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap var request = new HttpRequestMessage(HttpMethod.Post, "messages"); // Set the authorization header: - request.Headers.Add("x-api-key", requestedSecret.Secret); + request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); // Set the Anthropic version: request.Headers.Add("anthropic-version", "2023-06-01"); diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 913d0879..7e66fc13 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -1,3 +1,5 @@ +using RustService = AIStudio.Tools.RustService; + namespace AIStudio.Provider; /// @@ -15,6 +17,16 @@ public abstract class BaseProvider /// protected readonly ILogger logger; + static BaseProvider() + { + RUST_SERVICE = Program.RUST_SERVICE; + ENCRYPTION = Program.ENCRYPTION; + } + + protected static readonly RustService RUST_SERVICE; + + protected static readonly Encryption ENCRYPTION; + /// /// Constructor for the base provider. /// diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 190db509..65ae2528 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -27,7 +27,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew public async IAsyncEnumerable StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: - var requestedSecret = await settings.GetAPIKey(jsRuntime, this); + var requestedSecret = await RUST_SERVICE.GetAPIKey(this); if(!requestedSecret.Success) yield break; @@ -73,7 +73,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); // Set the content: request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json"); diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 160e0008..95c96059 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -26,7 +26,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api. public async IAsyncEnumerable StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: - var requestedSecret = await settings.GetAPIKey(jsRuntime, this); + var requestedSecret = await RUST_SERVICE.GetAPIKey(this); if(!requestedSecret.Success) yield break; @@ -75,7 +75,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api. var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); // Set the content: request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json"); @@ -153,9 +153,9 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api. var secretKey = apiKeyProvisional switch { not null => apiKeyProvisional, - _ => await settings.GetAPIKey(jsRuntime, this) switch + _ => await RUST_SERVICE.GetAPIKey(this) switch { - { Success: true } result => result.Secret, + { Success: true } result => await result.Secret.Decrypt(ENCRYPTION), _ => null, } }; diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index ca0c1769..fe1f19ba 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -30,7 +30,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o public async IAsyncEnumerable StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: - var requestedSecret = await settings.GetAPIKey(jsRuntime, this); + var requestedSecret = await RUST_SERVICE.GetAPIKey(this); if(!requestedSecret.Success) yield break; @@ -79,7 +79,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); // Set the content: request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json"); @@ -154,25 +154,25 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o /// public Task> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) { - return this.LoadModels(jsRuntime, settings, "gpt-", token, apiKeyProvisional); + return this.LoadModels("gpt-", token, apiKeyProvisional); } /// public Task> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) { - return this.LoadModels(jsRuntime, settings, "dall-e-", token, apiKeyProvisional); + return this.LoadModels("dall-e-", token, apiKeyProvisional); } #endregion - private async Task> LoadModels(IJSRuntime jsRuntime, SettingsManager settings, string prefix, CancellationToken token, string? apiKeyProvisional = null) + private async Task> LoadModels(string prefix, CancellationToken token, string? apiKeyProvisional = null) { var secretKey = apiKeyProvisional switch { not null => apiKeyProvisional, - _ => await settings.GetAPIKey(jsRuntime, this) switch + _ => await RUST_SERVICE.GetAPIKey(this) switch { - { Success: true } result => result.Secret, + { Success: true } result => await result.Secret.Decrypt(ENCRYPTION), _ => null, } }; diff --git a/app/MindWork AI Studio/Provider/ProvidersExtensions.cs b/app/MindWork AI Studio/Provider/ProvidersExtensions.cs index 806c3bb8..f5aea99b 100644 --- a/app/MindWork AI Studio/Provider/ProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/ProvidersExtensions.cs @@ -4,6 +4,8 @@ using AIStudio.Provider.Mistral; using AIStudio.Provider.OpenAI; using AIStudio.Provider.SelfHosted; +using RustService = AIStudio.Tools.RustService; + namespace AIStudio.Provider; public static class ProvidersExtensions @@ -33,8 +35,9 @@ public static class ProvidersExtensions /// /// The provider settings. /// The logger to use. + /// The Rust instance to use. /// The provider instance. - public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger) + public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger, RustService rustService) { try { diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs index d62c7770..c7cdc887 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs @@ -5,7 +5,6 @@ using AIStudio.Assistants.RewriteImprove; using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.EMail; using AIStudio.Settings.DataModel; -using AIStudio.Tools; using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles; using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs b/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs index aa59871d..46ef668f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs @@ -1,5 +1,4 @@ using AIStudio.Assistants.Agenda; -using AIStudio.Tools; namespace AIStudio.Settings.DataModel; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs b/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs index 710507f4..ab659fcf 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs @@ -1,5 +1,4 @@ using AIStudio.Assistants.EMail; -using AIStudio.Tools; namespace AIStudio.Settings.DataModel; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataGrammarSpelling.cs b/app/MindWork AI Studio/Settings/DataModel/DataGrammarSpelling.cs index cd74c0b4..01cc0f63 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataGrammarSpelling.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataGrammarSpelling.cs @@ -1,5 +1,3 @@ -using AIStudio.Tools; - namespace AIStudio.Settings.DataModel; public sealed class DataGrammarSpelling diff --git a/app/MindWork AI Studio/Settings/DataModel/DataRewriteImprove.cs b/app/MindWork AI Studio/Settings/DataModel/DataRewriteImprove.cs index 48742de7..738d63a4 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataRewriteImprove.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataRewriteImprove.cs @@ -1,5 +1,4 @@ using AIStudio.Assistants.RewriteImprove; -using AIStudio.Tools; namespace AIStudio.Settings.DataModel; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataTextSummarizer.cs b/app/MindWork AI Studio/Settings/DataModel/DataTextSummarizer.cs index b3ad839d..6074a77f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataTextSummarizer.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataTextSummarizer.cs @@ -1,5 +1,4 @@ using AIStudio.Assistants.TextSummarizer; -using AIStudio.Tools; namespace AIStudio.Settings.DataModel; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataTranslation.cs b/app/MindWork AI Studio/Settings/DataModel/DataTranslation.cs index 26c5aba9..a861dca8 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataTranslation.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataTranslation.cs @@ -1,5 +1,3 @@ -using AIStudio.Tools; - namespace AIStudio.Settings.DataModel; public sealed class DataTranslation diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV1V3.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV1V3.cs index 3419ccb3..0b1c7883 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV1V3.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV1V3.cs @@ -1,7 +1,6 @@ using AIStudio.Assistants.Coding; using AIStudio.Assistants.IconFinder; using AIStudio.Assistants.TextSummarizer; -using AIStudio.Tools; namespace AIStudio.Settings.DataModel.PreviousModels; diff --git a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs index 1adb775e..e228368d 100644 --- a/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Settings/ProviderDialog.razor.cs @@ -5,6 +5,7 @@ using AIStudio.Provider; using Microsoft.AspNetCore.Components; using Host = AIStudio.Provider.SelfHosted.Host; +using RustService = AIStudio.Tools.RustService; namespace AIStudio.Settings; @@ -78,6 +79,12 @@ public partial class ProviderDialog : ComponentBase [Inject] private ILogger Logger { get; init; } = null!; + + [Inject] + private RustService RustService { get; init; } = null!; + + [Inject] + private Encryption Encryption { get; init; } = null!; private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); @@ -136,7 +143,7 @@ public partial class ProviderDialog : ComponentBase } var loadedProviderSettings = this.CreateProviderSettings(); - var provider = loadedProviderSettings.CreateProvider(this.Logger); + var provider = loadedProviderSettings.CreateProvider(this.Logger, this.RustService); if(provider is NoProvider) { await base.OnInitializedAsync(); @@ -144,10 +151,10 @@ public partial class ProviderDialog : ComponentBase } // Load the API key: - var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider); + var requestedSecret = await this.RustService.GetAPIKey(provider); if(requestedSecret.Success) { - this.dataAPIKey = requestedSecret.Secret; + this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.Encryption); // Now, we try to load the list of available models: await this.ReloadModels(); @@ -190,7 +197,7 @@ public partial class ProviderDialog : ComponentBase if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED) { // We need to instantiate the provider to store the API key: - var provider = addedProviderSettings.CreateProvider(this.Logger); + var provider = addedProviderSettings.CreateProvider(this.Logger, this.RustService); // Store the API key in the OS secure storage: var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey); @@ -321,7 +328,7 @@ public partial class ProviderDialog : ComponentBase private async Task ReloadModels() { var currentProviderSettings = this.CreateProviderSettings(); - var provider = currentProviderSettings.CreateProvider(this.Logger); + var provider = currentProviderSettings.CreateProvider(this.Logger, this.RustService); if(provider is NoProvider) return; diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 14131474..cfdddccc 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -41,24 +41,6 @@ public sealed class SettingsManager(ILogger logger) private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory); #region API Key Handling - - 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); diff --git a/app/MindWork AI Studio/Tools/Encryption.cs b/app/MindWork AI Studio/Tools/Encryption.cs index 8c96b609..91cc71ae 100644 --- a/app/MindWork AI Studio/Tools/Encryption.cs +++ b/app/MindWork AI Studio/Tools/Encryption.cs @@ -93,20 +93,21 @@ public sealed class Encryption(ILogger logger, byte[] secretPassword public async Task Decrypt(EncryptedText encryptedData) { // Build a memory stream to access the given base64 encoded data: - await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData)); + await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData.EncryptedData)); // Wrap around the base64 decoder stream: await using var base64Stream = new CryptoStream(encodedEncryptedStream, new FromBase64Transform(), CryptoStreamMode.Read); // A buffer for the salt's bytes: var readSaltBytes = new byte[16]; // 16 bytes = Guid - + // Read the salt's bytes out of the stream: - var readBytes = await base64Stream.ReadAsync(readSaltBytes); - if(readBytes != 16) + var readBytes = 0; + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + while(readBytes < readSaltBytes.Length && !cts.Token.IsCancellationRequested) { - logger.LogError($"Read {readBytes} bytes instead of 16 bytes for the salt."); - throw new CryptographicException("Failed to read the salt bytes."); + readBytes += await base64Stream.ReadAsync(readSaltBytes, readBytes, readSaltBytes.Length - readBytes, cts.Token); + await Task.Delay(TimeSpan.FromMilliseconds(60), cts.Token); } // Check the salt bytes: diff --git a/app/MindWork AI Studio/Tools/Rust/GetSecretRequest.cs b/app/MindWork AI Studio/Tools/Rust/GetSecretRequest.cs new file mode 100644 index 00000000..0fd4283d --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/GetSecretRequest.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Tools.Rust; + +public readonly record struct GetSecretRequest( + string Destination, + [property:JsonPropertyName("user_name")] string UserName); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs b/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs new file mode 100644 index 00000000..ce55a784 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.Rust; + +/// +/// 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, EncryptedText Secret, string Issue); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/SetClipboardResponse.cs b/app/MindWork AI Studio/Tools/Rust/SetClipboardResponse.cs similarity index 87% rename from app/MindWork AI Studio/Tools/SetClipboardResponse.cs rename to app/MindWork AI Studio/Tools/Rust/SetClipboardResponse.cs index e7a8fcc3..c6d256c8 100644 --- a/app/MindWork AI Studio/Tools/SetClipboardResponse.cs +++ b/app/MindWork AI Studio/Tools/Rust/SetClipboardResponse.cs @@ -1,4 +1,4 @@ -namespace AIStudio.Tools; +namespace AIStudio.Tools.Rust; /// /// The response from the set clipboard operation. diff --git a/app/MindWork AI Studio/Tools/UpdateResponse.cs b/app/MindWork AI Studio/Tools/Rust/UpdateResponse.cs similarity index 94% rename from app/MindWork AI Studio/Tools/UpdateResponse.cs rename to app/MindWork AI Studio/Tools/Rust/UpdateResponse.cs index 5a5e3e2b..392562fa 100644 --- a/app/MindWork AI Studio/Tools/UpdateResponse.cs +++ b/app/MindWork AI Studio/Tools/Rust/UpdateResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace AIStudio.Tools; +namespace AIStudio.Tools.Rust; /// /// The response of the update check. diff --git a/app/MindWork AI Studio/Tools/Rust.cs b/app/MindWork AI Studio/Tools/RustService.cs similarity index 77% rename from app/MindWork AI Studio/Tools/Rust.cs rename to app/MindWork AI Studio/Tools/RustService.cs index caf858a1..b84e5471 100644 --- a/app/MindWork AI Studio/Tools/Rust.cs +++ b/app/MindWork AI Studio/Tools/RustService.cs @@ -1,19 +1,24 @@ +using AIStudio.Provider; +using AIStudio.Tools.Rust; + +// ReSharper disable NotAccessedPositionalProperty.Local + namespace AIStudio.Tools; /// /// Calling Rust functions. /// -public sealed class Rust(string apiPort) : IDisposable +public sealed class RustService(string apiPort) : IDisposable { private readonly HttpClient http = new() { BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"), }; - private ILogger? logger; + private ILogger? logger; private Encryption? encryptor; - public void SetLogger(ILogger logService) + public void SetLogger(ILogger logService) { this.logger = logService; } @@ -87,7 +92,8 @@ public sealed class Rust(string apiPort) : IDisposable var severity = Severity.Error; try { - var response = await this.http.PostAsync("/clipboard/set", new StringContent(await text.Encrypt(this.encryptor!))); + var encryptedText = await text.Encrypt(this.encryptor!); + var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData)); if (!response.IsSuccessStatusCode) { this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'"); @@ -136,7 +142,7 @@ public sealed class Rust(string apiPort) : IDisposable } } - public async Task InstallUpdate(IJSRuntime jsRuntime) + public async Task InstallUpdate() { try { @@ -149,6 +155,28 @@ public sealed class Rust(string apiPort) : IDisposable throw; } } + + /// + /// Try to get the API key for the given provider. + /// + /// The provider to get the API key for. + /// The requested secret. + public async Task GetAPIKey(IProvider provider) + { + var secretRequest = new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName); + var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'"); + return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue."); + } + + var secret = await result.Content.ReadFromJsonAsync(); + if (!secret.Success) + this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'"); + + return secret; + } #region IDisposable diff --git a/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs b/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs index 4b566ab4..b597aa5b 100644 --- a/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs +++ b/app/MindWork AI Studio/Tools/Services/MarkdownClipboardService.cs @@ -6,11 +6,11 @@ namespace AIStudio.Tools.Services; /// Wire up the clipboard service to copy Markdown to the clipboard. /// We use our own Rust-based clipboard service for this. /// -public sealed class MarkdownClipboardService(Rust rust, ISnackbar snackbar) : IMudMarkdownClipboardService +public sealed class MarkdownClipboardService(RustService rust, ISnackbar snackbar) : IMudMarkdownClipboardService { private ISnackbar Snackbar { get; } = snackbar; - private Rust Rust { get; } = rust; + private RustService Rust { get; } = rust; /// /// Gets called when the user wants to copy the Markdown to the clipboard. diff --git a/app/MindWork AI Studio/Tools/Services/UpdateService.cs b/app/MindWork AI Studio/Tools/Services/UpdateService.cs index df163849..8de0690d 100644 --- a/app/MindWork AI Studio/Tools/Services/UpdateService.cs +++ b/app/MindWork AI Studio/Tools/Services/UpdateService.cs @@ -12,11 +12,11 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver private readonly SettingsManager settingsManager; private readonly MessageBus messageBus; - private readonly Rust rust; + private readonly RustService rust; private TimeSpan updateInterval; - public UpdateService(MessageBus messageBus, SettingsManager settingsManager, Rust rust) + public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust) { this.settingsManager = settingsManager; this.messageBus = messageBus; diff --git a/app/MindWork AI Studio/Tools/SetClipboardText.cs b/app/MindWork AI Studio/Tools/SetClipboardText.cs deleted file mode 100644 index 9776d29b..00000000 --- a/app/MindWork AI Studio/Tools/SetClipboardText.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace AIStudio.Tools; - -/// -/// Model for setting clipboard text. -/// -/// The text to set to the clipboard. -public record SetClipboardText(string Text); \ No newline at end of file diff --git a/runtime/src/main.rs b/runtime/src/main.rs index ca1e6f26..8887089a 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -194,7 +194,7 @@ async fn main() { // tauri::async_runtime::spawn(async move { _ = rocket::custom(figment) - .mount("/", routes![dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update]) + .mount("/", routes![dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update, get_secret]) .ignite().await.unwrap() .launch().await.unwrap(); }); @@ -319,7 +319,7 @@ async fn main() { }) .plugin(tauri_plugin_window_state::Builder::default().build()) .invoke_handler(tauri::generate_handler![ - store_secret, get_secret, delete_secret + store_secret, delete_secret ]) .build(tauri::generate_context!()) .expect("Error while running Tauri application"); @@ -776,36 +776,57 @@ struct StoreSecretResponse { issue: String, } -#[tauri::command] -fn get_secret(destination: String, user_name: String) -> RequestedSecret { - let service = format!("mindwork-ai-studio::{}", destination); - let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap(); +#[post("/secrets/get", data = "")] +fn get_secret(request: Json) -> Json { + let user_name = request.user_name.as_str(); + let service = format!("mindwork-ai-studio::{}", request.destination); + let entry = Entry::new(service.as_str(), user_name).unwrap(); let secret = entry.get_password(); match secret { Ok(s) => { - info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was retrieved successfully."); - RequestedSecret { + info!(Source = "Secret Store"; "Secret for '{service}' and user '{user_name}' was retrieved successfully."); + + // Encrypt the secret: + let encrypted_secret = match ENCRYPTION.encrypt(s.as_str()) { + Ok(e) => e, + Err(e) => { + error!(Source = "Secret Store"; "Failed to encrypt the secret: {e}."); + return Json(RequestedSecret { + success: false, + secret: EncryptedText::new(String::from("")), + issue: format!("Failed to encrypt the secret: {e}"), + }); + }, + }; + + Json(RequestedSecret { success: true, - secret: s, + secret: encrypted_secret, issue: String::from(""), - } + }) }, Err(e) => { - error!(Source = "Secret Store"; "Failed to retrieve secret for {service} and user {user_name}: {e}."); - RequestedSecret { + error!(Source = "Secret Store"; "Failed to retrieve secret for '{service}' and user '{user_name}': {e}."); + Json(RequestedSecret { success: false, - secret: String::from(""), - issue: e.to_string(), - } + secret: EncryptedText::new(String::from("")), + issue: format!("Failed to retrieve secret for '{service}' and user '{user_name}': {e}"), + }) }, } } +#[derive(Deserialize)] +struct RequestSecret { + destination: String, + user_name: String, +} + #[derive(Serialize)] struct RequestedSecret { success: bool, - secret: String, + secret: EncryptedText, issue: String, }