From d95ce288567938aaa1e9e5472f712635f8aa8a03 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 31 Oct 2026 20:54:55 +0100 Subject: [PATCH] Changed the ERI Server assistant to save its specification through a save dialog --- .../Assistants/ERI/AssistantERI.razor | 4 +- .../Assistants/ERI/AssistantERI.razor.cs | 39 ++++++++++++++++++- app/MindWork AI Studio/Tools/WebRootFiles.cs | 18 +++++++++ .../Tools/AppAccess/WebRootFilesTests.cs | 30 ++++++++++++++ 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor index 6b4546810..8c330cd7a 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor @@ -116,8 +116,8 @@ else } - - @T("Download specification") + + @T("Save specification") diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs index fd971948a..0d1d4902a 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -3,6 +3,7 @@ using AIStudio.Dialogs; using AIStudio.Dialogs.Settings; using AIStudio.Settings.DataModel; using AIStudio.Tools.AssistantSessions; +using AIStudio.Tools.Rust; using Microsoft.AspNetCore.Components; @@ -375,9 +376,43 @@ public partial class AssistantERI : AssistantBaseCore private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing; /// - /// Gets whether the selected ERI specification cannot be downloaded. + /// Gets whether the selected ERI specification cannot be saved. /// - private bool IsSpecificationDownloadDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled; + private bool IsSpecificationSavingDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled; + + /// + /// Lets the user save the selected ERI specification, a copy of the one the app carries. + /// + /// + /// The app server answers the app window only, so a link to the file would fail in the browser + /// of the system. The copy also matches the installed version of AI Studio. + /// + private async Task SaveSpecificationAsync() + { + var specificationPath = this.selectedERIVersion.SpecificationURL(); + var response = await this.RustService.SaveFile(T("Save the ERI specification"), [FileTypes.JSON], Path.GetFileName(specificationPath)); + if (response.UserCancelled) + return; + + await using var feedback = new FileExportFeedback(); + feedback.ShowRunning(response.SaveFilePath); + try + { + if (await this.WebRootFiles.CopyToAsync(specificationPath, response.SaveFilePath)) + { + await feedback.SucceededAsync(Icons.Material.Filled.CheckCircle, T("The ERI specification was saved."), response.SaveFilePath, true); + return; + } + + this.Logger.LogError("The app carries no ERI specification at '{SpecificationPath}'.", specificationPath); + } + catch (Exception exception) + { + this.Logger.LogError(exception, "Failed to save the ERI specification {ERIVersion}.", this.selectedERIVersion); + } + + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, T("The ERI specification could not be saved."))); + } /// /// Gets whether the generated-code target directory selection should be disabled. diff --git a/app/MindWork AI Studio/Tools/WebRootFiles.cs b/app/MindWork AI Studio/Tools/WebRootFiles.cs index c66e565c1..c76f8334c 100644 --- a/app/MindWork AI Studio/Tools/WebRootFiles.cs +++ b/app/MindWork AI Studio/Tools/WebRootFiles.cs @@ -34,4 +34,22 @@ public sealed class WebRootFiles using var reader = new StreamReader(fileStream); return await reader.ReadToEndAsync(); } + + /// + /// Copies a file below wwwroot to the given place, such as one the user chose to save it to. + /// + /// The path below wwwroot, such as specs/eri/v1.json. + /// Where to write the copy. An existing file there gets replaced. + /// True when the copy was written, false when there is no such file below wwwroot. + public async Task CopyToAsync(string path, string destinationPath) + { + var file = this.FileProvider.GetFileInfo(path); + if (!file.Exists) + return false; + + await using var source = file.CreateReadStream(); + await using var destination = File.Create(destinationPath); + await source.CopyToAsync(destination); + return true; + } } \ No newline at end of file diff --git a/app/Tests/Tools/AppAccess/WebRootFilesTests.cs b/app/Tests/Tools/AppAccess/WebRootFilesTests.cs index b01771c8b..8ddef6e70 100644 --- a/app/Tests/Tools/AppAccess/WebRootFilesTests.cs +++ b/app/Tests/Tools/AppAccess/WebRootFilesTests.cs @@ -40,4 +40,34 @@ public sealed class WebRootFilesTests var text = await new WebRootFiles().ReadTextAsync("changelog/does-not-exist.md"); Assert.That(text, Is.Empty); } + + [TestCaseSource(nameof(SpecificationVersions))] + public async Task EveryERISpecificationCanBeSaved(ERIVersion version) + { + var webRootFiles = new WebRootFiles(); + var destinationPath = Path.Combine(Path.GetTempPath(), $"eri-specification-{Guid.NewGuid():N}.json"); + try + { + Assert.That(await webRootFiles.CopyToAsync(version.SpecificationURL(), destinationPath), Is.True); + Assert.That(await File.ReadAllTextAsync(destinationPath), Is.EqualTo(await version.ReadSpecification(webRootFiles))); + } + finally + { + File.Delete(destinationPath); + } + } + + [Test] + public async Task AMissingFileIsNotCopied() + { + var destinationPath = Path.Combine(Path.GetTempPath(), $"eri-specification-{Guid.NewGuid():N}.json"); + + var copied = await new WebRootFiles().CopyToAsync("specs/eri/does-not-exist.json", destinationPath); + + Assert.Multiple(() => + { + Assert.That(copied, Is.False); + Assert.That(File.Exists(destinationPath), Is.False); + }); + } } \ No newline at end of file