Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Endpoints/HealthEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>3.9.0</Version>
<AssemblyVersion>3.9.0.0</AssemblyVersion>
<FileVersion>3.9.0.0</FileVersion>
<Version>3.10.0</Version>
<AssemblyVersion>3.10.0.0</AssemblyVersion>
<FileVersion>3.10.0.0</FileVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Expand Down
36 changes: 36 additions & 0 deletions LocalLLMServerManager.Shared/Services/HttpHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LocalLLMServerManager.Shared.Services;

namespace LocalLLMServerManager.Shared.ViewModels;

Expand Down Expand Up @@ -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()
{
}
Expand Down Expand Up @@ -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;

Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
20 changes: 13 additions & 7 deletions LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<ToastItem> Toasts => ToastService.Instance.ActiveToasts;

Expand All @@ -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)
{
Expand All @@ -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();
Expand Down
20 changes: 15 additions & 5 deletions LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,23 @@
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();
}
}

Expand All @@ -54,7 +64,7 @@
[RelayCommand]
public async Task UnloadAllVramAsync()
{
await UnloadAllVramAsync(ApiBase, new HttpClient());
await UnloadAllVramAsync(ApiBase, HttpHelper.CreateClient(ApiBase));
}

public async Task UnloadAllVramAsync(string apiBase, HttpClient http)
Expand Down Expand Up @@ -93,7 +103,7 @@
using var stream = await resp.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);

while (!reader.EndOfStream)

Check warning on line 106 in LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

Do not use 'reader.EndOfStream' in an async method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)

Check warning on line 106 in LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

Do not use 'reader.EndOfStream' in an async method (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
{
string? line = await reader.ReadLineAsync();
if (!string.IsNullOrWhiteSpace(line))
Expand Down
14 changes: 8 additions & 6 deletions LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
42 changes: 42 additions & 0 deletions LocalLLMServerManager.Tests/HttpHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
19 changes: 9 additions & 10 deletions LocalLLMServerManager.Tests/PlaywrightWasmE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,16 @@ 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}");
}
};

page.PageError += (_, exception) =>
{
Console.WriteLine($"[BROWSER PAGE ERROR]: {exception}");
consoleErrors.Add($"Page Error: {exception}");
};

Expand All @@ -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<string>("() => 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();
Expand Down
2 changes: 1 addition & 1 deletion LocalLLMServerManager.Tests/WasmAssetFreshnessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
{
Expand Down
3 changes: 2 additions & 1 deletion LocalLLMServerManager.Web/LocalLLMServerManager.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>3.9.0</Version>
<Version>3.10.0</Version>
<WasmMainJS>main.js</WasmMainJS>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading