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 @@ + + + + + Choose File + + \ 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 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 Validation { get; set; } = _ => null; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + [Inject] + public RustService RustService { get; set; } = null!; + + [Inject] + protected ILogger Logger { get; init; } = null!; + + private static readonly Dictionary 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; + +/// +/// Data structure for selecting a file. +/// +/// Was the file selection canceled? +/// The selected file, if any. +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; + +/// +/// Data structure for selecting a file when a previous file was selected. +/// +/// The path of the previous file. +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(this.jsonRustSerializerOptions); } + + public async Task 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(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?", 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,