From cb744064d50f1866c4c9016cc1878eaa9e0ef07a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <mail@tsommer.org> Date: Wed, 8 Jan 2025 20:50:48 +0100 Subject: [PATCH] Added possibility that the user selects a file --- .../Components/SelectFile.razor | 17 +++++ .../Components/SelectFile.razor.cs | 63 +++++++++++++++++++ .../Tools/Rust/FileSelectionResponse.cs | 8 +++ .../Tools/Rust/PreviousFile.cs | 7 +++ .../Tools/RustService.FileSystem.cs | 13 ++++ runtime/src/app_window.rs | 49 +++++++++++++++ runtime/src/runtime_api.rs | 1 + 7 files changed, 158 insertions(+) create mode 100644 app/MindWork AI Studio/Components/SelectFile.razor create mode 100644 app/MindWork AI Studio/Components/SelectFile.razor.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/PreviousFile.cs diff --git a/app/MindWork AI Studio/Components/SelectFile.razor b/app/MindWork AI Studio/Components/SelectFile.razor new file mode 100644 index 00000000..34842360 --- /dev/null +++ b/app/MindWork AI Studio/Components/SelectFile.razor @@ -0,0 +1,17 @@ +<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center"> + <MudTextField + T="string" + Text="@this.File" + Label="@this.Label" + ReadOnly="@true" + Validation="@this.Validation" + Adornment="Adornment.Start" + AdornmentIcon="@Icons.Material.Filled.AttachFile" + UserAttributes="@SPELLCHECK_ATTRIBUTES" + Variant="Variant.Outlined" + /> + + <MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog"> + Choose File + </MudButton> +</MudStack> \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs new file mode 100644 index 00000000..5d1b7f04 --- /dev/null +++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs @@ -0,0 +1,63 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class SelectFile : ComponentBase +{ + [Parameter] + public string File { get; set; } = string.Empty; + + [Parameter] + public EventCallback<string> FileChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public string FileDialogTitle { get; set; } = "Select File"; + + [Parameter] + public Func<string, string?> Validation { get; set; } = _ => null; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + [Inject] + public RustService RustService { get; set; } = null!; + + [Inject] + protected ILogger<SelectDirectory> Logger { get; init; } = null!; + + private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new(); + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + await base.OnInitializedAsync(); + } + + #endregion + + private void InternalFileChanged(string file) + { + this.File = file; + this.FileChanged.InvokeAsync(file); + } + + private async Task OpenFileDialog() + { + var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File); + this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'."); + + if (!response.UserCancelled) + this.InternalFileChanged(response.SelectedFilePath); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs b/app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs new file mode 100644 index 00000000..fd3bed6f --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/FileSelectionResponse.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.Rust; + +/// <summary> +/// Data structure for selecting a file. +/// </summary> +/// <param name="UserCancelled">Was the file selection canceled?</param> +/// <param name="SelectedFilePath">The selected file, if any.</param> +public readonly record struct FileSelectionResponse(bool UserCancelled, string SelectedFilePath); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/PreviousFile.cs b/app/MindWork AI Studio/Tools/Rust/PreviousFile.cs new file mode 100644 index 00000000..217ea064 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/PreviousFile.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.Rust; + +/// <summary> +/// Data structure for selecting a file when a previous file was selected. +/// </summary> +/// <param name="FilePath">The path of the previous file.</param> +public readonly record struct PreviousFile(string FilePath); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/RustService.FileSystem.cs index 6d5b931d..bd4c26a9 100644 --- a/app/MindWork AI Studio/Tools/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/RustService.FileSystem.cs @@ -16,4 +16,17 @@ public sealed partial class RustService return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions); } + + public async Task<FileSelectionResponse> SelectFile(string title, string? initialFile = null) + { + PreviousFile? previousFile = initialFile is null ? null : new (initialFile); + var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'"); + return new FileSelectionResponse(true, string.Empty); + } + + return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions); + } } \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index e83b2b57..f6af44cd 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -270,4 +270,53 @@ pub struct PreviousDirectory { pub struct DirectorySelectionResponse { user_cancelled: bool, selected_directory: String, +} + +/// Let the user select a file. +#[post("/select/file?<title>", data = "<previous_file>")] +pub fn select_file(_token: APIToken, title: &str, previous_file: Option<Json<PreviousFile>>) -> Json<FileSelectionResponse> { + let file_path = match previous_file { + Some(previous) => { + let previous_path = previous.file_path.as_str(); + FileDialogBuilder::new() + .set_title(title) + .set_directory(previous_path) + .pick_file() + }, + + None => { + FileDialogBuilder::new() + .set_title(title) + .pick_file() + }, + }; + + match file_path { + Some(path) => { + info!("User selected file: {path:?}"); + Json(FileSelectionResponse { + user_cancelled: false, + selected_file_path: path.to_str().unwrap().to_string(), + }) + }, + + None => { + info!("User cancelled file selection."); + Json(FileSelectionResponse { + user_cancelled: true, + selected_file_path: String::from(""), + }) + }, + } +} + +#[derive(Clone, Deserialize)] +pub struct PreviousFile { + file_path: String, +} + +#[derive(Serialize)] +pub struct FileSelectionResponse { + user_cancelled: bool, + selected_file_path: String, } \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 963900d7..26bbbf90 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -85,6 +85,7 @@ pub fn start_runtime_api() { crate::app_window::check_for_update, crate::app_window::install_update, crate::app_window::select_directory, + crate::app_window::select_file, crate::secret::get_secret, crate::secret::store_secret, crate::secret::delete_secret,