Changed the ERI Server assistant to save its specification through a save dialog

This commit is contained in:
Thorsten Sommer committed 2026-10-31 20:54:55 +01:00
1 parent 0845a3a4bb
commit d95ce28856
4 files changed
+87 -4

No files matched your search

@@ -116,8 +116,8 @@ else
</MudSelectItem>
}
</MudSelect>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationDownloadDisabled" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> @T("Download specification")
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationSavingDisabled" OnClick="@this.SaveSpecificationAsync">
<MudIcon Icon="@Icons.Material.Filled.Save" Class="mr-2"/> @T("Save specification")
</MudButton>
</MudStack>
@@ -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<SettingsDialogERIServer>
private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing;
/// <summary>
/// Gets whether the selected ERI specification cannot be downloaded.
/// Gets whether the selected ERI specification cannot be saved.
/// </summary>
private bool IsSpecificationDownloadDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
private bool IsSpecificationSavingDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
/// <summary>
/// Lets the user save the selected ERI specification, a copy of the one the app carries.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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.")));
}
/// <summary>
/// Gets whether the generated-code target directory selection should be disabled.
@@ -34,4 +34,22 @@ public sealed class WebRootFiles
using var reader = new StreamReader(fileStream);
return await reader.ReadToEndAsync();
}
/// <summary>
/// Copies a file below wwwroot to the given place, such as one the user chose to save it to.
/// </summary>
/// <param name="path">The path below wwwroot, such as <c>specs/eri/v1.json</c>.</param>
/// <param name="destinationPath">Where to write the copy. An existing file there gets replaced.</param>
/// <returns>True when the copy was written, false when there is no such file below wwwroot.</returns>
public async Task<bool> 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;
}
}
@@ -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);
});
}
}