Allow selection of multiple files (#600)
Some checks are pending
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2025-12-16 19:14:27 +01:00 committed by GitHub
parent 23594cdda6
commit 7a6b66c802
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 99 additions and 9 deletions

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for selecting multiple files.
/// </summary>
/// <param name="UserCancelled">Was the file selection canceled?</param>
/// <param name="SelectedFilePaths">The selected files, if any.</param>
public readonly record struct FilesSelectionResponse(bool UserCancelled, IReadOnlyList<string> SelectedFilePaths);

View File

@ -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<FileSelectionResponse>(this.jsonRustSerializerOptions);
}
public async Task<FilesSelectionResponse> 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<string>());
}
return await result.Content.ReadFromJsonAsync<FilesSelectionResponse>(this.jsonRustSerializerOptions);
}
/// <summary>
/// Initiates a dialog to let the user select a file for a writing operation.
/// </summary>

View File

@ -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.

View File

@ -561,6 +561,56 @@ pub fn select_file(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<F
}
}
/// Let the user select some files.
#[post("/select/files", data = "<payload>")]
pub fn select_files(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<FilesSelectionResponse> {
// 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 => 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 = "<payload>")]
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
@ -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<String>,
}
#[derive(Serialize)]
pub struct FileSaveResponse {
user_cancelled: bool,

View File

@ -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,