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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,6 @@ $RECYCLE.BIN/

# Vim temporary swap files
*.swp

# Playwright video traces
docs/images/videos/
62 changes: 53 additions & 9 deletions Endpoints/EngineEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ public static void MapEngineEndpoints(this WebApplication app)
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
var execPath = string.IsNullOrWhiteSpace(settings.ComfyUiExecutablePath) ? @"C:\AI\ComfyUI\run_nvidia_gpu.bat" : settings.ComfyUiExecutablePath;
var execPath = AiEngineManager.ResolveComfyExecutable(settings.ComfyUiExecutablePath);

if (!Program.IsSafePath(execPath) || !System.IO.File.Exists(Program.ResolvePath(execPath, @"C:\AI\ComfyUI\run_nvidia_gpu.bat")))
if (!Program.IsSafePath(execPath) || !System.IO.File.Exists(execPath))
{
return Results.BadRequest(new { message = $"Invalid or unsafe executable path: {execPath}" });
return Results.BadRequest(new { message = $"Invalid or missing ComfyUI executable path: {execPath}" });
}

await orchestrator.EnsureVramForComfyUiAsync();
Expand All @@ -64,11 +64,11 @@ public static void MapEngineEndpoints(this WebApplication app)
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
var execPath = string.IsNullOrWhiteSpace(settings.ForgeExecutablePath) ? @"C:\AI\webui\webui-user.bat" : settings.ForgeExecutablePath;
var execPath = AiEngineManager.ResolveForgeExecutable(settings.ForgeExecutablePath);

if (!Program.IsSafePath(execPath) || !System.IO.File.Exists(Program.ResolvePath(execPath, @"C:\AI\webui\webui-user.bat")))
if (!Program.IsSafePath(execPath) || !System.IO.File.Exists(execPath))
{
return Results.BadRequest(new { message = $"Invalid or unsafe executable path: {execPath}" });
return Results.BadRequest(new { message = $"Invalid or missing SD Forge executable path: {execPath}" });
}

var success = await engineManager.StartForgeAsync(execPath, logger);
Expand Down Expand Up @@ -96,13 +96,13 @@ public static void MapEngineEndpoints(this WebApplication app)
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
var execPath = string.IsNullOrWhiteSpace(settings.AudioEngineExecutablePath) ? @"C:\AI\Kokoro-FastAPI\main.py" : settings.AudioEngineExecutablePath;
var execPath = AiEngineManager.ResolveAudioExecutable(settings.AudioEngineExecutablePath);

if (!execPath.TrimStart().StartsWith("docker", StringComparison.OrdinalIgnoreCase))
{
if (!Program.IsSafePath(execPath) || !System.IO.File.Exists(Program.ResolvePath(execPath, @"C:\AI\Kokoro-FastAPI\main.py")))
if (!Program.IsSafePath(execPath) || !System.IO.File.Exists(execPath))
{
return Results.BadRequest(new { message = $"Invalid or unsafe executable path: {execPath}" });
return Results.BadRequest(new { message = $"Invalid or missing Audio Engine executable path: {execPath}" });
}
}

Expand Down Expand Up @@ -152,5 +152,49 @@ public static void MapEngineEndpoints(this WebApplication app)
};
return Results.Ok(new { voices = defaultVoices, preferred = settings.PreferredAudioVoice });
});

app.MapPost("/api/ollama/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var settings = settingsService.LoadSettings();
var execPath = AiEngineManager.ResolveOllamaExecutable(settings.OllamaExecutablePath);

var success = await engineManager.StartOllamaAsync(execPath, logger);
if (success)
{
return Results.Ok(new { message = "Ollama Started", pid = engineManager.OllamaProcess?.Id });
}
return Results.Problem("Failed to start Ollama process");
});

app.MapPost("/api/ollama/stop", async (IAiEngineManager engineManager, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("EngineEndpoints");
var success = await engineManager.StopOllamaAsync(logger);
return Results.Ok(new { message = "Ollama Stopped" });
});

app.MapPost("/api/engine/start", async (EngineToggleRequest req, IAiEngineManager engineManager) =>
{
var result = await engineManager.StartEngineAsync(req.Engine ?? "");
if (result.Success)
{
return Results.Ok(new { message = result.Message, pid = result.Pid });
}
return Results.Problem(result.Message);
});

app.MapPost("/api/engine/stop", async (EngineToggleRequest req, IAiEngineManager engineManager) =>
{
var result = await engineManager.StopEngineAsync(req.Engine ?? "");
if (result.Success)
{
return Results.Ok(new { message = result.Message });
}
return Results.Problem(result.Message);
});
}
}

public record EngineToggleRequest(string? Engine);

2 changes: 1 addition & 1 deletion Endpoints/WorkflowEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ public static void MapWorkflowEndpoints(this WebApplication app)
var dirResults = processedFiles.Where(r => r.dir == dir && r.result != null);
foreach (var res in dirResults)
{
if (seenIds.Add(res.id))
if (res.result != null && seenIds.Add(res.id))
{
list.Add(res.result);
}
Expand Down
110 changes: 110 additions & 0 deletions LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using CommunityToolkit.Mvvm.ComponentModel;

namespace LocalLLMServerManager.Shared.Models;

public partial class EngineStatusCardModel : ObservableObject
{
[ObservableProperty] private string _engineKey = "";
[ObservableProperty] private string _displayName = "";

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusColor))]
[NotifyPropertyChangedFor(nameof(ActionButtonText))]
[NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(DisplaySubtitle))]
private bool _isOnline;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusColor))]
[NotifyPropertyChangedFor(nameof(ActionButtonText))]
[NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(DisplaySubtitle))]
private bool _isStarting;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusColor))]
[NotifyPropertyChangedFor(nameof(ActionButtonText))]
[NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(DisplaySubtitle))]
private bool _isError;

[ObservableProperty] private int _port;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasActiveModel))]
[NotifyPropertyChangedFor(nameof(DisplaySubtitle))]
private string _activeModel = "";

[ObservableProperty] private string _memoryUsage = "";
[ObservableProperty] private string _statusTooltip = "";

public string StatusColor => IsError ? "#ef4444" : IsOnline ? "#10b981" : IsStarting ? "#f59e0b" : "#64748b";
public string ActionButtonText => IsOnline ? "⏹ Stop" : "▷ Start";
public string StatusText => IsError ? "Error" : IsOnline ? "Online" : IsStarting ? "Starting..." : "Offline";
public bool HasActiveModel => !string.IsNullOrWhiteSpace(ActiveModel);
public string DisplaySubtitle => HasActiveModel ? ActiveModel : StatusText;

public EngineStatusCardModel()
{
}

public EngineStatusCardModel(string engineKey, string displayName, int port)
{
EngineKey = engineKey;
DisplayName = displayName;
Port = port;
StatusTooltip = $"{displayName}: Offline (Port {port})";
}
}

public class TelemetryData
{
public bool OllamaOnline { get; set; }
public bool ComfyOnline { get; set; }
public bool ForgeOnline { get; set; }
public bool KokoroOnline { get; set; }

public bool OllamaStarting { get; set; }
public bool ComfyStarting { get; set; }
public bool ForgeStarting { get; set; }
public bool KokoroStarting { get; set; }

public string OllamaModel { get; set; } = "";
public string ComfyModel { get; set; } = "";
public string ForgeModel { get; set; } = "";
public string KokoroModel { get; set; } = "";

public string OllamaMemory { get; set; } = "";
public string ComfyMemory { get; set; } = "";
public string ForgeMemory { get; set; } = "";
public string KokoroMemory { get; set; } = "";

public string GpuName { get; set; } = "";
public double UsedVramGb { get; set; }
public double TotalVramGb { get; set; } = 16.0;
public int VramPercent { get; set; }

public TelemetryData()
{
}

public TelemetryData(
bool ollamaOnline = false,
bool comfyOnline = false,
bool forgeOnline = false,
bool kokoroOnline = false,
string? ollamaModel = null,
string? comfyModel = null,
string? forgeModel = null,
string? kokoroModel = null)
{
OllamaOnline = ollamaOnline;
ComfyOnline = comfyOnline;
ForgeOnline = forgeOnline;
KokoroOnline = kokoroOnline;
OllamaModel = ollamaModel ?? "";
ComfyModel = comfyModel ?? "";
ForgeModel = forgeModel ?? "";
KokoroModel = kokoroModel ?? "";
}
}
28 changes: 14 additions & 14 deletions LocalLLMServerManager.Shared/Styles/DesignTokens.axaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Color Definitions -->
<Color x:Key="BgDarkColor">#0d1117</Color>
<Color x:Key="BgSurfaceColor">#161b22</Color>
<Color x:Key="BgCardColor">#1c2128</Color>
<Color x:Key="BorderColor">#30363d</Color>
<Color x:Key="PrimaryColor">#388bfd</Color>
<Color x:Key="SecondaryColor">#58a6ff</Color>
<Color x:Key="AccentColor">#79c0ff</Color>
<Color x:Key="TextMainColor">#f0f6fc</Color>
<Color x:Key="TextMutedColor">#8b949e</Color>
<Color x:Key="OnlineColor">#238636</Color>
<Color x:Key="OfflineColor">#da3633</Color>
<Color x:Key="WarningColor">#d29922</Color>
<!-- Color Definitions (Carbon & Indigo Workstation) -->
<Color x:Key="BgDarkColor">#090A0F</Color>
<Color x:Key="BgSurfaceColor">#12141C</Color>
<Color x:Key="BgCardColor">#181B26</Color>
<Color x:Key="BorderColor">#222634</Color>
<Color x:Key="PrimaryColor">#6366F1</Color>
<Color x:Key="SecondaryColor">#4F46E5</Color>
<Color x:Key="AccentColor">#8B5CF6</Color>
<Color x:Key="TextMainColor">#F8FAFC</Color>
<Color x:Key="TextMutedColor">#94A3B8</Color>
<Color x:Key="OnlineColor">#10B981</Color>
<Color x:Key="OfflineColor">#EF4444</Color>
<Color x:Key="WarningColor">#F59E0B</Color>

<!-- Solid Brushes -->
<SolidColorBrush x:Key="BgDarkBrush" Color="{StaticResource BgDarkColor}" />
<SolidColorBrush x:Key="BgSurfaceBrush" Color="{StaticResource BgSurfaceColor}" />
<SolidColorBrush x:Key="BgCardBrush" Color="{StaticResource BgCardColor}" />
<SolidColorBrush x:Key="GlassBorderBrush" Color="{StaticResource BorderColor}" />
<SolidColorBrush x:Key="BorderColorBrush" Color="{StaticResource BorderColor}" />
<SolidColorBrush x:Key="BorderGlowBrush" Color="#388bfd" Opacity="0.4" />
<SolidColorBrush x:Key="BorderGlowBrush" Color="#6366F1" Opacity="0.4" />
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}" />
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource SecondaryColor}" />
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
Expand Down
37 changes: 20 additions & 17 deletions LocalLLMServerManager.Shared/Styles/MatteTheme.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,46 @@
</ResourceDictionary>
</Styles.Resources>

<!-- Matte Card Style (Flat, High-Contrast Carbon) -->
<!-- Matte Card Style (Elevated Carbon & Indigo Workstation) -->
<Style Selector="Border.matte-card, Border.glass-card">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource GlassBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="18" />
<Setter Property="BoxShadow" Value="0 4 24 0 #40000000" />
</Style>

<!-- Telemetry / Metadata Pill Style -->
<Style Selector="Border.matte-pill, Border.telemetry-pill">
<Setter Property="Background" Value="{StaticResource BgSurfaceBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource GlassBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="10,5" />
</Style>

<!-- Primary Matte Solid Button Style -->
<Style Selector="Button.matte-primary, Button.glass-primary">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Background" Value="{StaticResource SecondaryBrush}" />
<Setter Property="Foreground" Value="#ffffff" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="14,8" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.matte-primary /template/ ContentPresenter#PART_ContentPresenter, Button.glass-primary /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="6" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.matte-primary:pointerover /template/ ContentPresenter#PART_ContentPresenter, Button.glass-primary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource SecondaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource SecondaryBrush}" />
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="#ffffff" />
</Style>
<Style Selector="Button.matte-primary:pressed /template/ ContentPresenter#PART_ContentPresenter, Button.glass-primary:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Background" Value="{StaticResource SecondaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Opacity" Value="0.85" />
</Style>
Expand All @@ -58,16 +60,17 @@
<Setter Property="Foreground" Value="{StaticResource TextMainBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource GlassBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="12,7" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.matte-secondary /template/ ContentPresenter#PART_ContentPresenter, Button.glass-secondary /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="6" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.matte-secondary:pointerover /template/ ContentPresenter#PART_ContentPresenter, Button.glass-secondary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextMainBrush}" />
<Setter Property="Foreground" Value="#ffffff" />
</Style>
<Style Selector="Button.matte-secondary:pressed /template/ ContentPresenter#PART_ContentPresenter, Button.glass-secondary:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource BgSurfaceBrush}" />
Expand All @@ -86,16 +89,16 @@

<!-- Matte Input TextBox Style -->
<Style Selector="TextBox.matte-input, TextBox.glass-input">
<Setter Property="Background" Value="{StaticResource BgSurfaceBrush}" />
<Setter Property="Background" Value="#0D0E15" />
<Setter Property="Foreground" Value="{StaticResource TextMainBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource GlassBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="10,8" />
</Style>
<Style Selector="TextBox.matte-input:focus /template/ Border#PART_BorderElement, TextBox.glass-input:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="Background" Value="{StaticResource BgSurfaceBrush}" />
</Style>

<!-- Modern Segmented TabControl Style -->
Expand Down
11 changes: 11 additions & 0 deletions LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,21 @@ public partial class DocumentationViewModel : ObservableObject

public Action<int>? OnNavigateToTabRequested { get; set; }
public Action? OnPopOutNativeWindowRequested { get; set; }
public Action<string>? OnAskCopilotRequested { get; set; }

[ObservableProperty] private bool _isDrawerOpen = false;
[ObservableProperty] private bool _isDetailActive = false;

[RelayCommand]
public void AskCopilotAboutGuide()
{
if (SelectedSection != null)
{
var prompt = $"Can you explain '{SelectedSection.Title}' ({SelectedSection.Summary}) and how I use it in the Studio?";
OnAskCopilotRequested?.Invoke(prompt);
}
}

[RelayCommand]
public void BackToTopics()
{
Expand Down
Loading
Loading