diff --git a/Endpoints/HealthEndpoints.cs b/Endpoints/HealthEndpoints.cs index b28c6fa..5919a10 100644 --- a/Endpoints/HealthEndpoints.cs +++ b/Endpoints/HealthEndpoints.cs @@ -29,7 +29,7 @@ public static void MapHealthEndpoints(this WebApplication app) StableDiffusion = forgeHealthy ? "Online" : "Offline", ComfyUI = comfyHealthy ? "Online" : "Offline", PreferredImageEngine = settings.PreferredImageEngine, - Version = typeof(Program).Assembly.GetName().Version?.ToString(3) ?? "3.9.0" + Version = typeof(Program).Assembly.GetName().Version?.ToString(3) ?? "3.10.0" }); }); } diff --git a/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj b/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj index 06968b0..eaf7b8a 100644 --- a/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj +++ b/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj @@ -4,9 +4,9 @@ net10.0 enable enable - 3.9.0 - 3.9.0.0 - 3.9.0.0 + 3.10.0 + 3.10.0.0 + 3.10.0.0 true diff --git a/LocalLLMServerManager.Shared/Services/HttpHelper.cs b/LocalLLMServerManager.Shared/Services/HttpHelper.cs new file mode 100644 index 0000000..bf974be --- /dev/null +++ b/LocalLLMServerManager.Shared/Services/HttpHelper.cs @@ -0,0 +1,36 @@ +using System; +using System.Net.Http; +using LocalLLMServerManager.Shared.ViewModels; + +namespace LocalLLMServerManager.Shared.Services; + +public static class HttpHelper +{ + public static HttpClient CreateClient(string? apiBase = null) + { + var client = new HttpClient(); + if (!string.IsNullOrWhiteSpace(apiBase) && Uri.TryCreate(apiBase, UriKind.Absolute, out var baseUri)) + { + client.BaseAddress = baseUri; + } + else if (MainViewModel.DefaultHttpClient.BaseAddress != null) + { + client.BaseAddress = MainViewModel.DefaultHttpClient.BaseAddress; + } + else if (!string.IsNullOrWhiteSpace(MainViewModel.BrowserOrigin) && Uri.TryCreate(MainViewModel.BrowserOrigin, UriKind.Absolute, out var originUri)) + { + client.BaseAddress = originUri; + } + return client; + } + + public static string FormatEndpoint(string apiBase, string relativePath) + { + var rel = relativePath.StartsWith("/") ? relativePath : "/" + relativePath; + if (string.IsNullOrWhiteSpace(apiBase)) + { + return rel; + } + return apiBase.TrimEnd('/') + rel; + } +} diff --git a/LocalLLMServerManager.Shared/ViewModels/AudioStudioViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/AudioStudioViewModel.cs index dd21ab2..49db3cd 100644 --- a/LocalLLMServerManager.Shared/ViewModels/AudioStudioViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/AudioStudioViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using LocalLLMServerManager.Shared.Services; namespace LocalLLMServerManager.Shared.ViewModels; @@ -46,6 +47,8 @@ partial void OnIsPlayingChanged(bool value) OnPropertyChanged(nameof(PlayButtonText)); } + [ObservableProperty] private string _apiBase = OperatingSystem.IsBrowser() ? "" : "http://127.0.0.1:5246"; + public AudioStudioViewModel() { } @@ -106,7 +109,7 @@ public async Task LoadAudioFilesAsync(string apiBase, HttpClient http) } [RelayCommand] - public async Task GenerateAudioAsync(ParamContext? ctx) + public async Task GenerateAudioAsync(ParamContext? ctx = null) { if (IsGenerating) return; @@ -115,8 +118,8 @@ public async Task GenerateAudioAsync(ParamContext? ctx) try { - var apiBase = ctx?.ApiBase ?? (OperatingSystem.IsBrowser() ? "" : "http://127.0.0.1:5246"); - var http = ctx?.Http ?? MainViewModel.DefaultHttpClient; + var apiBase = ctx?.ApiBase ?? (!string.IsNullOrWhiteSpace(ApiBase) ? ApiBase : (OperatingSystem.IsBrowser() ? "" : "http://127.0.0.1:5246")); + var http = ctx?.Http ?? HttpHelper.CreateClient(apiBase); var payload = new { diff --git a/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs index adc1628..fea4b09 100644 --- a/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs @@ -27,7 +27,7 @@ public CivitaiSearchViewModel(ICivitaiSearchService civitaiSearchService) [RelayCommand] public async Task SearchCivitaiAsync() { - await SearchCivitaiAsync(ApiBase, new HttpClient()); + await SearchCivitaiAsync(ApiBase, HttpHelper.CreateClient(ApiBase)); } public async Task SearchCivitaiAsync(string apiBase, HttpClient http) diff --git a/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs index 3a4ca2f..c6c3c58 100644 --- a/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs @@ -30,7 +30,7 @@ public HuggingFaceSearchViewModel(IHuggingFaceSearchService hfSearchService) [RelayCommand] public async Task SearchHuggingFaceAsync() { - await SearchHuggingFaceAsync(ApiBase, new HttpClient()); + await SearchHuggingFaceAsync(ApiBase, HttpHelper.CreateClient(ApiBase)); } [RelayCommand] diff --git a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs index 3bf0097..3a25528 100644 --- a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs @@ -57,6 +57,7 @@ DateTime CreatedAt public partial class MainViewModel : ObservableObject { + public static string BrowserOrigin { get; set; } = ""; public static HttpClient DefaultHttpClient { get; set; } = new(); private HttpClient? _customHttp; @@ -66,7 +67,7 @@ public HttpClient Http set => _customHttp = value; } - public static bool EnableAutomaticPolling { get; set; } = false; + public static bool EnableAutomaticPolling { get; set; } = true; public ObservableCollection Toasts => ToastService.Instance.ActiveToasts; @@ -79,7 +80,7 @@ public HttpClient Http public AudioStudioViewModel Audio { get; } [ObservableProperty] - private string _appVersionText = $"LocalLLMServerManager v{typeof(MainViewModel).Assembly.GetName().Version?.ToString(3) ?? "3.9.0"} — Unified WASM & Desktop UI"; + private string _appVersionText = $"LocalLLMServerManager v{typeof(MainViewModel).Assembly.GetName().Version?.ToString(3) ?? "3.10.0"} — Unified WASM & Desktop UI"; public MainViewModel() : this(null) { @@ -103,19 +104,24 @@ public MainViewModel( { ApiBase = Http.BaseAddress.ToString().TrimEnd('/'); } + else if (!string.IsNullOrWhiteSpace(BrowserOrigin)) + { + ApiBase = BrowserOrigin; + } else if (OperatingSystem.IsBrowser()) { ApiBase = GetDefaultApiBase(); } Telemetry = new TelemetryViewModel(telemetryService) { ApiBase = ApiBase }; - Ollama = new OllamaLibraryViewModel(ollamaModelService); - HuggingFace = new HuggingFaceSearchViewModel(hfSearchService); - Civitai = new CivitaiSearchViewModel(civitaiSearchService); - Settings = new SettingsViewModel(); - Audio = new AudioStudioViewModel(); + Ollama = new OllamaLibraryViewModel(ollamaModelService) { ApiBase = ApiBase }; + HuggingFace = new HuggingFaceSearchViewModel(hfSearchService) { ApiBase = ApiBase }; + Civitai = new CivitaiSearchViewModel(civitaiSearchService) { ApiBase = ApiBase }; + Settings = new SettingsViewModel { ApiBase = ApiBase }; + Audio = new AudioStudioViewModel { ApiBase = ApiBase }; _ = RefreshStatusAsync(); + _ = Ollama.LoadInstalledModelsAsync(ApiBase, Http); _ = Audio.LoadAudioWorkflowsAsync(ApiBase, Http); _ = Audio.LoadAudioFilesAsync(ApiBase, Http); _ = LoadSettingsAsync(); diff --git a/LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs index ccefc1e..b7bba16 100644 --- a/LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs @@ -39,13 +39,23 @@ partial void OnTargetContextTokensChanged(double value) EstimatedKvCacheText = mb >= 1024 ? $"~{(mb / 1024.0):F1} GB" : $"~{mb:F0} MB"; } + private readonly System.Threading.SemaphoreSlim _loadLock = new(1, 1); + public async Task LoadInstalledModelsAsync(string apiBase, HttpClient http) { - var models = await _ollamaModelService.LoadInstalledModelsAsync(apiBase, http); - InstalledModels.Clear(); - foreach (var m in models) + await _loadLock.WaitAsync(); + try + { + var models = await _ollamaModelService.LoadInstalledModelsAsync(apiBase, http); + InstalledModels.Clear(); + foreach (var m in models) + { + InstalledModels.Add(m); + } + } + finally { - InstalledModels.Add(m); + _loadLock.Release(); } } @@ -54,7 +64,7 @@ public async Task LoadInstalledModelsAsync(string apiBase, HttpClient http) [RelayCommand] public async Task UnloadAllVramAsync() { - await UnloadAllVramAsync(ApiBase, new HttpClient()); + await UnloadAllVramAsync(ApiBase, HttpHelper.CreateClient(ApiBase)); } public async Task UnloadAllVramAsync(string apiBase, HttpClient http) diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs index 6c299be..e93c7ae 100644 --- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs @@ -169,12 +169,14 @@ public static string EvaluateDirectoryStatus(string? path) return "⚠️ Missing"; } - private string EffectiveApiBase => OperatingSystem.IsBrowser() ? "" : (string.IsNullOrWhiteSpace(LanAccessUrl) ? "http://127.0.0.1:5246" : LanAccessUrl.TrimEnd('/')); + [ObservableProperty] private string _apiBase = OperatingSystem.IsBrowser() ? "" : "http://127.0.0.1:5246"; + + public string EffectiveApiBase => !string.IsNullOrWhiteSpace(ApiBase) ? ApiBase : (OperatingSystem.IsBrowser() ? "" : (string.IsNullOrWhiteSpace(LanAccessUrl) ? "http://127.0.0.1:5246" : LanAccessUrl.TrimEnd('/'))); [RelayCommand] public async Task RefreshComponentStatusesAsync() { - await RefreshComponentStatusesAsync(EffectiveApiBase, new HttpClient()); + await RefreshComponentStatusesAsync(EffectiveApiBase, HttpHelper.CreateClient(EffectiveApiBase)); } public async Task RefreshComponentStatusesAsync(string apiBase, HttpClient http) @@ -214,8 +216,8 @@ public async Task ToggleAudioPackAsync() private async Task ToggleComponentAsync(string componentId, bool currentlyInstalled) { - var http = new HttpClient(); var apiBase = EffectiveApiBase; + var http = HttpHelper.CreateClient(apiBase); try { if (currentlyInstalled) @@ -262,7 +264,7 @@ public void SwitchThemeStyle(string style) [RelayCommand] public async Task AutoDetectToolsAsync() { - await AutoDetectToolsAsync(EffectiveApiBase, new HttpClient()); + await AutoDetectToolsAsync(EffectiveApiBase, HttpHelper.CreateClient(EffectiveApiBase)); } public async Task AutoDetectToolsAsync(string apiBase, HttpClient http) @@ -468,7 +470,7 @@ public async Task BrowseAudioEngineExecutableAsync(IStorageProvider? provider = [RelayCommand] public async Task TestVoiceSynthesizerAsync() { - await TestVoiceSynthesizerAsync(EffectiveApiBase, new HttpClient()); + await TestVoiceSynthesizerAsync(EffectiveApiBase, HttpHelper.CreateClient(EffectiveApiBase)); } public async Task TestVoiceSynthesizerAsync(string apiBase, HttpClient http) @@ -593,7 +595,7 @@ public async Task LoadSettingsAsync(string apiBase, HttpClient http) [RelayCommand] public async Task SaveSettingsAsync() { - await SaveSettingsAsync(EffectiveApiBase, new HttpClient()); + await SaveSettingsAsync(EffectiveApiBase, HttpHelper.CreateClient(EffectiveApiBase)); } public async Task SaveSettingsAsync(string apiBase, HttpClient http) diff --git a/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs index 84ba67e..866e66d 100644 --- a/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs @@ -53,7 +53,7 @@ public TelemetryViewModel(ITelemetryService telemetryService) [RelayCommand] public async Task RefreshStatusAsync() { - await RefreshStatusAsync(ApiBase, "http://127.0.0.1:8188", new HttpClient()); + await RefreshStatusAsync(ApiBase, "http://127.0.0.1:8188", HttpHelper.CreateClient(ApiBase)); } public async Task RefreshStatusAsync(string apiBase, string comfyUrl, HttpClient http) diff --git a/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs b/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs index 0e29bd7..39fc181 100644 --- a/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs +++ b/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs @@ -34,7 +34,7 @@ public void MainView_RendersVisualTree_AndBindsVersionCorrectly() var versionTextBlock = textBlocks.FirstOrDefault(t => t.Text != null && t.Text.Contains("LocalLLMServerManager v")); Assert.NotNull(versionTextBlock); - Assert.Contains("v3.9.0", versionTextBlock.Text); + Assert.Contains("v3.10.0", versionTextBlock.Text); window.Close(); } diff --git a/LocalLLMServerManager.Tests/HttpHelperTests.cs b/LocalLLMServerManager.Tests/HttpHelperTests.cs new file mode 100644 index 0000000..7b44ae5 --- /dev/null +++ b/LocalLLMServerManager.Tests/HttpHelperTests.cs @@ -0,0 +1,42 @@ +using System; +using System.Net.Http; +using LocalLLMServerManager.Shared.Services; +using LocalLLMServerManager.Shared.ViewModels; +using Xunit; + +namespace LocalLLMServerManager.Tests; + +public class HttpHelperTests +{ + [Fact] + public void FormatEndpoint_FormatsRelativeAndAbsoluteUrlsCorrectly() + { + Assert.Equal("/health", HttpHelper.FormatEndpoint("", "health")); + Assert.Equal("/health", HttpHelper.FormatEndpoint("", "/health")); + Assert.Equal("http://127.0.0.1:5246/health", HttpHelper.FormatEndpoint("http://127.0.0.1:5246", "health")); + Assert.Equal("http://127.0.0.1:5246/health", HttpHelper.FormatEndpoint("http://127.0.0.1:5246/", "/health")); + } + + [Fact] + public void CreateClient_WithAbsoluteBase_ConfiguresBaseAddress() + { + var client = HttpHelper.CreateClient("https://localllms.wileyriley.com"); + Assert.NotNull(client.BaseAddress); + Assert.Equal("https://localllms.wileyriley.com/", client.BaseAddress.ToString()); + } + + [Fact] + public void MainViewModel_PropagatesApiBaseToAllSubViewModels() + { + var customHttp = new HttpClient { BaseAddress = new Uri("http://10.0.0.21:5246") }; + var vm = new MainViewModel(customHttp); + + Assert.Equal("http://10.0.0.21:5246", vm.ApiBase); + Assert.Equal("http://10.0.0.21:5246", vm.Telemetry.ApiBase); + Assert.Equal("http://10.0.0.21:5246", vm.Ollama.ApiBase); + Assert.Equal("http://10.0.0.21:5246", vm.HuggingFace.ApiBase); + Assert.Equal("http://10.0.0.21:5246", vm.Civitai.ApiBase); + Assert.Equal("http://10.0.0.21:5246", vm.Settings.ApiBase); + Assert.Equal("http://10.0.0.21:5246", vm.Audio.ApiBase); + } +} diff --git a/LocalLLMServerManager.Tests/PlaywrightWasmE2ETests.cs b/LocalLLMServerManager.Tests/PlaywrightWasmE2ETests.cs index 1d8ac59..72f4ce3 100644 --- a/LocalLLMServerManager.Tests/PlaywrightWasmE2ETests.cs +++ b/LocalLLMServerManager.Tests/PlaywrightWasmE2ETests.cs @@ -42,11 +42,8 @@ public async Task WebDashboard_BootsCleanlyWithoutConsoleOr404Errors() page.Console += (_, msg) => { - if (msg.Type == "error" - && !msg.Text.Contains("ERR_CONNECTION_REFUSED") - && !msg.Text.Contains("ERR_FAILED") - && !msg.Text.Contains("Failed to load resource") - && !msg.Text.Contains("Access to fetch")) + Console.WriteLine($"[BROWSER {msg.Type}]: {msg.Text}"); + if (msg.Type == "error") { consoleErrors.Add($"Console Error: {msg.Text}"); } @@ -54,6 +51,7 @@ public async Task WebDashboard_BootsCleanlyWithoutConsoleOr404Errors() page.PageError += (_, exception) => { + Console.WriteLine($"[BROWSER PAGE ERROR]: {exception}"); consoleErrors.Add($"Page Error: {exception}"); }; @@ -66,17 +64,18 @@ public async Task WebDashboard_BootsCleanlyWithoutConsoleOr404Errors() }; await page.GotoAsync(AppTestServerFixture.TestBaseUrl); - await page.WaitForTimeoutAsync(5000); + await page.WaitForTimeoutAsync(6000); var outputContainer = await page.QuerySelectorAsync("#out"); - var canvas = await page.QuerySelectorAsync("#out canvas"); + var outHtml = outputContainer != null ? await outputContainer.InnerHTMLAsync() : "null"; + var canvas = await page.QuerySelectorAsync("#out canvas") ?? await page.QuerySelectorAsync("canvas"); var loadedVersion = await page.EvaluateAsync("() => window.getAppVersion ? window.getAppVersion() : null"); Assert.True(network404s.IsEmpty, "404s:\n" + string.Join("\n", network404s)); - Assert.True(consoleErrors.IsEmpty, "Errors:\n" + string.Join("\n", consoleErrors)); + Assert.True(consoleErrors.IsEmpty, $"Errors:\n{string.Join("\n", consoleErrors)}\nOut HTML:\n{outHtml}"); Assert.NotNull(outputContainer); - Assert.NotNull(canvas); - Assert.Equal("3.9.0", loadedVersion); + Assert.True(canvas != null, $"Canvas element not found in DOM! Container HTML: {outHtml}"); + Assert.Equal("3.10.0", loadedVersion); // Exercise interactive browser pointer & keyboard events var boundingBox = await canvas.BoundingBoxAsync(); diff --git a/LocalLLMServerManager.Tests/WasmAssetFreshnessTests.cs b/LocalLLMServerManager.Tests/WasmAssetFreshnessTests.cs index 263a513..a558d05 100644 --- a/LocalLLMServerManager.Tests/WasmAssetFreshnessTests.cs +++ b/LocalLLMServerManager.Tests/WasmAssetFreshnessTests.cs @@ -52,7 +52,7 @@ public void MainJs_VersionStringMatchesCurrentVersion() var mainJsPath = Path.Combine(root, "wwwroot", "main.js"); var webMainJsPath = Path.Combine(root, "LocalLLMServerManager.Web", "main.js"); - var expectedVersion = typeof(MainViewModel).Assembly.GetName().Version?.ToString(3) ?? "3.9.0"; + var expectedVersion = typeof(MainViewModel).Assembly.GetName().Version?.ToString(3) ?? "3.10.0"; foreach (var path in new[] { mainJsPath, webMainJsPath }) { diff --git a/LocalLLMServerManager.Web/LocalLLMServerManager.Web.csproj b/LocalLLMServerManager.Web/LocalLLMServerManager.Web.csproj index e065024..912d7a1 100644 --- a/LocalLLMServerManager.Web/LocalLLMServerManager.Web.csproj +++ b/LocalLLMServerManager.Web/LocalLLMServerManager.Web.csproj @@ -4,9 +4,10 @@ net10.0 enable enable - 3.9.0 + 3.10.0 main.js Exe + true diff --git a/LocalLLMServerManager.Web/Program.cs b/LocalLLMServerManager.Web/Program.cs index 52335b3..91381b7 100644 --- a/LocalLLMServerManager.Web/Program.cs +++ b/LocalLLMServerManager.Web/Program.cs @@ -1,16 +1,42 @@ +using System.Runtime.InteropServices.JavaScript; using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; using LocalLLMServerManager; +using LocalLLMServerManager.Shared.ViewModels; [assembly: SupportedOSPlatform("browser")] internal partial class Program { - private static async Task Main(string[] args) => await BuildAvaloniaApp() + [JSImport("globalThis.getOrigin")] + internal static partial string GetBrowserOrigin(); + + private static async Task Main(string[] args) + { + try + { + if (OperatingSystem.IsBrowser()) + { + var origin = GetBrowserOrigin(); + if (!string.IsNullOrWhiteSpace(origin)) + { + origin = origin.TrimEnd('/'); + MainViewModel.BrowserOrigin = origin; + if (Uri.TryCreate(origin, UriKind.Absolute, out var originUri)) + { + MainViewModel.DefaultHttpClient = new HttpClient { BaseAddress = originUri }; + } + } + } + } + catch { } + + await BuildAvaloniaApp() .WithInterFont() .StartBrowserAppAsync("out"); + } public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); diff --git a/LocalLLMServerManager.Web/main.js b/LocalLLMServerManager.Web/main.js index 8f2b639..2cb633d 100644 --- a/LocalLLMServerManager.Web/main.js +++ b/LocalLLMServerManager.Web/main.js @@ -5,7 +5,7 @@ if (!is_browser) { throw new Error(`Expected to be running in a browser`); } -const APP_VERSION = "3.9.0"; +const APP_VERSION = "3.10.0"; globalThis.getOrigin = function () { return window.location.origin; diff --git a/LocalLLMServerManager.csproj b/LocalLLMServerManager.csproj index 1c370ea..0fe3e92 100644 --- a/LocalLLMServerManager.csproj +++ b/LocalLLMServerManager.csproj @@ -11,9 +11,9 @@ MINOR — new user-facing features (bump per feature PR) PATCH — bug fixes, dependency updates, doc-only changes --> - 3.9.0 - 3.9.0.0 - 3.9.0.0 + 3.10.0 + 3.10.0.0 + 3.10.0.0 Assets\app-icon.ico true diff --git a/README.md b/README.md index 098dbd4..fc774ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Local LLM Server Manager -> **v3.9.0** — A unified cross-platform application (.NET 10 + Avalonia UI & WebAssembly), System Tray app, background service/daemon, Model Context Protocol (MCP) AI API, visual orchestrator dashboard, and automated Playwright E2E testing framework to manage local Large Language Models (**Ollama**), Image Generation (**Stable Diffusion / Forge & ComfyUI**), **3D Mesh Generation (TRELLIS V2 & Hunyuan3D v2)**, **Video Generation (Wan 2.2, LTX-2.5, HunyuanVideo)**, and **Audio & Speech Generation (Kokoro TTS, AllTalk XTTS-v2, Faster-Whisper, Stable Audio Open 3.0, MusicGen, YuE)** on Windows, Linux, Mobile, and Web. +> **v3.10.0** — A unified cross-platform application (.NET 10 + Avalonia UI & WebAssembly), System Tray app, background service/daemon, Model Context Protocol (MCP) AI API, visual orchestrator dashboard, and automated Playwright E2E testing framework to manage local Large Language Models (**Ollama**), Image Generation (**Stable Diffusion / Forge & ComfyUI**), **3D Mesh Generation (TRELLIS V2 & Hunyuan3D v2)**, **Video Generation (Wan 2.2, LTX-2.5, HunyuanVideo)**, and **Audio & Speech Generation (Kokoro TTS, AllTalk XTTS-v2, Faster-Whisper, Stable Audio Open 3.0, MusicGen, YuE)** on Windows, Linux, Mobile, and Web. It features the official **`L³M²`** monochromatic brand identity, a high-contrast **Matte Carbon Design System**, a live **Dynamic Theming Engine** (Matte Carbon, OLED Black, Clean Light), integrated **`playwright-layout-inspector`** automated visual & layout audits, NVML CUDA real-time telemetry, **Hugging Face Hub** Multimodal discovery (GGUF, Text-to-Video, Image-to-Video, TTS, Text-to-Audio), **CivitAI** checkpoint downloads, **Multimodal Studio** with interactive 3D WebGL viewer, Video Player Preview, Audio Waveform Visualizer, a unified **Avalonia WebAssembly (WASM)** dashboard, **Modular Feature Packs** (`--with-video`, `--with-audio`), and an active **Model Context Protocol (MCP) Server** (`/mcp`). ![Dashboard Overview](docs/images/dashboard_desktop.png) @@ -32,7 +32,7 @@ The application features a dark Fluent Avalonia UI theme (`#0F172A`) organized i | [====================================------------------------------------------------] | | 8,192 tokens | +-----------------------------------------------------------------------------------------+ -| LocalLLMServerManager v3.9.0 -- Unified WASM & Desktop UI System Tray Enabled 🟢 | +| LocalLLMServerManager v3.10.0 -- Unified WASM & Desktop UI System Tray Enabled 🟢 | +-----------------------------------------------------------------------------------------+ ``` @@ -356,6 +356,7 @@ We use **MAJOR.MINOR.PATCH** (SemVer): | `3.7.0` | Multimodal Video & Audio Studio (Wan 2.2, LTX-2.5, HunyuanVideo, Kokoro TTS, Stable Audio Open 3.0, YuE), interactive Video Player and Audio Waveform controls, Multimodal Hugging Face Discovery filters, 3 new MCP AI Tools (`generate_video`, `synthesize_speech`, `generate_audio`), OpenAI-compatible `/v1/audio/speech`, and Modular Feature Packs (`--with-video`, `--with-audio`) | | `3.8.0` | Cross-Platform Tool Discovery (FFmpeg hardware encoder detection: NVENC, Intel QSV, VAAPI, AMD AMF; Kokoro Python environment inspection; Linux paths & shell runners), Dual-OS GitHub Actions CI Matrix (`[windows-latest, ubuntu-latest]`), Windows Service directory handling & Linux headless guard, and enhanced Windows & Linux installers with automated Firewall rule creation and LAN/MCP endpoint summaries | | `3.9.0` | Local Audio & Music Studio suite (Kokoro TTS, AllTalk XTTS-v2 voice cloning, Faster-Whisper STT with `/v1/audio/transcriptions` & `/v1/audio/translations`, ComfyUI MusicGen & Stable Audio Open presets, automated setup scripts, and `D:\AI\audio` storage isolation) | +| `3.10.0` | Dynamic WebAssembly browser origin resolution via JSImport, centralized `HttpHelper` with `BaseAddress` validation, thread-safe model collection synchronization, dynamic engine health status indicators, headless UI interaction test suite, and enhanced browser E2E test harness | --- diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index f939338..2b86b20 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -6,7 +6,7 @@ mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="840" x:Class="LocalLLMServerManager.Views.MainWindow" Icon="avares://LocalLLMServerManager/Assets/app-icon.ico" - Title="Local LLM Server Manager v3.9.0" + Title="Local LLM Server Manager v3.10.0" Width="1280" Height="840" MinWidth="1024" MinHeight="700" WindowStartupLocation="CenterScreen" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6a2aa7d..68f8b31 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # LocalLLMServerManager — System Architecture & Component Design -> **v3.9.0 Architecture Specification & Mermaid Diagrams** +> **v3.10.0 Architecture Specification & Mermaid Diagrams** This document provides a visual and structural blueprint of **LocalLLMServerManager**, detailing its component decomposition, MVVM hierarchy, Minimal API route modules, Dependency Injection lifecycle, Model Context Protocol (MCP) Multimodal AI integration, VRAM orchestration flow, Modular Feature Pack management, WebAssembly static asset pipeline, Playwright E2E testing layer, and Docker containerization architecture. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 6684b46..ed78d7e 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -1,6 +1,6 @@ # Local LLM Server Manager — Detailed User Guide -Welcome to the **Local LLM Server Manager (v3.9.0)**. This guide will walk you through the main tabs of the dashboard, showing you how to manage your local AI engines (Ollama, Stable Diffusion / Forge, ComfyUI, and Kokoro TTS), configure your settings, and successfully generate text, images, 3D models, video, and speech. +Welcome to the **Local LLM Server Manager (v3.10.0)**. This guide will walk you through the main tabs of the dashboard, showing you how to manage your local AI engines (Ollama, Stable Diffusion / Forge, ComfyUI, and Kokoro TTS), configure your settings, and successfully generate text, images, 3D models, video, and speech. --- diff --git a/scripts/build_release.ps1 b/scripts/build_release.ps1 index 7e028a4..0ece81c 100644 --- a/scripts/build_release.ps1 +++ b/scripts/build_release.ps1 @@ -1,7 +1,7 @@ -# LocalLLMServerManager v3.9.0 — Release Build & Package Script +# LocalLLMServerManager v3.10.0 — Release Build & Package Script $ErrorActionPreference = "Stop" -$Version = "3.9.0" +$Version = "3.10.0" $RootDir = Split-Path $PSScriptRoot -Parent $PublishDir = Join-Path $RootDir "publish" $DistDir = Join-Path $RootDir "dist" diff --git a/scripts/installer.iss b/scripts/installer.iss index 926eeac..f19463a 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -1,7 +1,7 @@ -; Script generated for Inno Setup - LocalLLMServerManager v3.9.0 +; Script generated for Inno Setup - LocalLLMServerManager v3.10.0 ; Windows Inno Setup build configuration with automated Firewall configuration, Windows Service management, and system tray startup #define MyAppName "Local LLM Server Manager" -#define MyAppVersion "3.9.0" +#define MyAppVersion "3.10.0" #define MyAppPublisher "LocalLLMServerManager Team" #define MyAppURL "https://github.com/spelech/LocalLLMServerManager" #define MyAppExeName "LocalLLMServerManager.exe" diff --git a/wwwroot/_framework/LocalLLMServerManager.Shared.wasm b/wwwroot/_framework/LocalLLMServerManager.Shared.wasm index 7ce207d..2149195 100644 Binary files a/wwwroot/_framework/LocalLLMServerManager.Shared.wasm and b/wwwroot/_framework/LocalLLMServerManager.Shared.wasm differ diff --git a/wwwroot/_framework/LocalLLMServerManager.Web.wasm b/wwwroot/_framework/LocalLLMServerManager.Web.wasm index 5dbf92e..dbdcc84 100644 Binary files a/wwwroot/_framework/LocalLLMServerManager.Web.wasm and b/wwwroot/_framework/LocalLLMServerManager.Web.wasm differ diff --git a/wwwroot/_framework/System.Net.Http.wasm b/wwwroot/_framework/System.Net.Http.wasm index 82cf8d6..e97da42 100644 Binary files a/wwwroot/_framework/System.Net.Http.wasm and b/wwwroot/_framework/System.Net.Http.wasm differ diff --git a/wwwroot/_framework/dotnet.boot.js b/wwwroot/_framework/dotnet.boot.js index c9bba9d..1156052 100644 --- a/wwwroot/_framework/dotnet.boot.js +++ b/wwwroot/_framework/dotnet.boot.js @@ -1,7 +1,7 @@ export const config = /*json-start*/{ "mainAssemblyName": "LocalLLMServerManager.Web.dll", "resources": { - "hash": "sha256-vDSvbC442EjoDLR2xRtOBpa6xyXYKXaNMR6Mz4m3JLs=", + "hash": "sha256-mCFfMcYFbKzUdAI9W5WHcwsN9WQtP7Qf6TTsJiJMlC4=", "jsModuleNative": [ { "name": "dotnet.native.js" @@ -131,12 +131,12 @@ export const config = /*json-start*/{ { "virtualPath": "LocalLLMServerManager.Shared.wasm", "name": "LocalLLMServerManager.Shared.wasm", - "hash": "sha256-di2RGrHJchrOB061EZ/cOrDUZIdxDM6ls8+eWHgd0qI=" + "hash": "sha256-trrqqctpdM9GB6WNCBC5xOCklQA1P1CsKDLyFHSxDGY=" }, { "virtualPath": "LocalLLMServerManager.Web.wasm", "name": "LocalLLMServerManager.Web.wasm", - "hash": "sha256-TOa+EB5TXMV6vtQDnSatH9pr2vpWDqtWDjlu6jNuuU0=" + "hash": "sha256-GZv4Bx1GoUDUqGHmaVTdmBN5I4laB/J+OUuTgucdrxs=" }, { "virtualPath": "Semi.Avalonia.wasm", @@ -226,7 +226,7 @@ export const config = /*json-start*/{ { "virtualPath": "System.Net.Http.wasm", "name": "System.Net.Http.wasm", - "hash": "sha256-Uzv14qqZDuGz3se5EBKjzi5sct25saKqQGDll/pd11Q=" + "hash": "sha256-rn0ZmdzKxzammusdWDK4ww5DTPv1GQlWYqEga1ZYXKM=" }, { "virtualPath": "System.Net.Http.Json.wasm", diff --git a/wwwroot/avalonia.js.br b/wwwroot/avalonia.js.br deleted file mode 100644 index 573defc..0000000 Binary files a/wwwroot/avalonia.js.br and /dev/null differ diff --git a/wwwroot/avalonia.js.gz b/wwwroot/avalonia.js.gz deleted file mode 100644 index 6d7476a..0000000 Binary files a/wwwroot/avalonia.js.gz and /dev/null differ diff --git a/wwwroot/avalonia.js.map.br b/wwwroot/avalonia.js.map.br deleted file mode 100644 index d4cf677..0000000 Binary files a/wwwroot/avalonia.js.map.br and /dev/null differ diff --git a/wwwroot/avalonia.js.map.gz b/wwwroot/avalonia.js.map.gz deleted file mode 100644 index deceb6c..0000000 Binary files a/wwwroot/avalonia.js.map.gz and /dev/null differ diff --git a/wwwroot/index.html b/wwwroot/index.html index 38cc914..06e8faf 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -44,6 +44,6 @@
- + diff --git a/wwwroot/index.html.br b/wwwroot/index.html.br deleted file mode 100644 index ab40bf0..0000000 Binary files a/wwwroot/index.html.br and /dev/null differ diff --git a/wwwroot/index.html.gz b/wwwroot/index.html.gz deleted file mode 100644 index bd0783d..0000000 Binary files a/wwwroot/index.html.gz and /dev/null differ diff --git a/wwwroot/main.js b/wwwroot/main.js index 8f2b639..2cb633d 100644 --- a/wwwroot/main.js +++ b/wwwroot/main.js @@ -5,7 +5,7 @@ if (!is_browser) { throw new Error(`Expected to be running in a browser`); } -const APP_VERSION = "3.9.0"; +const APP_VERSION = "3.10.0"; globalThis.getOrigin = function () { return window.location.origin; diff --git a/wwwroot/storage.js.br b/wwwroot/storage.js.br deleted file mode 100644 index acbafe6..0000000 Binary files a/wwwroot/storage.js.br and /dev/null differ diff --git a/wwwroot/storage.js.gz b/wwwroot/storage.js.gz deleted file mode 100644 index 3cf068e..0000000 Binary files a/wwwroot/storage.js.gz and /dev/null differ diff --git a/wwwroot/storage.js.map.br b/wwwroot/storage.js.map.br deleted file mode 100644 index 11e626e..0000000 Binary files a/wwwroot/storage.js.map.br and /dev/null differ diff --git a/wwwroot/storage.js.map.gz b/wwwroot/storage.js.map.gz deleted file mode 100644 index e404edb..0000000 Binary files a/wwwroot/storage.js.map.gz and /dev/null differ diff --git a/wwwroot/sw.js.br b/wwwroot/sw.js.br deleted file mode 100644 index 57195f2..0000000 --- a/wwwroot/sw.js.br +++ /dev/null @@ -1,4 +0,0 @@ -� �W���e�%���G�0���z%�s�Ə?$��' -ɽ?cuO�˲��-��.�}��*(8���'#+.^��\��s��lC����-�RŔT{p�+NJz+�K2��1Lb�| �+ObӘ��(�Re1�K��;p��[�� �_�M�C��W��������|`�i7��Qo -۶:A{� :3֘itƵ/���vc�Ӯ۲L:���3WC,O��]ɵ�/�f8-»����S��?��x ^ {f�.���� ��9@��q8č,����N�C�&��7%�_̑l#�����*b���/|�ΛWw畐��h�Z�p@��J�� x+�ʇ5�j�q��V4�G�u���P�����؀%k�J;�C�v��\�K�J�޸ -�J�5����=�=����i�Z \ No newline at end of file diff --git a/wwwroot/sw.js.gz b/wwwroot/sw.js.gz deleted file mode 100644 index 9d91218..0000000 Binary files a/wwwroot/sw.js.gz and /dev/null differ diff --git a/wwwroot/sw.js.map.br b/wwwroot/sw.js.map.br deleted file mode 100644 index a27f315..0000000 Binary files a/wwwroot/sw.js.map.br and /dev/null differ diff --git a/wwwroot/sw.js.map.gz b/wwwroot/sw.js.map.gz deleted file mode 100644 index e819476..0000000 Binary files a/wwwroot/sw.js.map.gz and /dev/null differ