Skip to content
Closed
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
103 changes: 103 additions & 0 deletions Endpoints/ComponentEndpoints.cs
Original file line number Diff line number Diff line change
@@ -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<double>(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 });
});
}
}
16 changes: 16 additions & 0 deletions LocalLLMServerManager.Shared/Models/ComponentPackInfo.cs
Original file line number Diff line number Diff line change
@@ -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;
}
83 changes: 83 additions & 0 deletions LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LocalLLMServerManager.Shared.ViewModels"
xmlns:controls="using:LocalLLMServerManager.Shared.Views.Controls"
x:Class="LocalLLMServerManager.Shared.Views.Controls.EngineStudioTabControl"
x:DataType="vm:MainViewModel">

<StackPanel Spacing="16" Margin="0,12,0,0">
<!-- Feature Pack Banners for Video & Audio -->
<controls:FeaturePackBannerControl x:Name="VideoFeatureBanner"
Title="ComfyUI Video Generation Pack Available"
Description="Enables Wan 2.2, LTX-2.5, and HunyuanVideo DiT workflows."
DiskSize="14.2 GB"
MinVram="8 GB"
InstallCommand="{Binding Settings.ToggleVideoPackCommand}"
IsVisible="{Binding !Settings.IsVideoPackInstalled}" Margin="0,0,0,8"/>
<controls:FeaturePackBannerControl x:Name="AudioFeatureBanner"
Title="Kokoro TTS &amp; Audio Engine Pack Available"
Description="Enables fast local text-to-speech with OpenAI /v1/audio/speech compatibility."
DiskSize="350 MB"
MinVram="CPU / 2 GB"
InstallCommand="{Binding Settings.ToggleAudioPackCommand}"
IsVisible="{Binding !Settings.IsAudioPackInstalled}" Margin="0,0,0,8"/>

<StackPanel Spacing="2">
<TextBlock Text="🎨 AI Image &amp; 3D Generation Engines" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextMainBrush}"/>
<TextBlock Text="Manage background engine processes and web interfaces" FontSize="12" Foreground="{StaticResource TextMutedBrush}"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:LocalLLMServerManager.Shared.Views.Controls"
x:Class="LocalLLMServerManager.Shared.Views.Controls.FeaturePackBannerControl"
x:Name="RootControl">

<Border Classes="matte-card" Background="{StaticResource BgSurfaceBrush}" BorderBrush="{StaticResource AccentBrush}" BorderThickness="1" Padding="20" CornerRadius="8">
<Grid RowDefinitions="Auto, Auto, Auto" ColumnDefinitions="*, Auto">
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="📦" FontSize="20" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Title, ElementName=RootControl}" FontSize="16" FontWeight="Bold" Foreground="{StaticResource TextMainBrush}" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="{Binding Description, ElementName=RootControl}" FontSize="13" Foreground="{StaticResource TextMutedBrush}" TextWrapping="Wrap" Margin="0,4,0,0"/>
</StackPanel>

<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Spacing="16" Margin="0,12,0,0">
<Border Classes="matte-pill" Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="💾 Disk Size:" FontSize="12" Foreground="{StaticResource TextMutedBrush}"/>
<TextBlock Text="{Binding DiskSize, ElementName=RootControl}" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextMainBrush}"/>
</StackPanel>
</Border>
<Border Classes="matte-pill" Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="⚡ Min VRAM:" FontSize="12" Foreground="{StaticResource TextMutedBrush}"/>
<TextBlock Text="{Binding MinVram, ElementName=RootControl}" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource SecondaryBrush}"/>
</StackPanel>
</Border>
</StackPanel>

<Button Grid.Row="0" Grid.Column="1" Grid.RowSpan="2"
Content="📥 Install Feature Pack"
Command="{Binding InstallCommand, ElementName=RootControl}"
CommandParameter="{Binding InstallCommandParameter, ElementName=RootControl}"
Classes="matte-primary" VerticalAlignment="Center" Padding="16,10"/>
</Grid>
</Border>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace LocalLLMServerManager.Shared.Views.Controls;

public partial class FeaturePackBannerControl : UserControl
{
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<FeaturePackBannerControl, string>(nameof(Title), "Feature Pack Available");

public static readonly StyledProperty<string> DescriptionProperty =
AvaloniaProperty.Register<FeaturePackBannerControl, string>(nameof(Description), "Optional feature pack description.");

public static readonly StyledProperty<string> DiskSizeProperty =
AvaloniaProperty.Register<FeaturePackBannerControl, string>(nameof(DiskSize), "0 MB");

public static readonly StyledProperty<string> MinVramProperty =
AvaloniaProperty.Register<FeaturePackBannerControl, string>(nameof(MinVram), "4 GB");

public static readonly StyledProperty<ICommand?> InstallCommandProperty =
AvaloniaProperty.Register<FeaturePackBannerControl, ICommand?>(nameof(InstallCommand));

public static readonly StyledProperty<object?> InstallCommandParameterProperty =
AvaloniaProperty.Register<FeaturePackBannerControl, object?>(nameof(InstallCommandParameter));

public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}

public string Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}

public string DiskSize
{
get => GetValue(DiskSizeProperty);
set => SetValue(DiskSizeProperty, value);
}

public string MinVram
{
get => GetValue(MinVramProperty);
set => SetValue(MinVramProperty, value);
}

public ICommand? InstallCommand
{
get => GetValue(InstallCommandProperty);
set => SetValue(InstallCommandProperty, value);
}

public object? InstallCommandParameter
{
get => GetValue(InstallCommandParameterProperty);
set => SetValue(InstallCommandParameterProperty, value);
}

public FeaturePackBannerControl()
{
InitializeComponent();
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
Loading
Loading