From a72dc90f314701a28590bc0557e776bca70ec6dd Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 31 Oct 2026 19:06:37 +0100 Subject: [PATCH] Changed the changelog and the ERI specifications to load without a request to the app server --- .../Assistants/ERI/AssistantERI.razor.cs | 4 +- .../Assistants/ERI/ERIVersionExtensions.cs | 18 ++------ .../Components/Changelog.razor.cs | 5 +-- app/MindWork AI Studio/Pages/Home.razor.cs | 5 +-- app/MindWork AI Studio/Program.cs | 15 ++----- app/MindWork AI Studio/Tools/WebRootFiles.cs | 36 ++++++++++++++++ .../Tools/AppAccess/WebRootFilesTests.cs | 43 +++++++++++++++++++ 7 files changed, 92 insertions(+), 34 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/WebRootFiles.cs create mode 100644 app/Tests/Tools/AppAccess/WebRootFilesTests.cs diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs index 09c80c2e0..fd971948a 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -13,7 +13,7 @@ namespace AIStudio.Assistants.ERI; public partial class AssistantERI : AssistantBaseCore { [Inject] - private HttpClient HttpClient { get; set; } = null!; + private WebRootFiles WebRootFiles { get; set; } = null!; [Inject] private IDialogService DialogService { get; init; } = null!; @@ -843,7 +843,7 @@ public partial class AssistantERI : AssistantBaseCore var baseDirectorySnapshot = this.baseDirectory; var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray(); - this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient); + this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.WebRootFiles); if (string.IsNullOrWhiteSpace(this.eriSpecification)) { this.AddInputIssue(T("The ERI specification could not be loaded. Please try again later.")); diff --git a/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs index 99631e142..78d095c83 100644 --- a/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs +++ b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs @@ -2,26 +2,14 @@ namespace AIStudio.Assistants.ERI; public static class ERIVersionExtensions { - public static async Task ReadSpecification(this ERIVersion version, HttpClient httpClient) - { - try - { - var url = version.SpecificationURL(); - using var response = await httpClient.GetAsync(url); - return await response.Content.ReadAsStringAsync(); - } - catch - { - return string.Empty; - } - } - + public static Task ReadSpecification(this ERIVersion version, WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL()); + public static string SpecificationURL(this ERIVersion version) { var nameLower = version.ToString().ToLowerInvariant(); var filename = $"{nameLower}.json"; return $"specs/eri/{filename}"; } - + public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Changelog.razor.cs b/app/MindWork AI Studio/Components/Changelog.razor.cs index 6402b1dd7..38d41bd71 100644 --- a/app/MindWork AI Studio/Components/Changelog.razor.cs +++ b/app/MindWork AI Studio/Components/Changelog.razor.cs @@ -5,7 +5,7 @@ namespace AIStudio.Components; public partial class Changelog : MSGComponentBase { [Inject] - private HttpClient HttpClient { get; set; } = null!; + private WebRootFiles WebRootFiles { get; set; } = null!; #region Overrides of ComponentBase @@ -23,7 +23,6 @@ public partial class Changelog : MSGComponentBase private async Task ReadLogAsync() { - using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}"); - this.LogContent = await response.Content.ReadAsStringAsync(); + this.LogContent = await this.WebRootFiles.ReadTextAsync($"changelog/{this.SelectedLog.Filename}"); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 011e2d96d..1a8fe5b2a 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -13,7 +13,7 @@ namespace AIStudio.Pages; public partial class Home : MSGComponentBase { [Inject] - private HttpClient HttpClient { get; init; } = null!; + private WebRootFiles WebRootFiles { get; init; } = null!; [Inject] private NavigationManager NavigationManager { get; init; } = null!; @@ -222,8 +222,7 @@ public partial class Home : MSGComponentBase private async Task ReadLastChangeAsync() { var latest = Changelog.LOGS.MaxBy(n => n.Build); - using var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}"); - this.LastChangeContent = await response.Content.ReadAsStringAsync(); + this.LastChangeContent = await this.WebRootFiles.ReadTextAsync($"changelog/{latest.Filename}"); await this.InvokeAsync(this.StateHasChanged); } diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index f5af7adc8..53ad77567 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -32,11 +32,6 @@ using MudBlazor.Services; using MudExtensions.Services; -#if !DEBUG -using System.Reflection; -using Microsoft.Extensions.FileProviders; -#endif - namespace AIStudio; internal sealed class Program @@ -283,10 +278,9 @@ internal sealed class Program options.KeepAliveInterval = TimeSpan.FromSeconds(30); }); - builder.Services.AddSingleton(new HttpClient - { - BaseAddress = new Uri($"http://127.0.0.1:{appPort}") - }); + // For absolute URLs only. The app reads its own files through WebRootFiles: + builder.Services.AddSingleton(new HttpClient()); + builder.Services.AddSingleton(); // // We listen on the IPv4 loopback address only, where the app window goes. With localhost, the @@ -356,10 +350,9 @@ internal sealed class Program app.UseStaticFiles(); app.UseDeveloperExceptionPage(); #else - var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot"); app.UseStaticFiles(new StaticFileOptions { - FileProvider = fileProvider, + FileProvider = app.Services.GetRequiredService().FileProvider, RequestPath = string.Empty, }); #endif diff --git a/app/MindWork AI Studio/Tools/WebRootFiles.cs b/app/MindWork AI Studio/Tools/WebRootFiles.cs new file mode 100644 index 000000000..51fbcb659 --- /dev/null +++ b/app/MindWork AI Studio/Tools/WebRootFiles.cs @@ -0,0 +1,36 @@ +using System.Reflection; + +using Microsoft.Extensions.FileProviders; + +namespace AIStudio.Tools; + +/// +/// Reads the files below wwwroot, which every build embeds into the app. +/// +/// +/// The app reads files such as its changelogs directly instead of requesting them over HTTP from +/// its own server. The release build serves its static files from the same provider. +/// +public sealed class WebRootFiles +{ + /// + /// Gets the files below wwwroot. + /// + public IFileProvider FileProvider { get; } = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot"); + + /// + /// Reads a text file below wwwroot. + /// + /// The path below wwwroot, such as changelog/v26.10.1.md. + /// The text of the file, or an empty string when there is no such file. + public async Task ReadTextAsync(string path) + { + var file = this.FileProvider.GetFileInfo(path); + if (!file.Exists) + return string.Empty; + + await using var fileStream = file.CreateReadStream(); + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } +} \ No newline at end of file diff --git a/app/Tests/Tools/AppAccess/WebRootFilesTests.cs b/app/Tests/Tools/AppAccess/WebRootFilesTests.cs new file mode 100644 index 000000000..b01771c8b --- /dev/null +++ b/app/Tests/Tools/AppAccess/WebRootFilesTests.cs @@ -0,0 +1,43 @@ +using AIStudio.Assistants.ERI; +using AIStudio.Tools; + +using Changelog = AIStudio.Components.Changelog; + +namespace AIStudio.Tests.Tools.AppAccess; + +/// +/// Checks that the app reads the files below wwwroot which it shows itself. +/// +/// +/// The changelogs and the ERI specifications come from the files embedded into the app, not from a +/// request to its own server. A file which is missing there would leave the changelog or the ERI +/// assistant empty without any error. +/// +[TestFixture] +public sealed class WebRootFilesTests +{ + private static IEnumerable ChangelogFiles() => Changelog.LOGS.Select(log => log.Filename); + + private static IEnumerable SpecificationVersions() => Enum.GetValues().Where(version => version.WasSpecificationSelected()); + + [TestCaseSource(nameof(ChangelogFiles))] + public async Task EveryChangelogCanBeRead(string filename) + { + var text = await new WebRootFiles().ReadTextAsync($"changelog/{filename}"); + Assert.That(text, Is.Not.Empty); + } + + [TestCaseSource(nameof(SpecificationVersions))] + public async Task EveryERISpecificationCanBeRead(ERIVersion version) + { + var text = await version.ReadSpecification(new WebRootFiles()); + Assert.That(text, Is.Not.Empty); + } + + [Test] + public async Task AMissingFileReadsAsEmptyText() + { + var text = await new WebRootFiles().ReadTextAsync("changelog/does-not-exist.md"); + Assert.That(text, Is.Empty); + } +} \ No newline at end of file