From 59c83d4ca77cfc32a132672e52bbd7709cea889b Mon Sep 17 00:00:00 2001 From: spelech <28486500+spelech@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:39:56 +0000 Subject: [PATCH 1/2] feat(installer): add modular feature packs and on-demand installer for Video & Audio Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- Endpoints/ComponentEndpoints.cs | 103 ++++++++++++ .../Models/ComponentPackInfo.cs | 16 ++ .../ViewModels/SettingsViewModel.cs | 83 ++++++++++ .../Controls/EngineStudioTabControl.axaml | 17 ++ .../Controls/FeaturePackBannerControl.axaml | 39 +++++ .../FeaturePackBannerControl.axaml.cs | 73 +++++++++ .../Views/Controls/SettingsTabControl.axaml | 43 ++++++ .../ComponentManagerAndEndpointsTests.cs | 66 ++++++++ Program.cs | 2 + Services/ComponentManagerService.cs | 146 ++++++++++++++++++ Services/IComponentManagerService.cs | 12 ++ scripts/install.ps1 | 23 ++- scripts/install_linux.sh | 29 +++- scripts/installer.iss | 10 ++ scripts/setup_ai_tools.ps1 | 32 +++- 15 files changed, 690 insertions(+), 4 deletions(-) create mode 100644 Endpoints/ComponentEndpoints.cs create mode 100644 LocalLLMServerManager.Shared/Models/ComponentPackInfo.cs create mode 100644 LocalLLMServerManager.Shared/Views/Controls/FeaturePackBannerControl.axaml create mode 100644 LocalLLMServerManager.Shared/Views/Controls/FeaturePackBannerControl.axaml.cs create mode 100644 LocalLLMServerManager.Tests/ComponentManagerAndEndpointsTests.cs create mode 100644 Services/ComponentManagerService.cs create mode 100644 Services/IComponentManagerService.cs diff --git a/Endpoints/ComponentEndpoints.cs b/Endpoints/ComponentEndpoints.cs new file mode 100644 index 0000000..0ff3163 --- /dev/null +++ b/Endpoints/ComponentEndpoints.cs @@ -0,0 +1,103 @@ +using System.Text.Json; +using LocalLLMServerManager.Services; +using LocalLLMServerManager.Shared.Models; + +namespace LocalLLMServerManager.Endpoints; + +public static class ComponentEndpoints +{ + public static void MapComponentEndpoints(this WebApplication app) + { + app.MapGet("/api/components", async (IComponentManagerService componentService) => + { + var components = await componentService.GetComponentsAsync(); + return Results.Ok(components); + }); + + app.MapPost("/api/components/install", async (HttpContext httpContext, ComponentInstallRequest? request, IComponentManagerService componentService) => + { + var componentId = request?.ComponentId; + if (string.IsNullOrWhiteSpace(componentId)) + { + return Results.BadRequest(new { error = "ComponentId is required." }); + } + + httpContext.Response.ContentType = "text/event-stream"; + httpContext.Response.Headers.CacheControl = "no-cache"; + httpContext.Response.Headers.Connection = "keep-alive"; + + var syncLock = new SemaphoreSlim(1, 1); + var progress = new Progress(percent => + { + _ = Task.Run(async () => + { + await syncLock.WaitAsync(); + try + { + var eventData = JsonSerializer.Serialize(new { progress = percent, status = "installing" }); + await httpContext.Response.WriteAsync($"data: {eventData}\n\n"); + await httpContext.Response.Body.FlushAsync(); + } + catch { } + finally + { + syncLock.Release(); + } + }); + }); + + try + { + var result = await componentService.InstallComponentAsync(componentId, progress, httpContext.RequestAborted); + await syncLock.WaitAsync(); + try + { + var finalData = JsonSerializer.Serialize(new { progress = 100.0, status = result ? "completed" : "failed", success = result }); + await httpContext.Response.WriteAsync($"data: {finalData}\n\n"); + await httpContext.Response.Body.FlushAsync(); + } + finally + { + syncLock.Release(); + } + } + catch (OperationCanceledException) + { + // Request canceled + } + catch (Exception ex) + { + await syncLock.WaitAsync(); + try + { + var errData = JsonSerializer.Serialize(new { progress = 0.0, status = "error", message = ex.Message }); + await httpContext.Response.WriteAsync($"data: {errData}\n\n"); + await httpContext.Response.Body.FlushAsync(); + } + finally + { + syncLock.Release(); + } + } + + return Results.Empty; + }); + + app.MapPost("/api/components/uninstall", async (ComponentInstallRequest? request, IComponentManagerService componentService) => + { + var componentId = request?.ComponentId; + if (string.IsNullOrWhiteSpace(componentId)) + { + return Results.BadRequest(new { error = "ComponentId is required." }); + } + + var success = await componentService.UninstallComponentAsync(componentId); + if (!success) + { + return Results.BadRequest(new { error = $"Failed to uninstall component '{componentId}'." }); + } + + return Results.Ok(new { message = $"Component '{componentId}' uninstalled successfully.", success = true }); + }); + } +} diff --git a/LocalLLMServerManager.Shared/Models/ComponentPackInfo.cs b/LocalLLMServerManager.Shared/Models/ComponentPackInfo.cs new file mode 100644 index 0000000..138a703 --- /dev/null +++ b/LocalLLMServerManager.Shared/Models/ComponentPackInfo.cs @@ -0,0 +1,16 @@ +namespace LocalLLMServerManager.Shared.Models; + +public class ComponentPackInfo +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public bool Installed { get; set; } + public string DiskSizeEstimate { get; set; } = string.Empty; + public string MinVramRequired { get; set; } = string.Empty; +} + +public class ComponentInstallRequest +{ + public string ComponentId { get; set; } = string.Empty; +} diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs index 48c1274..0dc2c3e 100644 --- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs @@ -39,6 +39,15 @@ public partial class SettingsViewModel : ObservableObject [ObservableProperty] private string _threeDModelsStatus = "⚠️ Missing"; [ObservableProperty] private string _workflowsStatus = "⚠️ Missing"; + // Feature Packs Status & Management + [ObservableProperty] private bool _isVideoPackInstalled; + [ObservableProperty] private bool _isAudioPackInstalled; + [ObservableProperty] private string _videoPackDiskUsage = "14.2 GB"; + [ObservableProperty] private string _audioPackDiskUsage = "350 MB"; + + public string VideoPackStatusText => IsVideoPackInstalled ? "🟢 Installed" : "⚠️ Optional Pack Not Installed"; + public string AudioPackStatusText => IsAudioPackInstalled ? "🟢 Installed" : "⚠️ Optional Pack Not Installed"; + public string OllamaExecutableStatus => OllamaStatus; private readonly IThemeService _themeService; @@ -154,6 +163,80 @@ public static string EvaluateDirectoryStatus(string? path) return "⚠️ Missing"; } + [RelayCommand] + public async Task RefreshComponentStatusesAsync() + { + var apiBase = string.IsNullOrWhiteSpace(LanAccessUrl) ? "http://127.0.0.1:5246" : LanAccessUrl.TrimEnd('/'); + await RefreshComponentStatusesAsync(apiBase, new HttpClient()); + } + + public async Task RefreshComponentStatusesAsync(string apiBase, HttpClient http) + { + try + { + var response = await http.GetAsync($"{apiBase}/api/components"); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + foreach (var elem in doc.RootElement.EnumerateArray()) + { + var id = elem.GetProperty("id").GetString(); + var installed = elem.GetProperty("installed").GetBoolean(); + if (id == "video-generation") IsVideoPackInstalled = installed; + else if (id == "audio-tts") IsAudioPackInstalled = installed; + } + OnPropertyChanged(nameof(VideoPackStatusText)); + OnPropertyChanged(nameof(AudioPackStatusText)); + } + } + catch { } + } + + [RelayCommand] + public async Task ToggleVideoPackAsync() + { + await ToggleComponentAsync("video-generation", IsVideoPackInstalled); + } + + [RelayCommand] + public async Task ToggleAudioPackAsync() + { + await ToggleComponentAsync("audio-tts", IsAudioPackInstalled); + } + + private async Task ToggleComponentAsync(string componentId, bool currentlyInstalled) + { + var http = new HttpClient(); + var apiBase = string.IsNullOrWhiteSpace(LanAccessUrl) ? "http://127.0.0.1:5246" : LanAccessUrl.TrimEnd('/'); + try + { + if (currentlyInstalled) + { + var content = new StringContent(JsonSerializer.Serialize(new { componentId }), System.Text.Encoding.UTF8, "application/json"); + var res = await http.PostAsync($"{apiBase}/api/components/uninstall", content); + if (res.IsSuccessStatusCode) + { + ToastService.Instance.Show($"Uninstalled component '{componentId}'.", ToastType.Info); + } + } + else + { + var content = new StringContent(JsonSerializer.Serialize(new { componentId }), System.Text.Encoding.UTF8, "application/json"); + var res = await http.PostAsync($"{apiBase}/api/components/install", content); + if (res.IsSuccessStatusCode) + { + ToastService.Instance.Show($"Installed component '{componentId}'.", ToastType.Success); + } + } + await RefreshComponentStatusesAsync(); + } + catch + { + ToastService.Instance.Show($"Failed component action for '{componentId}'.", ToastType.Error); + } + } + [RelayCommand] public void SwitchThemeStyle(string style) { diff --git a/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml index 04b0135..7fd246e 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml @@ -1,10 +1,27 @@ + + + + diff --git a/LocalLLMServerManager.Shared/Views/Controls/FeaturePackBannerControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/FeaturePackBannerControl.axaml new file mode 100644 index 0000000..867a679 --- /dev/null +++ b/LocalLLMServerManager.Shared/Views/Controls/FeaturePackBannerControl.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +