mirror of
				https://github.com/MindWorkAI/AI-Studio.git
				synced 2025-11-04 00:00:21 +00:00 
			
		
		
		
	Refactored logging into a unified logging
This commit is contained in:
		
							parent
							
								
									200b87f47a
								
							
						
					
					
						commit
						82f7329a39
					
				@ -7,7 +7,7 @@ using AIStudio.Tools;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Agents;
 | 
			
		||||
 | 
			
		||||
public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
 | 
			
		||||
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
 | 
			
		||||
{
 | 
			
		||||
    protected SettingsManager SettingsManager { get; init; } = settingsManager;
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,8 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
 | 
			
		||||
 | 
			
		||||
    protected ThreadSafeRandom RNG { get; init; } = rng;
 | 
			
		||||
    
 | 
			
		||||
    protected ILogger<AgentBase> Logger { get; init; } = logger;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Represents the type or category of this agent.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@ -104,6 +106,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
 | 
			
		||||
        // 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.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
 | 
			
		||||
        await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -4,7 +4,7 @@ using AIStudio.Tools;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Agents;
 | 
			
		||||
 | 
			
		||||
public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng)
 | 
			
		||||
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng)
 | 
			
		||||
{
 | 
			
		||||
    private static readonly ContentBlock EMPTY_BLOCK = new()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ namespace AIStudio.Assistants;
 | 
			
		||||
public abstract partial class AssistantBase : ComponentBase
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected SettingsManager SettingsManager { get; set; } = null!;
 | 
			
		||||
    protected SettingsManager SettingsManager { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
@ -27,6 +27,9 @@ public abstract partial class AssistantBase : ComponentBase
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected NavigationManager NavigationManager { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected ILogger<AssistantBase> Logger { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
 | 
			
		||||
    internal const string RESULT_DIV_ID = "assistantResult";
 | 
			
		||||
    
 | 
			
		||||
@ -151,7 +154,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.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
 | 
			
		||||
        await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
 | 
			
		||||
        
 | 
			
		||||
        this.isProcessing = false;
 | 
			
		||||
        this.StateHasChanged();
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,9 @@ public partial class Workspaces : ComponentBase
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private ThreadSafeRandom RNG { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private ILogger<Workspaces> Logger { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    public ChatThread? CurrentChatThread { get; set; }
 | 
			
		||||
    
 | 
			
		||||
@ -309,7 +312,7 @@ public partial class Workspaces : ComponentBase
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception e)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine(e);
 | 
			
		||||
            this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return null;
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ namespace AIStudio.Pages;
 | 
			
		||||
public partial class Chat : MSGComponentBase, IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private SettingsManager SettingsManager { get; set; } = null!;
 | 
			
		||||
    private SettingsManager SettingsManager { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
@ -28,7 +28,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
 | 
			
		||||
    private ThreadSafeRandom RNG { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public IDialogService DialogService { get; set; } = null!;
 | 
			
		||||
    private IDialogService DialogService { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private ILogger<Chat> Logger { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    private InnerScrolling scrollingArea = null!;
 | 
			
		||||
 | 
			
		||||
@ -189,7 +192,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.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
 | 
			
		||||
        await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
 | 
			
		||||
        
 | 
			
		||||
        // Save the chat:
 | 
			
		||||
        if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
 | 
			
		||||
 | 
			
		||||
@ -14,16 +14,19 @@ namespace AIStudio.Pages;
 | 
			
		||||
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public SettingsManager SettingsManager { get; init; } = null!;
 | 
			
		||||
    private SettingsManager SettingsManager { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public IDialogService DialogService { get; init; } = null!;
 | 
			
		||||
    private IDialogService DialogService { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
    private IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected MessageBus MessageBus { get; init; } = null!;
 | 
			
		||||
    private MessageBus MessageBus { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private ILogger<Settings> Logger { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    private readonly List<ConfigurationSelectData<string>> availableProviders = new();
 | 
			
		||||
 | 
			
		||||
@ -111,7 +114,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
 | 
			
		||||
        if (dialogResult is null || dialogResult.Canceled)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        var providerInstance = provider.CreateProvider();
 | 
			
		||||
        var providerInstance = provider.CreateProvider(this.Logger);
 | 
			
		||||
        var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
 | 
			
		||||
        if(deleteSecretResponse.Success)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ using AIStudio.Settings;
 | 
			
		||||
using AIStudio.Tools;
 | 
			
		||||
using AIStudio.Tools.Services;
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging.Console;
 | 
			
		||||
 | 
			
		||||
using MudBlazor.Services;
 | 
			
		||||
 | 
			
		||||
#if !DEBUG
 | 
			
		||||
@ -35,6 +37,18 @@ if(string.IsNullOrWhiteSpace(secretKey))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<TerminalLogger, ConsoleFormatterOptions>();
 | 
			
		||||
 | 
			
		||||
builder.Services.AddMudServices(config =>
 | 
			
		||||
{
 | 
			
		||||
    config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
 | 
			
		||||
@ -101,5 +115,8 @@ app.MapRazorComponents<App>()
 | 
			
		||||
 | 
			
		||||
var serverTask = app.RunAsync();
 | 
			
		||||
 | 
			
		||||
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
 | 
			
		||||
rust.SetLogger(rustLogger);
 | 
			
		||||
 | 
			
		||||
await rust.AppIsReady();
 | 
			
		||||
await serverTask;
 | 
			
		||||
@ -8,7 +8,7 @@ using AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Provider.Anthropic;
 | 
			
		||||
 | 
			
		||||
public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider
 | 
			
		||||
public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://api.anthropic.com/v1/", logger), IProvider
 | 
			
		||||
{
 | 
			
		||||
    private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -10,12 +10,20 @@ public abstract class BaseProvider
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected readonly HttpClient httpClient = new();
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The logger to use.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected readonly ILogger logger;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Constructor for the base provider.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="url">The base URL for the provider.</param>
 | 
			
		||||
    protected BaseProvider(string url)
 | 
			
		||||
    /// <param name="loggerService">The logger service to use.</param>
 | 
			
		||||
    protected BaseProvider(string url, ILogger loggerService)
 | 
			
		||||
    {
 | 
			
		||||
        this.logger = loggerService;
 | 
			
		||||
 | 
			
		||||
        // Set the base URL:
 | 
			
		||||
        this.httpClient.BaseAddress = new(url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ using AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Provider.Fireworks;
 | 
			
		||||
 | 
			
		||||
public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider
 | 
			
		||||
public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger), IProvider
 | 
			
		||||
{
 | 
			
		||||
    private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ using AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Provider.Mistral;
 | 
			
		||||
 | 
			
		||||
public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/"), IProvider
 | 
			
		||||
public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.mistral.ai/v1/", logger), IProvider
 | 
			
		||||
{
 | 
			
		||||
    private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// The OpenAI provider.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"), IProvider
 | 
			
		||||
public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.openai.com/v1/", logger), IProvider
 | 
			
		||||
{
 | 
			
		||||
    private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,3 @@
 | 
			
		||||
using AIStudio.Provider.Anthropic;
 | 
			
		||||
using AIStudio.Provider.Fireworks;
 | 
			
		||||
using AIStudio.Provider.Mistral;
 | 
			
		||||
using AIStudio.Provider.OpenAI;
 | 
			
		||||
using AIStudio.Provider.SelfHosted;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Provider;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@ -21,58 +15,3 @@ public enum Providers
 | 
			
		||||
    
 | 
			
		||||
    SELF_HOSTED = 4,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Extension methods for the provider enum.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class ExtensionsProvider
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns the human-readable name of the provider.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="provider">The provider.</param>
 | 
			
		||||
    /// <returns>The human-readable name of the provider.</returns>
 | 
			
		||||
    public static string ToName(this Providers provider) => provider switch
 | 
			
		||||
    {
 | 
			
		||||
        Providers.NONE => "No provider selected",
 | 
			
		||||
        
 | 
			
		||||
        Providers.OPEN_AI => "OpenAI",
 | 
			
		||||
        Providers.ANTHROPIC => "Anthropic",
 | 
			
		||||
        Providers.MISTRAL => "Mistral",
 | 
			
		||||
        
 | 
			
		||||
        Providers.FIREWORKS => "Fireworks.ai",
 | 
			
		||||
        
 | 
			
		||||
        Providers.SELF_HOSTED => "Self-hosted",
 | 
			
		||||
        
 | 
			
		||||
        _ => "Unknown",
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a new provider instance based on the provider value.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="providerSettings">The provider settings.</param>
 | 
			
		||||
    /// <returns>The provider instance.</returns>
 | 
			
		||||
    public static IProvider CreateProvider(this Settings.Provider providerSettings)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return providerSettings.UsedProvider switch
 | 
			
		||||
            {
 | 
			
		||||
                Providers.OPEN_AI => new ProviderOpenAI { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                Providers.MISTRAL => new ProviderMistral { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                
 | 
			
		||||
                Providers.FIREWORKS => new ProviderFireworks { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                
 | 
			
		||||
                Providers.SELF_HOSTED => new ProviderSelfHosted(providerSettings) { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                
 | 
			
		||||
                _ => new NoProvider(),
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception e)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine($"Failed to create provider: {e.Message}");
 | 
			
		||||
            return new NoProvider();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								app/MindWork AI Studio/Provider/ProvidersExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/MindWork AI Studio/Provider/ProvidersExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
using AIStudio.Provider.Anthropic;
 | 
			
		||||
using AIStudio.Provider.Fireworks;
 | 
			
		||||
using AIStudio.Provider.Mistral;
 | 
			
		||||
using AIStudio.Provider.OpenAI;
 | 
			
		||||
using AIStudio.Provider.SelfHosted;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Provider;
 | 
			
		||||
 | 
			
		||||
public static class ProvidersExtensions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns the human-readable name of the provider.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="provider">The provider.</param>
 | 
			
		||||
    /// <returns>The human-readable name of the provider.</returns>
 | 
			
		||||
    public static string ToName(this Providers provider) => provider switch
 | 
			
		||||
    {
 | 
			
		||||
        Providers.NONE => "No provider selected",
 | 
			
		||||
        
 | 
			
		||||
        Providers.OPEN_AI => "OpenAI",
 | 
			
		||||
        Providers.ANTHROPIC => "Anthropic",
 | 
			
		||||
        Providers.MISTRAL => "Mistral",
 | 
			
		||||
        
 | 
			
		||||
        Providers.FIREWORKS => "Fireworks.ai",
 | 
			
		||||
        
 | 
			
		||||
        Providers.SELF_HOSTED => "Self-hosted",
 | 
			
		||||
        
 | 
			
		||||
        _ => "Unknown",
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a new provider instance based on the provider value.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="providerSettings">The provider settings.</param>
 | 
			
		||||
    /// <param name="logger">The logger to use.</param>
 | 
			
		||||
    /// <returns>The provider instance.</returns>
 | 
			
		||||
    public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return providerSettings.UsedProvider switch
 | 
			
		||||
            {
 | 
			
		||||
                Providers.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                Providers.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                Providers.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                
 | 
			
		||||
                Providers.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                
 | 
			
		||||
                Providers.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName },
 | 
			
		||||
                
 | 
			
		||||
                _ => new NoProvider(),
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception e)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError($"Failed to create provider: {e.Message}");
 | 
			
		||||
            return new NoProvider();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -8,7 +8,7 @@ using AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Provider.SelfHosted;
 | 
			
		||||
 | 
			
		||||
public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider
 | 
			
		||||
public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}", logger), IProvider
 | 
			
		||||
{
 | 
			
		||||
    private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
 | 
			
		||||
    {
 | 
			
		||||
@ -162,7 +162,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
 | 
			
		||||
        }
 | 
			
		||||
        catch(Exception e)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine($"Failed to load text models from self-hosted provider: {e.Message}");
 | 
			
		||||
            this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase
 | 
			
		||||
    public bool IsEditing { get; init; }
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private SettingsManager SettingsManager { get; set; } = null!;
 | 
			
		||||
    private SettingsManager SettingsManager { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private IJSRuntime JsRuntime { get; set; } = null!;
 | 
			
		||||
    private IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private ILogger<ProviderDialog> Logger { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
 | 
			
		||||
    
 | 
			
		||||
@ -133,7 +136,7 @@ public partial class ProviderDialog : ComponentBase
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var loadedProviderSettings = this.CreateProviderSettings();
 | 
			
		||||
            var provider = loadedProviderSettings.CreateProvider();
 | 
			
		||||
            var provider = loadedProviderSettings.CreateProvider(this.Logger);
 | 
			
		||||
            if(provider is NoProvider)
 | 
			
		||||
            {
 | 
			
		||||
                await base.OnInitializedAsync();
 | 
			
		||||
@ -187,7 +190,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();
 | 
			
		||||
            var provider = addedProviderSettings.CreateProvider(this.Logger);
 | 
			
		||||
            
 | 
			
		||||
            // Store the API key in the OS secure storage:
 | 
			
		||||
            var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
 | 
			
		||||
@ -318,7 +321,7 @@ public partial class ProviderDialog : ComponentBase
 | 
			
		||||
    private async Task ReloadModels()
 | 
			
		||||
    {
 | 
			
		||||
        var currentProviderSettings = this.CreateProviderSettings();
 | 
			
		||||
        var provider = currentProviderSettings.CreateProvider();
 | 
			
		||||
        var provider = currentProviderSettings.CreateProvider(this.Logger);
 | 
			
		||||
        if(provider is NoProvider)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ namespace AIStudio.Settings;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// The settings manager.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class SettingsManager
 | 
			
		||||
public sealed class SettingsManager(ILogger<SettingsManager> logger)
 | 
			
		||||
{
 | 
			
		||||
    private const string SETTINGS_FILENAME = "settings.json";
 | 
			
		||||
    
 | 
			
		||||
@ -21,6 +21,8 @@ public sealed class SettingsManager
 | 
			
		||||
        Converters = { new JsonStringEnumConverter() },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private ILogger<SettingsManager> logger = logger;
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The directory where the configuration files are stored.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@ -102,11 +104,17 @@ public sealed class SettingsManager
 | 
			
		||||
    public async Task LoadSettings()
 | 
			
		||||
    {
 | 
			
		||||
        if(!this.IsSetUp)
 | 
			
		||||
        {
 | 
			
		||||
            this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
 | 
			
		||||
        if(!File.Exists(settingsPath))
 | 
			
		||||
        {
 | 
			
		||||
            this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We read the `"Version": "V3"` line to determine the version of the settings file:
 | 
			
		||||
        await foreach (var line in File.ReadLinesAsync(settingsPath))
 | 
			
		||||
@ -123,16 +131,16 @@ public sealed class SettingsManager
 | 
			
		||||
            Enum.TryParse(settingsVersionText, out Version settingsVersion);
 | 
			
		||||
            if(settingsVersion is Version.UNKNOWN)
 | 
			
		||||
            {
 | 
			
		||||
                Console.WriteLine("Error: Unknown version of the settings file.");
 | 
			
		||||
                this.logger.LogError("Unknown version of the settings file found.");
 | 
			
		||||
                this.ConfigurationData = new();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
                
 | 
			
		||||
            this.ConfigurationData = SettingsMigrations.Migrate(settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
 | 
			
		||||
            this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Console.WriteLine("Error: Failed to read the version of the settings file.");
 | 
			
		||||
        this.logger.LogError("Failed to read the version of the settings file.");
 | 
			
		||||
        this.ConfigurationData = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -142,14 +150,22 @@ public sealed class SettingsManager
 | 
			
		||||
    public async Task StoreSettings()
 | 
			
		||||
    {
 | 
			
		||||
        if(!this.IsSetUp)
 | 
			
		||||
        {
 | 
			
		||||
            this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
 | 
			
		||||
        if(!Directory.Exists(ConfigDirectory))
 | 
			
		||||
        {
 | 
			
		||||
            this.logger.LogInformation("Creating the configuration directory.");
 | 
			
		||||
            Directory.CreateDirectory(ConfigDirectory!);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
 | 
			
		||||
        await File.WriteAllTextAsync(settingsPath, settingsJson);
 | 
			
		||||
        
 | 
			
		||||
        this.logger.LogInformation("Stored the settings to the file system.");
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false";
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ namespace AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
public static class SettingsMigrations
 | 
			
		||||
{
 | 
			
		||||
    public static Data Migrate(Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
 | 
			
		||||
    public static Data Migrate(ILogger<SettingsManager> logger, Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
 | 
			
		||||
    {
 | 
			
		||||
        switch (previousVersion)
 | 
			
		||||
        {
 | 
			
		||||
@ -17,41 +17,41 @@ public static class SettingsMigrations
 | 
			
		||||
                var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
 | 
			
		||||
                if (configV1 is null)
 | 
			
		||||
                {
 | 
			
		||||
                    Console.WriteLine("Error: failed to parse the configuration. Using default values.");
 | 
			
		||||
                    logger.LogError("Failed to parse the v1 configuration. Using default values.");
 | 
			
		||||
                    return new();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                configV1 = MigrateV1ToV2(configV1);
 | 
			
		||||
                configV1 = MigrateV2ToV3(configV1);
 | 
			
		||||
                return MigrateV3ToV4(configV1);
 | 
			
		||||
                configV1 = MigrateV1ToV2(logger, configV1);
 | 
			
		||||
                configV1 = MigrateV2ToV3(logger, configV1);
 | 
			
		||||
                return MigrateV3ToV4(logger, configV1);
 | 
			
		||||
 | 
			
		||||
            case Version.V2:
 | 
			
		||||
                var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
 | 
			
		||||
                if (configV2 is null)
 | 
			
		||||
                {
 | 
			
		||||
                    Console.WriteLine("Error: failed to parse the configuration. Using default values.");
 | 
			
		||||
                    logger.LogError("Failed to parse the v2 configuration. Using default values.");
 | 
			
		||||
                    return new();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                configV2 = MigrateV2ToV3(configV2);
 | 
			
		||||
                return MigrateV3ToV4(configV2);
 | 
			
		||||
                configV2 = MigrateV2ToV3(logger, configV2);
 | 
			
		||||
                return MigrateV3ToV4(logger, configV2);
 | 
			
		||||
 | 
			
		||||
            case Version.V3:
 | 
			
		||||
                var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
 | 
			
		||||
                if (configV3 is null)
 | 
			
		||||
                {
 | 
			
		||||
                    Console.WriteLine("Error: failed to parse the configuration. Using default values.");
 | 
			
		||||
                    logger.LogError("Failed to parse the v3 configuration. Using default values.");
 | 
			
		||||
                    return new();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return MigrateV3ToV4(configV3);
 | 
			
		||||
                return MigrateV3ToV4(logger, configV3);
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                Console.WriteLine("No configuration migration needed.");
 | 
			
		||||
                logger.LogInformation("No configuration migration is needed.");
 | 
			
		||||
                var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
 | 
			
		||||
                if (configV4 is null)
 | 
			
		||||
                {
 | 
			
		||||
                    Console.WriteLine("Error: failed to parse the configuration. Using default values.");
 | 
			
		||||
                    logger.LogError("Failed to parse the v4 configuration. Using default values.");
 | 
			
		||||
                    return new();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -59,14 +59,14 @@ public static class SettingsMigrations
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData)
 | 
			
		||||
    private static DataV1V3 MigrateV1ToV2(ILogger<SettingsManager> logger, DataV1V3 previousData)
 | 
			
		||||
    {
 | 
			
		||||
        //
 | 
			
		||||
        // Summary:
 | 
			
		||||
        // In v1 we had no self-hosted providers. Thus, we had no hostnames.
 | 
			
		||||
        //
 | 
			
		||||
 | 
			
		||||
        Console.WriteLine("Migrating from v1 to v2...");
 | 
			
		||||
        logger.LogInformation("Migrating from v1 to v2...");
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Version = Version.V2,
 | 
			
		||||
@ -81,14 +81,14 @@ public static class SettingsMigrations
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private static DataV1V3 MigrateV2ToV3(DataV1V3 previousData)
 | 
			
		||||
    private static DataV1V3 MigrateV2ToV3(ILogger<SettingsManager> logger, DataV1V3 previousData)
 | 
			
		||||
    {
 | 
			
		||||
        //
 | 
			
		||||
        // Summary:
 | 
			
		||||
        // In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
 | 
			
		||||
        //
 | 
			
		||||
 | 
			
		||||
        Console.WriteLine("Migrating from v2 to v3...");
 | 
			
		||||
        logger.LogInformation("Migrating from v2 to v3...");
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Version = Version.V3,
 | 
			
		||||
@ -110,14 +110,14 @@ public static class SettingsMigrations
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Data MigrateV3ToV4(DataV1V3 previousConfig)
 | 
			
		||||
    private static Data MigrateV3ToV4(ILogger<SettingsManager> logger, DataV1V3 previousConfig)
 | 
			
		||||
    {
 | 
			
		||||
        //
 | 
			
		||||
        // Summary:
 | 
			
		||||
        // We grouped the settings into different categories.
 | 
			
		||||
        //
 | 
			
		||||
        
 | 
			
		||||
        Console.WriteLine("Migrating from v3 to v4...");
 | 
			
		||||
        logger.LogInformation("Migrating from v3 to v4...");
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Version = Version.V4,
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,13 @@ public sealed class Rust(string apiPort) : IDisposable
 | 
			
		||||
        BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private ILogger<Rust>? logger;
 | 
			
		||||
    
 | 
			
		||||
    public void SetLogger(ILogger<Rust> logService)
 | 
			
		||||
    {
 | 
			
		||||
        this.logger = logService;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<int> GetAppPort()
 | 
			
		||||
    {
 | 
			
		||||
        Console.WriteLine("Trying to get app port from Rust runtime...");
 | 
			
		||||
@ -36,14 +43,14 @@ public sealed class Rust(string apiPort) : IDisposable
 | 
			
		||||
            var response = await initialHttp.GetAsync(url);
 | 
			
		||||
            if (!response.IsSuccessStatusCode)
 | 
			
		||||
            {
 | 
			
		||||
                Console.WriteLine($"   Try {tris}/{MAX_TRIES}");
 | 
			
		||||
                Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
 | 
			
		||||
                await Task.Delay(wait4Try);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var appPortContent = await response.Content.ReadAsStringAsync();
 | 
			
		||||
            var appPort = int.Parse(appPortContent);
 | 
			
		||||
            Console.WriteLine($"   Received app port from Rust runtime: '{appPort}'");
 | 
			
		||||
            Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
 | 
			
		||||
            return appPort;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -54,11 +61,11 @@ public sealed class Rust(string apiPort) : IDisposable
 | 
			
		||||
    public async Task AppIsReady()
 | 
			
		||||
    {
 | 
			
		||||
        const string URL = "/system/dotnet/ready";
 | 
			
		||||
        Console.WriteLine($"Notifying Rust runtime that the app is ready.");
 | 
			
		||||
        this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
 | 
			
		||||
        var response = await this.http.PostAsync(URL, new StringContent(string.Empty));
 | 
			
		||||
        if (!response.IsSuccessStatusCode)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
 | 
			
		||||
             this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, IS
 | 
			
		||||
    private Rust Rust { get; } = rust;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets called when the user wants to copy the markdown to the clipboard.
 | 
			
		||||
    /// Gets called when the user wants to copy the Markdown to the clipboard.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="text">The Markdown text to copy.</param>
 | 
			
		||||
    public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text);
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,13 @@ using AIStudio.Settings.DataModel;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Tools.Services;
 | 
			
		||||
 | 
			
		||||
public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService
 | 
			
		||||
public class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService
 | 
			
		||||
{
 | 
			
		||||
    private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
 | 
			
		||||
    private static bool IS_INITIALIZED;
 | 
			
		||||
 | 
			
		||||
    private readonly ILogger<TemporaryChatService> logger = logger;
 | 
			
		||||
    
 | 
			
		||||
    #region Overrides of BackgroundService
 | 
			
		||||
 | 
			
		||||
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 | 
			
		||||
@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
 | 
			
		||||
        while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
 | 
			
		||||
            await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
 | 
			
		||||
 | 
			
		||||
        this.logger.LogInformation("The temporary chat maintenance service was initialized.");
 | 
			
		||||
        
 | 
			
		||||
        await settingsManager.LoadSettings();
 | 
			
		||||
        if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE)
 | 
			
		||||
        {
 | 
			
		||||
            Console.WriteLine("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
 | 
			
		||||
            this.logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -34,9 +38,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
 | 
			
		||||
 | 
			
		||||
    private Task StartMaintenance()
 | 
			
		||||
    {
 | 
			
		||||
        this.logger.LogInformation("Starting maintenance of temporary chat storage.");
 | 
			
		||||
        var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
 | 
			
		||||
        if(!Directory.Exists(temporaryDirectories))
 | 
			
		||||
        {
 | 
			
		||||
            this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance.");
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
 | 
			
		||||
        {
 | 
			
		||||
@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            if(deleteChat)
 | 
			
		||||
            {
 | 
			
		||||
                this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy.");
 | 
			
		||||
                Directory.Delete(tempChatDirPath, true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logger.LogInformation("Finished maintenance of temporary chat storage.");
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								app/MindWork AI Studio/Tools/TerminalLogger.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/MindWork AI Studio/Tools/TerminalLogger.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
using Microsoft.Extensions.Logging.Abstractions;
 | 
			
		||||
using Microsoft.Extensions.Logging.Console;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Tools;
 | 
			
		||||
 | 
			
		||||
public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
 | 
			
		||||
{
 | 
			
		||||
    public const string FORMATTER_NAME = "AI Studio Terminal Logger";
 | 
			
		||||
    
 | 
			
		||||
    public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
 | 
			
		||||
    {
 | 
			
		||||
        var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
 | 
			
		||||
        var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
 | 
			
		||||
        var logLevel = logEntry.LogLevel.ToString();
 | 
			
		||||
        var category = logEntry.Category;
 | 
			
		||||
        
 | 
			
		||||
        textWriter.Write($"=> {timestamp} [{logLevel}] {category}: {message}");
 | 
			
		||||
        if (logEntry.Exception is not null)
 | 
			
		||||
            textWriter.Write($" Exception was = {logEntry.Exception}");
 | 
			
		||||
        
 | 
			
		||||
        textWriter.WriteLine();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -17,7 +17,7 @@ keyring = { version = "3.2", features = ["apple-native", "windows-native", "sync
 | 
			
		||||
arboard = "3.4.0"
 | 
			
		||||
tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] }
 | 
			
		||||
flexi_logger = "0.28"
 | 
			
		||||
log = "0.4"
 | 
			
		||||
log = { version = "0.4", features = ["kv"] }
 | 
			
		||||
once_cell = "1.19.0"
 | 
			
		||||
rocket = { version = "0.5", default-features = false, features = ["json"] }
 | 
			
		||||
rand = "0.8"
 | 
			
		||||
 | 
			
		||||
@ -4,9 +4,11 @@
 | 
			
		||||
extern crate rocket;
 | 
			
		||||
extern crate core;
 | 
			
		||||
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
use std::collections::{BTreeMap, HashMap, HashSet};
 | 
			
		||||
use std::iter::once;
 | 
			
		||||
use std::net::TcpListener;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
use once_cell::sync::Lazy;
 | 
			
		||||
 | 
			
		||||
use arboard::Clipboard;
 | 
			
		||||
@ -15,9 +17,12 @@ use serde::Serialize;
 | 
			
		||||
use tauri::{Manager, Url, Window};
 | 
			
		||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
 | 
			
		||||
use tokio::time;
 | 
			
		||||
use flexi_logger::{AdaptiveFormat, Logger};
 | 
			
		||||
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger};
 | 
			
		||||
use flexi_logger::writers::FileLogWriter;
 | 
			
		||||
use keyring::error::Error::NoEntry;
 | 
			
		||||
use log::{debug, error, info, warn};
 | 
			
		||||
use log::{debug, error, info, kv, warn};
 | 
			
		||||
use log::kv::{Key, Value, VisitSource};
 | 
			
		||||
use rand::{RngCore, SeedableRng};
 | 
			
		||||
use rocket::figment::Figment;
 | 
			
		||||
use rocket::{get, post, routes};
 | 
			
		||||
use rocket::config::Shutdown;
 | 
			
		||||
@ -65,16 +70,36 @@ async fn main() {
 | 
			
		||||
    let tauri_version = metadata_lines.next().unwrap();
 | 
			
		||||
    let app_commit_hash = metadata_lines.next().unwrap();
 | 
			
		||||
 | 
			
		||||
    // Set the log level according to the environment:
 | 
			
		||||
    // In debug mode, the log level is set to debug, in release mode to info.
 | 
			
		||||
    let log_level = match is_dev() {
 | 
			
		||||
        true => "debug",
 | 
			
		||||
        false => "info",
 | 
			
		||||
    //
 | 
			
		||||
    // Configure the logger:
 | 
			
		||||
    //
 | 
			
		||||
    let mut log_config = String::new();
 | 
			
		||||
 | 
			
		||||
    // Set the log level depending on the environment:
 | 
			
		||||
    match is_dev() {
 | 
			
		||||
        true => log_config.push_str("debug, "),
 | 
			
		||||
        false => log_config.push_str("info, "),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Logger::try_with_str(log_level).expect("Cannot create logging")
 | 
			
		||||
        .log_to_stdout()
 | 
			
		||||
        .adaptive_format_for_stdout(AdaptiveFormat::Detailed)
 | 
			
		||||
    // Set the log level for the Rocket library:
 | 
			
		||||
    log_config.push_str("rocket=info, ");
 | 
			
		||||
 | 
			
		||||
    // Set the log level for the Rocket server:
 | 
			
		||||
    log_config.push_str("rocket::server=warn, ");
 | 
			
		||||
 | 
			
		||||
    // Set the log level for the Reqwest library:
 | 
			
		||||
    log_config.push_str("reqwest::async_impl::client=info");
 | 
			
		||||
 | 
			
		||||
    let logger = Logger::try_with_str(log_config).expect("Cannot create logging")
 | 
			
		||||
        .log_to_file(FileSpec::default()
 | 
			
		||||
            .basename("AI Studio Events")
 | 
			
		||||
            .suppress_timestamp()
 | 
			
		||||
            .suffix("log"))
 | 
			
		||||
        .duplicate_to_stdout(Duplicate::All)
 | 
			
		||||
        .use_utc()
 | 
			
		||||
        .format_for_files(file_logger_format)
 | 
			
		||||
        .format_for_stderr(terminal_colored_logger_format)
 | 
			
		||||
        .format_for_stdout(terminal_colored_logger_format)
 | 
			
		||||
        .start().expect("Cannot start logging");
 | 
			
		||||
 | 
			
		||||
    info!("Starting MindWork AI Studio:");
 | 
			
		||||
@ -94,6 +119,8 @@ async fn main() {
 | 
			
		||||
 | 
			
		||||
    let api_port = *API_SERVER_PORT;
 | 
			
		||||
    info!("Try to start the API server on 'http://localhost:{api_port}'...");
 | 
			
		||||
 | 
			
		||||
    // Configure the runtime API server:
 | 
			
		||||
    let figment = Figment::from(rocket::Config::release_default())
 | 
			
		||||
 | 
			
		||||
        // We use the next available port which was determined before:
 | 
			
		||||
@ -112,6 +139,9 @@ async fn main() {
 | 
			
		||||
        .merge(("workers", 3))
 | 
			
		||||
        .merge(("max_blocking", 12))
 | 
			
		||||
 | 
			
		||||
        // No colors and emojis in the log output:
 | 
			
		||||
        .merge(("cli_colors", false))
 | 
			
		||||
 | 
			
		||||
        // Set the shutdown configuration:
 | 
			
		||||
        .merge(("shutdown", Shutdown {
 | 
			
		||||
 | 
			
		||||
@ -125,6 +155,7 @@ async fn main() {
 | 
			
		||||
            ..Shutdown::default()
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // Start the runtime API server in a separate thread. This is necessary
 | 
			
		||||
    // because the server is blocking, and we need to run the Tauri app in
 | 
			
		||||
    // parallel:
 | 
			
		||||
@ -134,6 +165,7 @@ async fn main() {
 | 
			
		||||
            .mount("/", routes![dotnet_port, dotnet_ready])
 | 
			
		||||
            .ignite().await.unwrap()
 | 
			
		||||
            .launch().await.unwrap();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // Generate a secret key for the AES encryption for the IPC channel:
 | 
			
		||||
@ -208,56 +240,33 @@ async fn main() {
 | 
			
		||||
        // Log the output of the .NET server:
 | 
			
		||||
        while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
 | 
			
		||||
 | 
			
		||||
                    _ if line_cleared.contains("fail") || line_cleared.contains("error") || line_cleared.contains("exception") => _ = sender.send(ServerEvent::Error(line)).await,
 | 
			
		||||
                    _ if line_cleared.contains("warn") => _ = sender.send(ServerEvent::Warning(line)).await,
 | 
			
		||||
                    _ if line_cleared.contains("404") => _ = sender.send(ServerEvent::NotFound(line)).await,
 | 
			
		||||
                    _ => (),
 | 
			
		||||
            // Remove newline characters from the end:
 | 
			
		||||
            let line = line.trim_end();
 | 
			
		||||
 | 
			
		||||
            // Starts the line with '=>'?
 | 
			
		||||
            if line.starts_with("=>") {
 | 
			
		||||
                // Yes. This means that the line is a log message from the .NET server.
 | 
			
		||||
                // The format is: '<YYYY-MM-dd HH:mm:ss.fff> [<log level>] <source>: <message>'.
 | 
			
		||||
                // We try to parse this line and log it with the correct log level:
 | 
			
		||||
                let line = line.trim_start_matches("=>").trim();
 | 
			
		||||
                let parts = line.split_once(": ").unwrap();
 | 
			
		||||
                let left_part = parts.0.trim();
 | 
			
		||||
                let message = parts.1.trim();
 | 
			
		||||
                let parts = left_part.split_once("] ").unwrap();
 | 
			
		||||
                let level = parts.0.split_once("[").unwrap().1.trim();
 | 
			
		||||
                let source = parts.1.trim();
 | 
			
		||||
                match level {
 | 
			
		||||
                    "Trace" => debug!(Source = ".NET Server", Comp = source; "{message}"),
 | 
			
		||||
                    "Debug" => debug!(Source = ".NET Server", Comp = source; "{message}"),
 | 
			
		||||
                    "Information" => info!(Source = ".NET Server", Comp = source; "{message}"),
 | 
			
		||||
                    "Warning" => warn!(Source = ".NET Server", Comp = source; "{message}"),
 | 
			
		||||
                    "Error" => error!(Source = ".NET Server", Comp = source; "{message}"),
 | 
			
		||||
                    "Critical" => error!(Source = ".NET Server", Comp = source; "{message}"),
 | 
			
		||||
 | 
			
		||||
                    _ => error!(Source = ".NET Server", Comp = source; "{message} (unknown log level '{level}')"),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let sending_stop_result = sender.send(ServerEvent::Stopped).await;
 | 
			
		||||
            match sending_stop_result {
 | 
			
		||||
                Ok(_) => (),
 | 
			
		||||
                Err(e) => error!("Was not able to send the server stop message: {e}."),
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        warn!("Running in development mode, no .NET server will be started.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: Migrate logging to runtime API server:
 | 
			
		||||
    let server_receive_clone = DOTNET_SERVER.clone();
 | 
			
		||||
 | 
			
		||||
    // Create a thread to handle server events:
 | 
			
		||||
    tauri::async_runtime::spawn(async move {
 | 
			
		||||
        info!("Start listening for server events...");
 | 
			
		||||
        loop {
 | 
			
		||||
            match receiver.recv().await {
 | 
			
		||||
                Some(ServerEvent::Started) => {
 | 
			
		||||
                    info!("The .NET server was started.");
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Some(ServerEvent::NotFound(line)) => {
 | 
			
		||||
                    warn!("The .NET server issued a 404 error: {line}.");
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Some(ServerEvent::Warning(line)) => {
 | 
			
		||||
                    warn!("The .NET server issued a warning: {line}.");
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Some(ServerEvent::Error(line)) => {
 | 
			
		||||
                    error!("The .NET server issued an error: {line}.");
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Some(ServerEvent::Stopped) => {
 | 
			
		||||
                    warn!("The .NET server was stopped.");
 | 
			
		||||
                    *server_receive_clone.lock().unwrap() = None;
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                None => {
 | 
			
		||||
                    debug!("Server event channel was closed.");
 | 
			
		||||
                    break;
 | 
			
		||||
                },
 | 
			
		||||
            } else {
 | 
			
		||||
                info!(Source = ".NET Server"; "{line}");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
@ -267,6 +276,19 @@ async fn main() {
 | 
			
		||||
        .setup(move |app| {
 | 
			
		||||
            let window = app.get_window("main").expect("Failed to get main window.");
 | 
			
		||||
            *MAIN_WINDOW.lock().unwrap() = Some(window);
 | 
			
		||||
 | 
			
		||||
            info!(Source = "Bootloader Tauri"; "Setup is running.");
 | 
			
		||||
            let logger_path = app.path_resolver().app_local_data_dir().unwrap();
 | 
			
		||||
            let logger_path = logger_path.join("data");
 | 
			
		||||
 | 
			
		||||
            info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {logger_path:?}");
 | 
			
		||||
            logger.reset_flw(&FileLogWriter::builder(
 | 
			
		||||
                FileSpec::default()
 | 
			
		||||
                .directory(logger_path)
 | 
			
		||||
                .basename("events")
 | 
			
		||||
                .suppress_timestamp()
 | 
			
		||||
                .suffix("log")))?;
 | 
			
		||||
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
        .plugin(tauri_plugin_window_state::Builder::default().build())
 | 
			
		||||
@ -282,15 +304,15 @@ async fn main() {
 | 
			
		||||
        tauri::RunEvent::WindowEvent { event, label, .. } => {
 | 
			
		||||
            match event {
 | 
			
		||||
                tauri::WindowEvent::CloseRequested { .. } => {
 | 
			
		||||
                    warn!("Window '{label}': close was requested.");
 | 
			
		||||
                    warn!(Source = "Tauri"; "Window '{label}': close was requested.");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::WindowEvent::Destroyed => {
 | 
			
		||||
                    warn!("Window '{label}': was destroyed.");
 | 
			
		||||
                    warn!(Source = "Tauri"; "Window '{label}': was destroyed.");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::WindowEvent::FileDrop(files) => {
 | 
			
		||||
                    info!("Window '{label}': files were dropped: {files:?}");
 | 
			
		||||
                    info!(Source = "Tauri"; "Window '{label}': files were dropped: {files:?}");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _ => (),
 | 
			
		||||
@ -302,57 +324,136 @@ async fn main() {
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
 | 
			
		||||
                    let body_len = body.len();
 | 
			
		||||
                    info!("Updater: update available: body size={body_len} time={date:?} version={version}");
 | 
			
		||||
                    info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::Pending => {
 | 
			
		||||
                    info!("Updater: update is pending!");
 | 
			
		||||
                    info!(Source = "Tauri"; "Updater: update is pending!");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => {
 | 
			
		||||
                    info!("Updater: downloaded {} of {:?}", chunk_length, content_length);
 | 
			
		||||
                    info!(Source = "Tauri"; "Updater: downloaded {} of {:?}", chunk_length, content_length);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::Downloaded => {
 | 
			
		||||
                    info!("Updater: update has been downloaded!");
 | 
			
		||||
                    warn!("Try to stop the .NET server now...");
 | 
			
		||||
                    info!(Source = "Tauri"; "Updater: update has been downloaded!");
 | 
			
		||||
                    warn!(Source = "Tauri"; "Try to stop the .NET server now...");
 | 
			
		||||
                    stop_servers();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::Updated => {
 | 
			
		||||
                    info!("Updater: app has been updated");
 | 
			
		||||
                    warn!("Try to restart the app now...");
 | 
			
		||||
                    info!(Source = "Tauri"; "Updater: app has been updated");
 | 
			
		||||
                    warn!(Source = "Tauri"; "Try to restart the app now...");
 | 
			
		||||
                    app_handle.restart();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::AlreadyUpToDate => {
 | 
			
		||||
                    info!("Updater: app is already up to date");
 | 
			
		||||
                    info!(Source = "Tauri"; "Updater: app is already up to date");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                tauri::UpdaterEvent::Error(error) => {
 | 
			
		||||
                    warn!("Updater: failed to update: {error}");
 | 
			
		||||
                    warn!(Source = "Tauri"; "Updater: failed to update: {error}");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        tauri::RunEvent::ExitRequested { .. } => {
 | 
			
		||||
            warn!("Run event: exit was requested.");
 | 
			
		||||
            warn!(Source = "Tauri"; "Run event: exit was requested.");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        tauri::RunEvent::Ready => {
 | 
			
		||||
            info!("Run event: Tauri app is ready.");
 | 
			
		||||
            info!(Source = "Tauri"; "Run event: Tauri app is ready.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ => {}
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    info!("Tauri app was stopped.");
 | 
			
		||||
    warn!(Source = "Tauri"; "Tauri app was stopped.");
 | 
			
		||||
    if is_prod() {
 | 
			
		||||
        info!("Try to stop the .NET & runtime API servers as well...");
 | 
			
		||||
        warn!("Try to stop the .NET server as well...");
 | 
			
		||||
        stop_servers();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// Data structure for iterating over key-value pairs of log messages.
 | 
			
		||||
//
 | 
			
		||||
struct LogKVCollect<'kvs>(BTreeMap<Key<'kvs>, Value<'kvs>>);
 | 
			
		||||
 | 
			
		||||
impl<'kvs> VisitSource<'kvs> for LogKVCollect<'kvs> {
 | 
			
		||||
    fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), kv::Error> {
 | 
			
		||||
        self.0.insert(key, value);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn write_kv_pairs(w: &mut dyn std::io::Write, record: &log::Record) -> Result<(), std::io::Error> {
 | 
			
		||||
    if record.key_values().count() > 0 {
 | 
			
		||||
        let mut visitor = LogKVCollect(BTreeMap::new());
 | 
			
		||||
        record.key_values().visit(&mut visitor).unwrap();
 | 
			
		||||
        write!(w, "[")?;
 | 
			
		||||
        let mut index = 0;
 | 
			
		||||
        for (key, value) in visitor.0 {
 | 
			
		||||
            index += 1;
 | 
			
		||||
            if index > 1 {
 | 
			
		||||
                write!(w, ", ")?;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            write!(w, "{} = {}", key, value)?;
 | 
			
		||||
        }
 | 
			
		||||
        write!(w, "] ")?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Custom logger format for the terminal:
 | 
			
		||||
pub fn terminal_colored_logger_format(
 | 
			
		||||
    w: &mut dyn std::io::Write,
 | 
			
		||||
    now: &mut DeferredNow,
 | 
			
		||||
    record: &log::Record,
 | 
			
		||||
) -> Result<(), std::io::Error> {
 | 
			
		||||
    let level = record.level();
 | 
			
		||||
 | 
			
		||||
    // Write the timestamp, log level, and module path:
 | 
			
		||||
    write!(
 | 
			
		||||
        w,
 | 
			
		||||
        "[{}] {} [{}] ",
 | 
			
		||||
        flexi_logger::style(level).paint(now.format(flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK).to_string()),
 | 
			
		||||
        flexi_logger::style(level).paint(record.level().to_string()),
 | 
			
		||||
        record.module_path().unwrap_or("<unnamed>"),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    // Write all key-value pairs:
 | 
			
		||||
    write_kv_pairs(w, record)?;
 | 
			
		||||
 | 
			
		||||
    // Write the log message:
 | 
			
		||||
    write!(w, "{}", flexi_logger::style(level).paint(record.args().to_string()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Custom logger format for the log files:
 | 
			
		||||
pub fn file_logger_format(
 | 
			
		||||
    w: &mut dyn std::io::Write,
 | 
			
		||||
    now: &mut DeferredNow,
 | 
			
		||||
    record: &log::Record,
 | 
			
		||||
) -> Result<(), std::io::Error> {
 | 
			
		||||
 | 
			
		||||
    // Write the timestamp, log level, and module path:
 | 
			
		||||
    write!(
 | 
			
		||||
        w,
 | 
			
		||||
        "[{}] {} [{}] ",
 | 
			
		||||
        now.format(flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK),
 | 
			
		||||
        record.level(),
 | 
			
		||||
        record.module_path().unwrap_or("<unnamed>"),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    // Write all key-value pairs:
 | 
			
		||||
    write_kv_pairs(w, record)?;
 | 
			
		||||
 | 
			
		||||
    // Write the log message:
 | 
			
		||||
    write!(w, "{}", &record.args())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/system/dotnet/port")]
 | 
			
		||||
fn dotnet_port() -> String {
 | 
			
		||||
    let dotnet_server_port = *DOTNET_SERVER_PORT;
 | 
			
		||||
@ -367,10 +468,11 @@ async fn dotnet_ready() {
 | 
			
		||||
    {
 | 
			
		||||
        Ok(url) => url,
 | 
			
		||||
        Err(msg) => {
 | 
			
		||||
            error!("Error while parsing URL: {msg}");
 | 
			
		||||
            error!("Error while parsing URL for navigating to the app: {msg}");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    info!("The .NET server was booted successfully.");
 | 
			
		||||
 | 
			
		||||
    // Try to get the main window. If it is not available yet, wait for it:
 | 
			
		||||
@ -397,20 +499,11 @@ async fn dotnet_ready() {
 | 
			
		||||
    let js_location_change = format!("window.location = '{url}';");
 | 
			
		||||
    let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
 | 
			
		||||
    match location_change_result {
 | 
			
		||||
        Ok(_) => info!("Location was changed to {url}."),
 | 
			
		||||
        Err(e) => error!("Failed to change location to {url}: {e}."),
 | 
			
		||||
        Ok(_) => info!("The app location was changed to {url}."),
 | 
			
		||||
        Err(e) => error!("Failed to change the app location to {url}: {e}."),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Enum for server events:
 | 
			
		||||
enum ServerEvent {
 | 
			
		||||
    Started,
 | 
			
		||||
    NotFound(String),
 | 
			
		||||
    Warning(String),
 | 
			
		||||
    Error(String),
 | 
			
		||||
    Stopped,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn is_dev() -> bool {
 | 
			
		||||
    cfg!(debug_assertions)
 | 
			
		||||
}
 | 
			
		||||
@ -447,7 +540,7 @@ async fn check_for_update() -> CheckUpdateResponse {
 | 
			
		||||
                true => {
 | 
			
		||||
                    *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
 | 
			
		||||
                    let new_version = update_response.latest_version();
 | 
			
		||||
                    info!("Updater: update to version '{new_version}' is available.");
 | 
			
		||||
                    info!(Source = "Updater"; "An update to version '{new_version}' is available.");
 | 
			
		||||
                    let changelog = update_response.body();
 | 
			
		||||
                    CheckUpdateResponse {
 | 
			
		||||
                        update_is_available: true,
 | 
			
		||||
@ -461,7 +554,7 @@ async fn check_for_update() -> CheckUpdateResponse {
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                false => {
 | 
			
		||||
                    info!("Updater: no updates available.");
 | 
			
		||||
                    info!(Source = "Updater"; "No updates are available.");
 | 
			
		||||
                    CheckUpdateResponse {
 | 
			
		||||
                        update_is_available: false,
 | 
			
		||||
                        error: false,
 | 
			
		||||
@ -472,7 +565,7 @@ async fn check_for_update() -> CheckUpdateResponse {
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                warn!("Failed to check updater: {e}.");
 | 
			
		||||
                warn!(Source = "Updater"; "Failed to check for updates: {e}.");
 | 
			
		||||
                CheckUpdateResponse {
 | 
			
		||||
                    update_is_available: false,
 | 
			
		||||
                    error: true,
 | 
			
		||||
@ -501,7 +594,7 @@ async fn install_update() {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        None => {
 | 
			
		||||
            error!("Update installer: no update available to install. Did you check for updates first?");
 | 
			
		||||
            error!(Source = "Updater"; "No update available to install. Did you check for updates first?");
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -513,7 +606,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store
 | 
			
		||||
    let result = entry.set_password(secret.as_str());
 | 
			
		||||
    match result {
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            info!("Secret for {service} and user {user_name} was stored successfully.");
 | 
			
		||||
            info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was stored successfully.");
 | 
			
		||||
            StoreSecretResponse {
 | 
			
		||||
                success: true,
 | 
			
		||||
                issue: String::from(""),
 | 
			
		||||
@ -521,7 +614,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to store secret for {service} and user {user_name}: {e}.");
 | 
			
		||||
            error!(Source = "Secret Store"; "Failed to store secret for {service} and user {user_name}: {e}.");
 | 
			
		||||
            StoreSecretResponse {
 | 
			
		||||
                success: false,
 | 
			
		||||
                issue: e.to_string(),
 | 
			
		||||
@ -543,7 +636,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
 | 
			
		||||
    let secret = entry.get_password();
 | 
			
		||||
    match secret {
 | 
			
		||||
        Ok(s) => {
 | 
			
		||||
            info!("Secret for {service} and user {user_name} was retrieved successfully.");
 | 
			
		||||
            info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was retrieved successfully.");
 | 
			
		||||
            RequestedSecret {
 | 
			
		||||
                success: true,
 | 
			
		||||
                secret: s,
 | 
			
		||||
@ -552,7 +645,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to retrieve secret for {service} and user {user_name}: {e}.");
 | 
			
		||||
            error!(Source = "Secret Store"; "Failed to retrieve secret for {service} and user {user_name}: {e}.");
 | 
			
		||||
            RequestedSecret {
 | 
			
		||||
                success: false,
 | 
			
		||||
                secret: String::from(""),
 | 
			
		||||
@ -577,7 +670,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
 | 
			
		||||
 | 
			
		||||
    match result {
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            warn!("Secret for {service} and user {user_name} was deleted successfully.");
 | 
			
		||||
            warn!(Source = "Secret Store"; "Secret for {service} and user {user_name} was deleted successfully.");
 | 
			
		||||
            DeleteSecretResponse {
 | 
			
		||||
                success: true,
 | 
			
		||||
                was_entry_found: true,
 | 
			
		||||
@ -586,7 +679,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Err(NoEntry) => {
 | 
			
		||||
            warn!("No secret for {service} and user {user_name} was found.");
 | 
			
		||||
            warn!(Source = "Secret Store"; "No secret for {service} and user {user_name} was found.");
 | 
			
		||||
            DeleteSecretResponse {
 | 
			
		||||
                success: true,
 | 
			
		||||
                was_entry_found: false,
 | 
			
		||||
@ -595,7 +688,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to delete secret for {service} and user {user_name}: {e}.");
 | 
			
		||||
            error!(Source = "Secret Store"; "Failed to delete secret for {service} and user {user_name}: {e}.");
 | 
			
		||||
            DeleteSecretResponse {
 | 
			
		||||
                success: false,
 | 
			
		||||
                was_entry_found: false,
 | 
			
		||||
@ -618,7 +711,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
 | 
			
		||||
    let mut clipboard = match clipboard_result {
 | 
			
		||||
        Ok(clipboard) => clipboard,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to get the clipboard instance: {e}.");
 | 
			
		||||
            error!(Source = "Clipboard"; "Failed to get the clipboard instance: {e}.");
 | 
			
		||||
            return SetClipboardResponse {
 | 
			
		||||
                success: false,
 | 
			
		||||
                issue: e.to_string(),
 | 
			
		||||
@ -629,7 +722,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
 | 
			
		||||
    let set_text_result = clipboard.set_text(text);
 | 
			
		||||
    match set_text_result {
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            debug!("Text was set to the clipboard successfully.");
 | 
			
		||||
            debug!(Source = "Clipboard"; "Text was set to the clipboard successfully.");
 | 
			
		||||
            SetClipboardResponse {
 | 
			
		||||
                success: true,
 | 
			
		||||
                issue: String::from(""),
 | 
			
		||||
@ -637,7 +730,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to set text to the clipboard: {e}.");
 | 
			
		||||
            error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
 | 
			
		||||
            SetClipboardResponse {
 | 
			
		||||
                success: false,
 | 
			
		||||
                issue: e.to_string(),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user