mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-04-28 21:39:46 +00:00
Refactored logging into a unified logging
This commit is contained in:
parent
200b87f47a
commit
82f7329a39
@ -7,13 +7,15 @@ using AIStudio.Tools;
|
|||||||
|
|
||||||
namespace AIStudio.Agents;
|
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;
|
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
||||||
|
|
||||||
protected IJSRuntime JsRuntime { get; init; } = jsRuntime;
|
protected IJSRuntime JsRuntime { get; init; } = jsRuntime;
|
||||||
|
|
||||||
protected ThreadSafeRandom RNG { get; init; } = rng;
|
protected ThreadSafeRandom RNG { get; init; } = rng;
|
||||||
|
|
||||||
|
protected ILogger<AgentBase> Logger { get; init; } = logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the type or category of this agent.
|
/// Represents the type or category of this agent.
|
||||||
@ -104,6 +106,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// 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;
|
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()
|
private static readonly ContentBlock EMPTY_BLOCK = new()
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ namespace AIStudio.Assistants;
|
|||||||
public abstract partial class AssistantBase : ComponentBase
|
public abstract partial class AssistantBase : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
protected SettingsManager SettingsManager { get; set; } = null!;
|
protected SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IJSRuntime JsRuntime { get; init; } = null!;
|
protected IJSRuntime JsRuntime { get; init; } = null!;
|
||||||
@ -27,6 +27,9 @@ public abstract partial class AssistantBase : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
protected NavigationManager NavigationManager { get; init; } = null!;
|
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 AFTER_RESULT_DIV_ID = "afterAssistantResult";
|
||||||
internal const string RESULT_DIV_ID = "assistantResult";
|
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.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// 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.isProcessing = false;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
|
@ -24,6 +24,9 @@ public partial class Workspaces : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<Workspaces> Logger { get; init; } = null!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public ChatThread? CurrentChatThread { get; set; }
|
public ChatThread? CurrentChatThread { get; set; }
|
||||||
|
|
||||||
@ -309,7 +312,7 @@ public partial class Workspaces : ComponentBase
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Console.WriteLine(e);
|
this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -19,7 +19,7 @@ namespace AIStudio.Pages;
|
|||||||
public partial class Chat : MSGComponentBase, IAsyncDisposable
|
public partial class Chat : MSGComponentBase, IAsyncDisposable
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
private SettingsManager SettingsManager { get; set; } = null!;
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
public IJSRuntime JsRuntime { get; init; } = null!;
|
||||||
@ -28,7 +28,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
|
|||||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[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!;
|
private InnerScrolling scrollingArea = null!;
|
||||||
|
|
||||||
@ -189,7 +192,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// 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:
|
// Save the chat:
|
||||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
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
|
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
public SettingsManager SettingsManager { get; init; } = null!;
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IDialogService DialogService { get; init; } = null!;
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[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();
|
private readonly List<ConfigurationSelectData<string>> availableProviders = new();
|
||||||
|
|
||||||
@ -111,7 +114,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
|||||||
if (dialogResult is null || dialogResult.Canceled)
|
if (dialogResult is null || dialogResult.Canceled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var providerInstance = provider.CreateProvider();
|
var providerInstance = provider.CreateProvider(this.Logger);
|
||||||
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
|
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
|
||||||
if(deleteSecretResponse.Success)
|
if(deleteSecretResponse.Success)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,8 @@ using AIStudio.Settings;
|
|||||||
using AIStudio.Tools;
|
using AIStudio.Tools;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging.Console;
|
||||||
|
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
@ -35,6 +37,18 @@ if(string.IsNullOrWhiteSpace(secretKey))
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder();
|
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 =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
|
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
|
||||||
@ -101,5 +115,8 @@ app.MapRazorComponents<App>()
|
|||||||
|
|
||||||
var serverTask = app.RunAsync();
|
var serverTask = app.RunAsync();
|
||||||
|
|
||||||
|
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
|
||||||
|
rust.SetLogger(rustLogger);
|
||||||
|
|
||||||
await rust.AppIsReady();
|
await rust.AppIsReady();
|
||||||
await serverTask;
|
await serverTask;
|
@ -8,7 +8,7 @@ using AIStudio.Settings;
|
|||||||
|
|
||||||
namespace AIStudio.Provider.Anthropic;
|
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()
|
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
{
|
{
|
||||||
|
@ -9,13 +9,21 @@ public abstract class BaseProvider
|
|||||||
/// The HTTP client to use for all requests.
|
/// The HTTP client to use for all requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly HttpClient httpClient = new();
|
protected readonly HttpClient httpClient = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger to use.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly ILogger logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor for the base provider.
|
/// Constructor for the base provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url">The base URL for the provider.</param>
|
/// <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:
|
// Set the base URL:
|
||||||
this.httpClient.BaseAddress = new(url);
|
this.httpClient.BaseAddress = new(url);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ using AIStudio.Settings;
|
|||||||
|
|
||||||
namespace AIStudio.Provider.Fireworks;
|
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()
|
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@ using AIStudio.Settings;
|
|||||||
|
|
||||||
namespace AIStudio.Provider.Mistral;
|
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()
|
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The OpenAI provider.
|
/// The OpenAI provider.
|
||||||
/// </summary>
|
/// </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()
|
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;
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -20,59 +14,4 @@ public enum Providers
|
|||||||
FIREWORKS = 5,
|
FIREWORKS = 5,
|
||||||
|
|
||||||
SELF_HOSTED = 4,
|
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;
|
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()
|
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
{
|
{
|
||||||
@ -162,7 +162,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
|
|||||||
}
|
}
|
||||||
catch(Exception e)
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
public bool IsEditing { get; init; }
|
public bool IsEditing { get; init; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private SettingsManager SettingsManager { get; set; } = null!;
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[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();
|
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||||
|
|
||||||
@ -133,7 +136,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var loadedProviderSettings = this.CreateProviderSettings();
|
var loadedProviderSettings = this.CreateProviderSettings();
|
||||||
var provider = loadedProviderSettings.CreateProvider();
|
var provider = loadedProviderSettings.CreateProvider(this.Logger);
|
||||||
if(provider is NoProvider)
|
if(provider is NoProvider)
|
||||||
{
|
{
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
@ -187,7 +190,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
|
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
|
||||||
{
|
{
|
||||||
// We need to instantiate the provider to store the API key:
|
// 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:
|
// Store the API key in the OS secure storage:
|
||||||
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
|
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
|
||||||
@ -318,7 +321,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
private async Task ReloadModels()
|
private async Task ReloadModels()
|
||||||
{
|
{
|
||||||
var currentProviderSettings = this.CreateProviderSettings();
|
var currentProviderSettings = this.CreateProviderSettings();
|
||||||
var provider = currentProviderSettings.CreateProvider();
|
var provider = currentProviderSettings.CreateProvider(this.Logger);
|
||||||
if(provider is NoProvider)
|
if(provider is NoProvider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ namespace AIStudio.Settings;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The settings manager.
|
/// The settings manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SettingsManager
|
public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||||
{
|
{
|
||||||
private const string SETTINGS_FILENAME = "settings.json";
|
private const string SETTINGS_FILENAME = "settings.json";
|
||||||
|
|
||||||
@ -20,6 +20,8 @@ public sealed class SettingsManager
|
|||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
Converters = { new JsonStringEnumConverter() },
|
Converters = { new JsonStringEnumConverter() },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private ILogger<SettingsManager> logger = logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The directory where the configuration files are stored.
|
/// The directory where the configuration files are stored.
|
||||||
@ -102,12 +104,18 @@ public sealed class SettingsManager
|
|||||||
public async Task LoadSettings()
|
public async Task LoadSettings()
|
||||||
{
|
{
|
||||||
if(!this.IsSetUp)
|
if(!this.IsSetUp)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
||||||
if(!File.Exists(settingsPath))
|
if(!File.Exists(settingsPath))
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We read the `"Version": "V3"` line to determine the version of the settings file:
|
// We read the `"Version": "V3"` line to determine the version of the settings file:
|
||||||
await foreach (var line in File.ReadLinesAsync(settingsPath))
|
await foreach (var line in File.ReadLinesAsync(settingsPath))
|
||||||
{
|
{
|
||||||
@ -123,16 +131,16 @@ public sealed class SettingsManager
|
|||||||
Enum.TryParse(settingsVersionText, out Version settingsVersion);
|
Enum.TryParse(settingsVersionText, out Version settingsVersion);
|
||||||
if(settingsVersion is Version.UNKNOWN)
|
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();
|
this.ConfigurationData = new();
|
||||||
return;
|
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;
|
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();
|
this.ConfigurationData = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,14 +150,22 @@ public sealed class SettingsManager
|
|||||||
public async Task StoreSettings()
|
public async Task StoreSettings()
|
||||||
{
|
{
|
||||||
if(!this.IsSetUp)
|
if(!this.IsSetUp)
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
||||||
if(!Directory.Exists(ConfigDirectory))
|
if(!Directory.Exists(ConfigDirectory))
|
||||||
|
{
|
||||||
|
this.logger.LogInformation("Creating the configuration directory.");
|
||||||
Directory.CreateDirectory(ConfigDirectory!);
|
Directory.CreateDirectory(ConfigDirectory!);
|
||||||
|
}
|
||||||
|
|
||||||
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
|
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
|
||||||
await File.WriteAllTextAsync(settingsPath, settingsJson);
|
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";
|
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 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)
|
switch (previousVersion)
|
||||||
{
|
{
|
||||||
@ -17,41 +17,41 @@ public static class SettingsMigrations
|
|||||||
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
||||||
if (configV1 is null)
|
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();
|
return new();
|
||||||
}
|
}
|
||||||
|
|
||||||
configV1 = MigrateV1ToV2(configV1);
|
configV1 = MigrateV1ToV2(logger, configV1);
|
||||||
configV1 = MigrateV2ToV3(configV1);
|
configV1 = MigrateV2ToV3(logger, configV1);
|
||||||
return MigrateV3ToV4(configV1);
|
return MigrateV3ToV4(logger, configV1);
|
||||||
|
|
||||||
case Version.V2:
|
case Version.V2:
|
||||||
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
||||||
if (configV2 is null)
|
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();
|
return new();
|
||||||
}
|
}
|
||||||
|
|
||||||
configV2 = MigrateV2ToV3(configV2);
|
configV2 = MigrateV2ToV3(logger, configV2);
|
||||||
return MigrateV3ToV4(configV2);
|
return MigrateV3ToV4(logger, configV2);
|
||||||
|
|
||||||
case Version.V3:
|
case Version.V3:
|
||||||
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
||||||
if (configV3 is null)
|
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 new();
|
||||||
}
|
}
|
||||||
|
|
||||||
return MigrateV3ToV4(configV3);
|
return MigrateV3ToV4(logger, configV3);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Console.WriteLine("No configuration migration needed.");
|
logger.LogInformation("No configuration migration is needed.");
|
||||||
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
|
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
|
||||||
if (configV4 is null)
|
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();
|
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:
|
// Summary:
|
||||||
// In v1 we had no self-hosted providers. Thus, we had no hostnames.
|
// 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()
|
return new()
|
||||||
{
|
{
|
||||||
Version = Version.V2,
|
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:
|
// Summary:
|
||||||
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
|
// 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()
|
return new()
|
||||||
{
|
{
|
||||||
Version = Version.V3,
|
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:
|
// Summary:
|
||||||
// We grouped the settings into different categories.
|
// We grouped the settings into different categories.
|
||||||
//
|
//
|
||||||
|
|
||||||
Console.WriteLine("Migrating from v3 to v4...");
|
logger.LogInformation("Migrating from v3 to v4...");
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Version = Version.V4,
|
Version = Version.V4,
|
||||||
|
@ -9,6 +9,13 @@ public sealed class Rust(string apiPort) : IDisposable
|
|||||||
{
|
{
|
||||||
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
|
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()
|
public async Task<int> GetAppPort()
|
||||||
{
|
{
|
||||||
@ -36,14 +43,14 @@ public sealed class Rust(string apiPort) : IDisposable
|
|||||||
var response = await initialHttp.GetAsync(url);
|
var response = await initialHttp.GetAsync(url);
|
||||||
if (!response.IsSuccessStatusCode)
|
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);
|
await Task.Delay(wait4Try);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var appPortContent = await response.Content.ReadAsStringAsync();
|
var appPortContent = await response.Content.ReadAsStringAsync();
|
||||||
var appPort = int.Parse(appPortContent);
|
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;
|
return appPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,11 +61,11 @@ public sealed class Rust(string apiPort) : IDisposable
|
|||||||
public async Task AppIsReady()
|
public async Task AppIsReady()
|
||||||
{
|
{
|
||||||
const string URL = "/system/dotnet/ready";
|
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));
|
var response = await this.http.PostAsync(URL, new StringContent(string.Empty));
|
||||||
if (!response.IsSuccessStatusCode)
|
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;
|
private Rust Rust { get; } = rust;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="text">The Markdown text to copy.</param>
|
/// <param name="text">The Markdown text to copy.</param>
|
||||||
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text);
|
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;
|
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 readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
|
||||||
private static bool IS_INITIALIZED;
|
private static bool IS_INITIALIZED;
|
||||||
|
|
||||||
|
private readonly ILogger<TemporaryChatService> logger = logger;
|
||||||
|
|
||||||
#region Overrides of BackgroundService
|
#region Overrides of BackgroundService
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
|
|||||||
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
|
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
|
||||||
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
|
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
|
||||||
|
|
||||||
|
this.logger.LogInformation("The temporary chat maintenance service was initialized.");
|
||||||
|
|
||||||
await settingsManager.LoadSettings();
|
await settingsManager.LoadSettings();
|
||||||
if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,10 +38,14 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
|
|||||||
|
|
||||||
private Task StartMaintenance()
|
private Task StartMaintenance()
|
||||||
{
|
{
|
||||||
|
this.logger.LogInformation("Starting maintenance of temporary chat storage.");
|
||||||
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
|
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
|
||||||
if(!Directory.Exists(temporaryDirectories))
|
if(!Directory.Exists(temporaryDirectories))
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance.");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
|
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
|
||||||
{
|
{
|
||||||
var chatPath = Path.Join(tempChatDirPath, "thread.json");
|
var chatPath = Path.Join(tempChatDirPath, "thread.json");
|
||||||
@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
|
|||||||
};
|
};
|
||||||
|
|
||||||
if(deleteChat)
|
if(deleteChat)
|
||||||
|
{
|
||||||
|
this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy.");
|
||||||
Directory.Delete(tempChatDirPath, true);
|
Directory.Delete(tempChatDirPath, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.LogInformation("Finished maintenance of temporary chat storage.");
|
||||||
return Task.CompletedTask;
|
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"
|
arboard = "3.4.0"
|
||||||
tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
flexi_logger = "0.28"
|
flexi_logger = "0.28"
|
||||||
log = "0.4"
|
log = { version = "0.4", features = ["kv"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
rocket = { version = "0.5", default-features = false, features = ["json"] }
|
rocket = { version = "0.5", default-features = false, features = ["json"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
use std::iter::once;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::fmt::Write;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
@ -15,9 +17,12 @@ use serde::Serialize;
|
|||||||
use tauri::{Manager, Url, Window};
|
use tauri::{Manager, Url, Window};
|
||||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||||
use tokio::time;
|
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 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::figment::Figment;
|
||||||
use rocket::{get, post, routes};
|
use rocket::{get, post, routes};
|
||||||
use rocket::config::Shutdown;
|
use rocket::config::Shutdown;
|
||||||
@ -65,16 +70,36 @@ async fn main() {
|
|||||||
let tauri_version = metadata_lines.next().unwrap();
|
let tauri_version = metadata_lines.next().unwrap();
|
||||||
let app_commit_hash = 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.
|
// Configure the logger:
|
||||||
let log_level = match is_dev() {
|
//
|
||||||
true => "debug",
|
let mut log_config = String::new();
|
||||||
false => "info",
|
|
||||||
|
// 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")
|
// Set the log level for the Rocket library:
|
||||||
.log_to_stdout()
|
log_config.push_str("rocket=info, ");
|
||||||
.adaptive_format_for_stdout(AdaptiveFormat::Detailed)
|
|
||||||
|
// 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");
|
.start().expect("Cannot start logging");
|
||||||
|
|
||||||
info!("Starting MindWork AI Studio:");
|
info!("Starting MindWork AI Studio:");
|
||||||
@ -94,6 +119,8 @@ async fn main() {
|
|||||||
|
|
||||||
let api_port = *API_SERVER_PORT;
|
let api_port = *API_SERVER_PORT;
|
||||||
info!("Try to start the API server on 'http://localhost:{api_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())
|
let figment = Figment::from(rocket::Config::release_default())
|
||||||
|
|
||||||
// We use the next available port which was determined before:
|
// We use the next available port which was determined before:
|
||||||
@ -112,6 +139,9 @@ async fn main() {
|
|||||||
.merge(("workers", 3))
|
.merge(("workers", 3))
|
||||||
.merge(("max_blocking", 12))
|
.merge(("max_blocking", 12))
|
||||||
|
|
||||||
|
// No colors and emojis in the log output:
|
||||||
|
.merge(("cli_colors", false))
|
||||||
|
|
||||||
// Set the shutdown configuration:
|
// Set the shutdown configuration:
|
||||||
.merge(("shutdown", Shutdown {
|
.merge(("shutdown", Shutdown {
|
||||||
|
|
||||||
@ -125,6 +155,7 @@ async fn main() {
|
|||||||
..Shutdown::default()
|
..Shutdown::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
//
|
||||||
// Start the runtime API server in a separate thread. This is necessary
|
// 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
|
// because the server is blocking, and we need to run the Tauri app in
|
||||||
// parallel:
|
// parallel:
|
||||||
@ -134,6 +165,7 @@ async fn main() {
|
|||||||
.mount("/", routes![dotnet_port, dotnet_ready])
|
.mount("/", routes![dotnet_port, dotnet_ready])
|
||||||
.ignite().await.unwrap()
|
.ignite().await.unwrap()
|
||||||
.launch().await.unwrap();
|
.launch().await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Generate a secret key for the AES encryption for the IPC channel:
|
// 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:
|
// Log the output of the .NET server:
|
||||||
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
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,
|
// Remove newline characters from the end:
|
||||||
_ if line_cleared.contains("warn") => _ = sender.send(ServerEvent::Warning(line)).await,
|
let line = line.trim_end();
|
||||||
_ if line_cleared.contains("404") => _ = sender.send(ServerEvent::NotFound(line)).await,
|
|
||||||
_ => (),
|
// 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}')"),
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
info!(Source = ".NET Server"; "{line}");
|
||||||
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;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -267,6 +276,19 @@ async fn main() {
|
|||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
let window = app.get_window("main").expect("Failed to get main window.");
|
let window = app.get_window("main").expect("Failed to get main window.");
|
||||||
*MAIN_WINDOW.lock().unwrap() = Some(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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||||
@ -282,15 +304,15 @@ async fn main() {
|
|||||||
tauri::RunEvent::WindowEvent { event, label, .. } => {
|
tauri::RunEvent::WindowEvent { event, label, .. } => {
|
||||||
match event {
|
match event {
|
||||||
tauri::WindowEvent::CloseRequested { .. } => {
|
tauri::WindowEvent::CloseRequested { .. } => {
|
||||||
warn!("Window '{label}': close was requested.");
|
warn!(Source = "Tauri"; "Window '{label}': close was requested.");
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::WindowEvent::Destroyed => {
|
tauri::WindowEvent::Destroyed => {
|
||||||
warn!("Window '{label}': was destroyed.");
|
warn!(Source = "Tauri"; "Window '{label}': was destroyed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::WindowEvent::FileDrop(files) => {
|
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 } => {
|
tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
|
||||||
let body_len = body.len();
|
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 => {
|
tauri::UpdaterEvent::Pending => {
|
||||||
info!("Updater: update is pending!");
|
info!(Source = "Tauri"; "Updater: update is pending!");
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => {
|
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 => {
|
tauri::UpdaterEvent::Downloaded => {
|
||||||
info!("Updater: update has been downloaded!");
|
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
||||||
warn!("Try to stop the .NET server now...");
|
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
||||||
stop_servers();
|
stop_servers();
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::UpdaterEvent::Updated => {
|
tauri::UpdaterEvent::Updated => {
|
||||||
info!("Updater: app has been updated");
|
info!(Source = "Tauri"; "Updater: app has been updated");
|
||||||
warn!("Try to restart the app now...");
|
warn!(Source = "Tauri"; "Try to restart the app now...");
|
||||||
app_handle.restart();
|
app_handle.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::UpdaterEvent::AlreadyUpToDate => {
|
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) => {
|
tauri::UpdaterEvent::Error(error) => {
|
||||||
warn!("Updater: failed to update: {error}");
|
warn!(Source = "Tauri"; "Updater: failed to update: {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::RunEvent::ExitRequested { .. } => {
|
tauri::RunEvent::ExitRequested { .. } => {
|
||||||
warn!("Run event: exit was requested.");
|
warn!(Source = "Tauri"; "Run event: exit was requested.");
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri::RunEvent::Ready => {
|
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() {
|
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();
|
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")]
|
#[get("/system/dotnet/port")]
|
||||||
fn dotnet_port() -> String {
|
fn dotnet_port() -> String {
|
||||||
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
||||||
@ -367,10 +468,11 @@ async fn dotnet_ready() {
|
|||||||
{
|
{
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
error!("Error while parsing URL: {msg}");
|
error!("Error while parsing URL for navigating to the app: {msg}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("The .NET server was booted successfully.");
|
info!("The .NET server was booted successfully.");
|
||||||
|
|
||||||
// Try to get the main window. If it is not available yet, wait for it:
|
// 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 js_location_change = format!("window.location = '{url}';");
|
||||||
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
||||||
match location_change_result {
|
match location_change_result {
|
||||||
Ok(_) => info!("Location was changed to {url}."),
|
Ok(_) => info!("The app location was changed to {url}."),
|
||||||
Err(e) => error!("Failed to change location to {url}: {e}."),
|
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 {
|
pub fn is_dev() -> bool {
|
||||||
cfg!(debug_assertions)
|
cfg!(debug_assertions)
|
||||||
}
|
}
|
||||||
@ -447,7 +540,7 @@ async fn check_for_update() -> CheckUpdateResponse {
|
|||||||
true => {
|
true => {
|
||||||
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
|
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
|
||||||
let new_version = update_response.latest_version();
|
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();
|
let changelog = update_response.body();
|
||||||
CheckUpdateResponse {
|
CheckUpdateResponse {
|
||||||
update_is_available: true,
|
update_is_available: true,
|
||||||
@ -461,7 +554,7 @@ async fn check_for_update() -> CheckUpdateResponse {
|
|||||||
},
|
},
|
||||||
|
|
||||||
false => {
|
false => {
|
||||||
info!("Updater: no updates available.");
|
info!(Source = "Updater"; "No updates are available.");
|
||||||
CheckUpdateResponse {
|
CheckUpdateResponse {
|
||||||
update_is_available: false,
|
update_is_available: false,
|
||||||
error: false,
|
error: false,
|
||||||
@ -472,7 +565,7 @@ async fn check_for_update() -> CheckUpdateResponse {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to check updater: {e}.");
|
warn!(Source = "Updater"; "Failed to check for updates: {e}.");
|
||||||
CheckUpdateResponse {
|
CheckUpdateResponse {
|
||||||
update_is_available: false,
|
update_is_available: false,
|
||||||
error: true,
|
error: true,
|
||||||
@ -501,7 +594,7 @@ async fn install_update() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
None => {
|
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());
|
let result = entry.set_password(secret.as_str());
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
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 {
|
StoreSecretResponse {
|
||||||
success: true,
|
success: true,
|
||||||
issue: String::from(""),
|
issue: String::from(""),
|
||||||
@ -521,7 +614,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(e) => {
|
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 {
|
StoreSecretResponse {
|
||||||
success: false,
|
success: false,
|
||||||
issue: e.to_string(),
|
issue: e.to_string(),
|
||||||
@ -543,7 +636,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
|
|||||||
let secret = entry.get_password();
|
let secret = entry.get_password();
|
||||||
match secret {
|
match secret {
|
||||||
Ok(s) => {
|
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 {
|
RequestedSecret {
|
||||||
success: true,
|
success: true,
|
||||||
secret: s,
|
secret: s,
|
||||||
@ -552,7 +645,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(e) => {
|
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 {
|
RequestedSecret {
|
||||||
success: false,
|
success: false,
|
||||||
secret: String::from(""),
|
secret: String::from(""),
|
||||||
@ -577,7 +670,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
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 {
|
DeleteSecretResponse {
|
||||||
success: true,
|
success: true,
|
||||||
was_entry_found: true,
|
was_entry_found: true,
|
||||||
@ -586,7 +679,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(NoEntry) => {
|
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 {
|
DeleteSecretResponse {
|
||||||
success: true,
|
success: true,
|
||||||
was_entry_found: false,
|
was_entry_found: false,
|
||||||
@ -595,7 +688,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(e) => {
|
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 {
|
DeleteSecretResponse {
|
||||||
success: false,
|
success: false,
|
||||||
was_entry_found: false,
|
was_entry_found: false,
|
||||||
@ -618,7 +711,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
|||||||
let mut clipboard = match clipboard_result {
|
let mut clipboard = match clipboard_result {
|
||||||
Ok(clipboard) => clipboard,
|
Ok(clipboard) => clipboard,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get the clipboard instance: {e}.");
|
error!(Source = "Clipboard"; "Failed to get the clipboard instance: {e}.");
|
||||||
return SetClipboardResponse {
|
return SetClipboardResponse {
|
||||||
success: false,
|
success: false,
|
||||||
issue: e.to_string(),
|
issue: e.to_string(),
|
||||||
@ -629,7 +722,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
|||||||
let set_text_result = clipboard.set_text(text);
|
let set_text_result = clipboard.set_text(text);
|
||||||
match set_text_result {
|
match set_text_result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Text was set to the clipboard successfully.");
|
debug!(Source = "Clipboard"; "Text was set to the clipboard successfully.");
|
||||||
SetClipboardResponse {
|
SetClipboardResponse {
|
||||||
success: true,
|
success: true,
|
||||||
issue: String::from(""),
|
issue: String::from(""),
|
||||||
@ -637,7 +730,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to set text to the clipboard: {e}.");
|
error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
|
||||||
SetClipboardResponse {
|
SetClipboardResponse {
|
||||||
success: false,
|
success: false,
|
||||||
issue: e.to_string(),
|
issue: e.to_string(),
|
||||||
|
Loading…
Reference in New Issue
Block a user