diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor
index 60746420..f4f96af3 100644
--- a/app/MindWork AI Studio/Pages/About.razor
+++ b/app/MindWork AI Studio/Pages/About.razor
@@ -26,6 +26,38 @@
+
+
+
+ Explanation
+
+
+ AI Studio creates a log file at startup, in which events during startup are recorded. After startup,
+ another log file is created that records all events that occur during the use of the app. This
+ includes any errors that may occur. Depending on when an error occurs (at startup or during use),
+ the contents of these log files can be helpful for troubleshooting. Sensitive information such as
+ passwords is not included in the log files.
+
+
+
+ By clicking on the respective path, the path is copied to the clipboard. You might open these files
+ with a text editor to view their contents.
+
+
+
+ Startup log file
+
+
+
+
+
+
+ Usage log file
+
+
+
+
+
diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs
index 100ebdbe..51727506 100644
--- a/app/MindWork AI Studio/Pages/About.razor.cs
+++ b/app/MindWork AI Studio/Pages/About.razor.cs
@@ -1,5 +1,8 @@
using System.Reflection;
+using AIStudio.Tools.Rust;
+using AIStudio.Tools.Services;
+
using Microsoft.AspNetCore.Components;
namespace AIStudio.Pages;
@@ -9,9 +12,15 @@ public partial class About : ComponentBase
[Inject]
private MessageBus MessageBus { get; init; } = null!;
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ [Inject]
+ private ISnackbar Snackbar { get; init; } = null!;
+
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute()!;
-
+
private static string VersionDotnetRuntime => $"Used .NET runtime: v{META_DATA.DotnetVersion}";
private static string VersionDotnetSdk => $"Used .NET SDK: v{META_DATA.DotnetSdkVersion}";
@@ -26,6 +35,28 @@ public partial class About : ComponentBase
private static string TauriVersion => $"Tauri: v{META_DATA.TauriVersion}";
+ private GetLogPathsResponse logPaths;
+
+ #region Overrides of ComponentBase
+
+ private async Task CopyStartupLogPath()
+ {
+ await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
+ }
+
+ private async Task CopyAppLogPath()
+ {
+ await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogAppPath);
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.logPaths = await this.RustService.GetLogPaths();
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
private const string LICENSE = """
# Functional Source License, Version 1.1, MIT Future License
diff --git a/app/MindWork AI Studio/Tools/Rust/GetLogPathsResponse.cs b/app/MindWork AI Studio/Tools/Rust/GetLogPathsResponse.cs
new file mode 100644
index 00000000..4cd6169e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/GetLogPathsResponse.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Tools.Rust;
+
+public readonly record struct GetLogPathsResponse(string LogStartupPath, string LogAppPath);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Log.cs b/app/MindWork AI Studio/Tools/Services/RustService.Log.cs
new file mode 100644
index 00000000..e7b52438
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/RustService.Log.cs
@@ -0,0 +1,15 @@
+using AIStudio.Tools.Rust;
+
+namespace AIStudio.Tools.Services;
+
+public sealed partial class RustService
+{
+ ///
+ /// Get the paths of the log files.
+ ///
+ /// The paths of the log files.
+ public async Task GetLogPaths()
+ {
+ return await this.http.GetFromJsonAsync("/log/paths", this.jsonRustSerializerOptions);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md
index 51bb5e75..b24969ab 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md
@@ -2,3 +2,4 @@
- Added Helmholtz (aka "Blablador") as provider. This provider is available to all researchers and employees of the 18 Helmholtz Centers as well as all eduGAIN organizations worldwide.
- Added GWDG SAIA as provider. This provider is available to all researchers and employees of the GWDG, the Max Planck Society, the 18 Helmholtz Centers, and most German universities.
- Added DeepSeek as provider.
+- Added paths to log files in the "About" dialog for easier troubleshooting and support.
\ No newline at end of file
diff --git a/runtime/src/log.rs b/runtime/src/log.rs
index 30befd46..4a244713 100644
--- a/runtime/src/log.rs
+++ b/runtime/src/log.rs
@@ -7,10 +7,18 @@ use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger, LoggerHandle};
use flexi_logger::writers::FileLogWriter;
use log::kv;
use log::kv::{Key, Value, VisitSource};
+use rocket::get;
+use rocket::serde::json::Json;
+use rocket::serde::Serialize;
+use crate::api_token::APIToken;
use crate::environment::is_dev;
static LOGGER: OnceLock = OnceLock::new();
+static LOG_STARTUP_PATH: OnceLock = OnceLock::new();
+
+static LOG_APP_PATH: OnceLock = OnceLock::new();
+
/// Initialize the logging system.
pub fn init_logging() {
@@ -42,11 +50,16 @@ pub fn init_logging() {
false => "AI Studio Events",
};
+ let log_path = FileSpec::default()
+ .basename(log_basename)
+ .suppress_timestamp()
+ .suffix("log");
+
+ // Store the startup log path:
+ LOG_STARTUP_PATH.set(log_path.as_pathbuf(None).canonicalize().unwrap().to_str().unwrap().to_string()).expect("Cannot store the startup log path");
+
let runtime_logger = Logger::try_with_str(log_config).expect("Cannot create logging")
- .log_to_file(FileSpec::default()
- .basename(log_basename)
- .suppress_timestamp()
- .suffix("log"))
+ .log_to_file(log_path)
.duplicate_to_stdout(Duplicate::All)
.use_utc()
.format_for_files(file_logger_format)
@@ -63,12 +76,13 @@ pub fn init_logging() {
/// Switch the logging system to a file-based output.
pub fn switch_to_file_logging(logger_path: PathBuf) -> Result<(), Box>{
- LOGGER.get().expect("No LOGGER was set").handle.reset_flw(&FileLogWriter::builder(
- FileSpec::default()
- .directory(logger_path)
- .basename("events")
- .suppress_timestamp()
- .suffix("log")))?;
+ let log_path = FileSpec::default()
+ .directory(logger_path)
+ .basename("events")
+ .suppress_timestamp()
+ .suffix("log");
+ LOG_APP_PATH.set(log_path.as_pathbuf(None).to_str().unwrap().to_string()).expect("Cannot store the app log path");
+ LOGGER.get().expect("No LOGGER was set").handle.reset_flw(&FileLogWriter::builder(log_path))?;
Ok(())
}
@@ -160,4 +174,19 @@ fn file_logger_format(
// Write the log message:
write!(w, "{}", &record.args())
+}
+
+#[get("/log/paths")]
+pub async fn get_log_paths(_token: APIToken) -> Json {
+ Json(LogPathsResponse {
+ log_startup_path: LOG_STARTUP_PATH.get().expect("No startup log path was set").clone(),
+ log_app_path: LOG_APP_PATH.get().expect("No app log path was set").clone(),
+ })
+}
+
+/// The response the get log paths request.
+#[derive(Serialize)]
+pub struct LogPathsResponse {
+ log_startup_path: String,
+ log_app_path: String,
}
\ No newline at end of file
diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs
index 1fa5bc75..728c4a55 100644
--- a/runtime/src/runtime_api.rs
+++ b/runtime/src/runtime_api.rs
@@ -77,6 +77,7 @@ pub fn start_runtime_api() {
crate::secret::delete_secret,
crate::environment::get_data_directory,
crate::environment::get_config_directory,
+ crate::log::get_log_paths,
])
.ignite().await.unwrap()
.launch().await.unwrap();