diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
index a911f62b..c178d610 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
@@ -127,17 +127,21 @@ public partial class AttachDocuments : MSGComponentBase
return;
}
- var selectedFile = await this.RustService.SelectFile(T("Select a file to attach"));
- if (selectedFile.UserCancelled)
+ var selectFiles = await this.RustService.SelectFiles(T("Select a file to attach"));
+ if (selectFiles.UserCancelled)
return;
- if (!File.Exists(selectedFile.SelectedFilePath))
- return;
+ foreach (var selectedFilePath in selectFiles.SelectedFilePaths)
+ {
+ if (!File.Exists(selectedFilePath))
+ continue;
- if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(selectedFile.SelectedFilePath))
- return;
+ if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(selectedFilePath))
+ return;
- this.DocumentPaths.Add(selectedFile.SelectedFilePath);
+ this.DocumentPaths.Add(selectedFilePath);
+ }
+
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths);
}
diff --git a/app/MindWork AI Studio/Tools/Rust/FilesSelectionResponse.cs b/app/MindWork AI Studio/Tools/Rust/FilesSelectionResponse.cs
new file mode 100644
index 00000000..da2040dd
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/FilesSelectionResponse.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.Rust;
+
+///
+/// Data structure for selecting multiple files.
+///
+/// Was the file selection canceled?
+/// The selected files, if any.
+public readonly record struct FilesSelectionResponse(bool UserCancelled, IReadOnlyList SelectedFilePaths);
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs
index 9fd670c0..4a498b01 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs
@@ -25,17 +25,36 @@ public sealed partial class RustService
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}'");
return new FileSelectionResponse(true, string.Empty);
}
-
+
return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions);
}
+ public async Task SelectFiles(string title, FileTypeFilter? filter = null, string? initialFile = null)
+ {
+ var payload = new SelectFileOptions
+ {
+ Title = title,
+ PreviousFile = initialFile is null ? null : new (initialFile),
+ Filter = filter
+ };
+
+ var result = await this.http.PostAsJsonAsync("/select/files", payload, this.jsonRustSerializerOptions);
+ if (!result.IsSuccessStatusCode)
+ {
+ this.logger!.LogError($"Failed to select files: '{result.StatusCode}'");
+ return new FilesSelectionResponse(true, Array.Empty());
+ }
+
+ return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions);
+ }
+
///
/// Initiates a dialog to let the user select a file for a writing operation.
///
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md
index 9375bbf5..7fde6291 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md
@@ -13,6 +13,7 @@
- Improved file reading, e.g. for the translation, summarization, and legal assistants, by performing the Pandoc validation in the first step. This prevents unnecessary selection of files that cannot be processed.
- Improved the file selection for file attachments in chat and assistant file loading by filtering out audio files. Audio attachments are not yet supported.
- Improved the developer experience by automating localization updates in the filesystem for the selected language in the localization assistant.
+- Improved the file selection so that users can now select multiple files at the same time. This is useful, for example, for document analysis (in preview) or adding file attachments to the chat.
- Fixed a bug in the local data sources info dialog (preview feature) for data directories that could cause the app to crash. The error was caused by a background thread producing data while the frontend attempted to display it.
- Fixed a visual bug where a function's preview status was misaligned. You might have seen it in document analysis or the ERI server assistant.
- Fixed a rare bug in the Microsoft Word export for huge documents.
diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs
index dd994415..87d58fda 100644
--- a/runtime/src/app_window.rs
+++ b/runtime/src/app_window.rs
@@ -561,6 +561,56 @@ pub fn select_file(_token: APIToken, payload: Json) -> Json) -> Json {
+
+ // 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::>())
+ },
+
+ 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_paths = file_dialog.pick_files();
+ match file_paths {
+ Some(paths) => {
+ info!("User selected {} files.", paths.len());
+ Json(FilesSelectionResponse {
+ user_cancelled: false,
+ selected_file_paths: paths.iter().map(|p| p.to_str().unwrap().to_string()).collect(),
+ })
+ }
+
+ None => {
+ info!("User cancelled file selection.");
+ Json(FilesSelectionResponse {
+ user_cancelled: true,
+ selected_file_paths: Vec::new(),
+ })
+ },
+ }
+}
+
#[post("/save/file", data = "")]
pub fn save_file(_token: APIToken, payload: Json) -> Json {
@@ -620,6 +670,13 @@ pub struct FileSelectionResponse {
user_cancelled: bool,
selected_file_path: String,
}
+
+#[derive(Serialize)]
+pub struct FilesSelectionResponse {
+ user_cancelled: bool,
+ selected_file_paths: Vec,
+}
+
#[derive(Serialize)]
pub struct FileSaveResponse {
user_cancelled: bool,
diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs
index 23fc5e33..745b82c4 100644
--- a/runtime/src/runtime_api.rs
+++ b/runtime/src/runtime_api.rs
@@ -73,6 +73,7 @@ pub fn start_runtime_api() {
crate::app_window::install_update,
crate::app_window::select_directory,
crate::app_window::select_file,
+ crate::app_window::select_files,
crate::app_window::save_file,
crate::secret::get_secret,
crate::secret::store_secret,