diff --git a/app/MindWork AI Studio/Components/ReadPDFContent.razor.cs b/app/MindWork AI Studio/Components/ReadPDFContent.razor.cs index 1cdefb2b..ab050bd3 100644 --- a/app/MindWork AI Studio/Components/ReadPDFContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadPDFContent.razor.cs @@ -1,3 +1,4 @@ +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -17,12 +18,9 @@ public partial class ReadPDFContent : MSGComponentBase private async Task SelectFile() { - var pdfFile = await this.RustService.SelectFile(T("Select PDF file")); + var pdfFile = await this.RustService.SelectFile(T("Select PDF file"), FileTypeFilter.PDF); if (pdfFile.UserCancelled) return; - - if (!pdfFile.SelectedFilePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) - return; if(!File.Exists(pdfFile.SelectedFilePath)) return; diff --git a/app/MindWork AI Studio/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs index d4a03ad5..70395ed9 100644 --- a/app/MindWork AI Studio/Components/SelectFile.razor.cs +++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Settings; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -22,6 +23,9 @@ public partial class SelectFile : ComponentBase [Parameter] public string FileDialogTitle { get; set; } = "Select File"; + [Parameter] + public FileTypeFilter? Filter { get; set; } + [Parameter] public Func Validation { get; set; } = _ => null; @@ -55,7 +59,7 @@ public partial class SelectFile : ComponentBase private async Task OpenFileDialog() { - var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File); + var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.File) ? null : this.File); this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'."); if (!response.UserCancelled) diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs new file mode 100644 index 00000000..6cd05e61 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs @@ -0,0 +1,18 @@ +// ReSharper disable NotAccessedPositionalProperty.Global +namespace AIStudio.Tools.Rust; + +/// +/// Represents a file type filter for file selection dialogs. +/// +/// The name of the filter. +/// The file extensions associated with the filter. +public readonly record struct FileTypeFilter(string FilterName, string[] FilterExtensions) +{ + public static FileTypeFilter PDF => new("PDF Files", ["pdf"]); + + public static FileTypeFilter Text => new("Text Files", ["txt", "md"]); + + public static FileTypeFilter AllOffice => new("All Office Files", ["docx", "xlsx", "pptx", "doc", "xls", "ppt", "pdf"]); + + public static FileTypeFilter AllImages => new("All Image Files", ["jpg", "jpeg", "png", "gif", "bmp", "tiff"]); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs b/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs new file mode 100644 index 00000000..28d16809 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/SelectFileOptions.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools.Rust; + +public sealed class SelectFileOptions +{ + public required string Title { get; init; } + + public PreviousFile? PreviousFile { get; init; } + + public FileTypeFilter? Filter { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs index 411400f1..fc3e73bd 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs @@ -17,10 +17,16 @@ public sealed partial class RustService return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); } - public async Task SelectFile(string title, string? initialFile = null) + public async Task SelectFile(string title, FileTypeFilter? filter = null, string? initialFile = null) { - PreviousFile? previousFile = initialFile is null ? null : new (initialFile); - var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions); + var payload = new SelectFileOptions + { + Title = title, + PreviousFile = initialFile is null ? null : new (initialFile), + Filter = filter + }; + + var result = await this.http.PostAsJsonAsync("/select/file", payload, this.jsonRustSerializerOptions); if (!result.IsSuccessStatusCode) { this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'"); diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index bcfbc4ad..6d584961 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -267,6 +267,19 @@ pub struct PreviousDirectory { path: String, } +#[derive(Clone, Deserialize)] +pub struct FileTypeFilter { + filter_name: String, + filter_extensions: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct SelectFileOptions { + title: String, + previous_file: Option, + filter: Option, +} + #[derive(Serialize)] pub struct DirectorySelectionResponse { user_cancelled: bool, @@ -274,24 +287,36 @@ pub struct DirectorySelectionResponse { } /// 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() +#[post("/select/file", data = "<payload>")] +pub fn select_file(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<FileSelectionResponse> { + + // Create a new file dialog builder: + let file_dialog = FileDialogBuilder::new(); + + // Set the title of the file dialog: + let file_dialog = file_dialog.set_title(&payload.title); + + // Set the file type filter if provided: + let file_dialog = match &payload.filter { + Some(filter) => { + file_dialog.add_filter(&filter.filter_name, &filter.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>()) }, - None => { - FileDialogBuilder::new() - .set_title(title) - .pick_file() - }, + None => file_dialog, }; + // Set the previous file path if provided: + let file_dialog = match &payload.previous_file { + Some(previous) => { + let previous_path = previous.file_path.as_str(); + file_dialog.set_directory(previous_path) + }, + + None => file_dialog, + }; + + // Show the file dialog and get the selected file path: + let file_path = file_dialog.pick_file(); match file_path { Some(path) => { info!("User selected file: {path:?}");