diff --git a/.gitignore b/.gitignore index f25c541..65fb6cb 100644 --- a/.gitignore +++ b/.gitignore @@ -496,3 +496,6 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# Playwright video traces +docs/images/videos/ diff --git a/Endpoints/EngineEndpoints.cs b/Endpoints/EngineEndpoints.cs index 2171859..dbdee43 100644 --- a/Endpoints/EngineEndpoints.cs +++ b/Endpoints/EngineEndpoints.cs @@ -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(); @@ -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); @@ -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}" }); } } @@ -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); + diff --git a/Endpoints/WorkflowEndpoints.cs b/Endpoints/WorkflowEndpoints.cs index 94e9510..dba39de 100644 --- a/Endpoints/WorkflowEndpoints.cs +++ b/Endpoints/WorkflowEndpoints.cs @@ -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); } diff --git a/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs b/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs new file mode 100644 index 0000000..12bbdbe --- /dev/null +++ b/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs @@ -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 ?? ""; + } +} diff --git a/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml b/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml index 62e2fac..4eaccb9 100644 --- a/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml +++ b/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml @@ -1,18 +1,18 @@ - - #0d1117 - #161b22 - #1c2128 - #30363d - #388bfd - #58a6ff - #79c0ff - #f0f6fc - #8b949e - #238636 - #da3633 - #d29922 + + #090A0F + #12141C + #181B26 + #222634 + #6366F1 + #4F46E5 + #8B5CF6 + #F8FAFC + #94A3B8 + #10B981 + #EF4444 + #F59E0B @@ -20,7 +20,7 @@ - + diff --git a/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml b/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml index 5b5c985..62af5c0 100644 --- a/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml +++ b/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml @@ -10,13 +10,14 @@ - + @@ -24,30 +25,31 @@ - - + + @@ -58,16 +60,17 @@ - + + diff --git a/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs index c995638..ffda35f 100644 --- a/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs @@ -56,10 +56,21 @@ public partial class DocumentationViewModel : ObservableObject public Action? OnNavigateToTabRequested { get; set; } public Action? OnPopOutNativeWindowRequested { get; set; } + public Action? 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() { diff --git a/LocalLLMServerManager.Shared/ViewModels/EngineStudioViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/EngineStudioViewModel.cs new file mode 100644 index 0000000..1aeba3b --- /dev/null +++ b/LocalLLMServerManager.Shared/ViewModels/EngineStudioViewModel.cs @@ -0,0 +1,332 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace LocalLLMServerManager.Shared.ViewModels; + +/// +/// ViewModel managing the fluid creative studio canvas, top modality switching, +/// creative prompt dock, and fine-tuning parameters flyout. +/// +public partial class EngineStudioViewModel : ObservableObject +{ + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsImageModalityActive))] + [NotifyPropertyChangedFor(nameof(IsTextModalityActive))] + [NotifyPropertyChangedFor(nameof(IsVideoModalityActive))] + [NotifyPropertyChangedFor(nameof(Is3DModalityActive))] + [NotifyPropertyChangedFor(nameof(IsAudioModalityActive))] + [NotifyPropertyChangedFor(nameof(IsStickerModalityActive))] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + [NotifyPropertyChangedFor(nameof(ActivePromptPlaceholder))] + private string _selectedModality = "Image"; + + public bool IsImageModalityActive => SelectedModality == "Image" || SelectedModality == "Images"; + public bool IsTextModalityActive => SelectedModality == "Text"; + public bool IsVideoModalityActive => SelectedModality == "Video"; + public bool Is3DModalityActive => SelectedModality == "3D Mesh"; + public bool IsAudioModalityActive => SelectedModality == "Audio"; + public bool IsStickerModalityActive => SelectedModality == "Sticker" || SelectedModality == "Sticker Studio"; + + [ObservableProperty] + private bool _isParametersFlyoutOpen; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + private string _imageModel = "SDXL Base 1.0"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + private string _textModel = "llama3.2:latest"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + private string _videoModel = "Wan 2.2 / LTX-2.5"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + private string _meshModel = "TRELLIS V2 (Gaussian Splat)"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + private string _audioModel = "Kokoro TTS (af_heart)"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + private string _stickerModel = "Sticker Studio (BirefNet + SDXL)"; + + [ObservableProperty] + private string _activeAspectPreset = "16:9"; + + [ObservableProperty] + private int _steps = 30; + + [ObservableProperty] + private double _cfgScale = 7.0; + + [ObservableProperty] + private long _seed = -1; + + [ObservableProperty] + private double _denoise = 0.75; + + [ObservableProperty] + private string _promptText = ""; + + [ObservableProperty] + private string _negativePromptText = ""; + + [ObservableProperty] + private bool _isGenerating; + + [ObservableProperty] + private string _attachedImagePath = ""; + + [ObservableProperty] + private bool _hasAttachedImage; + + public string ActiveModelBadge => SelectedModality switch + { + "Image" or "Images" => !string.IsNullOrWhiteSpace(ImageModel) ? ImageModel : "SDXL Base 1.0", + "Text" => !string.IsNullOrWhiteSpace(TextModel) ? TextModel : "llama3.2:latest", + "Video" => !string.IsNullOrWhiteSpace(VideoModel) ? VideoModel : "Wan 2.2 / LTX-2.5", + "3D Mesh" => !string.IsNullOrWhiteSpace(MeshModel) ? MeshModel : "TRELLIS V2 (Gaussian Splat)", + "Audio" => !string.IsNullOrWhiteSpace(AudioModel) ? AudioModel : "Kokoro TTS (af_heart)", + "Sticker" or "Sticker Studio" => !string.IsNullOrWhiteSpace(StickerModel) ? StickerModel : "Sticker Studio (BirefNet + SDXL)", + _ => "SDXL Base 1.0" + }; + + public string ActivePromptPlaceholder => SelectedModality switch + { + "Image" or "Images" => "Describe an image to generate with SDXL & Forge (e.g. 'cyberpunk street in rain')...", + "Text" => "Enter a prompt or query for the local LLM...", + "Video" => "Describe action and motion for video generation (e.g. 'cinematic drone sweep over mountains')...", + "3D Mesh" => "Describe a 3D object to sculpt or synthesize (e.g. 'ancient stone relic with glowing runes')...", + "Audio" => "Describe audio atmosphere or enter speech to synthesize with Kokoro TTS...", + "Sticker" or "Sticker Studio" => "Describe a sticker design to generate with transparent background (e.g. 'cute astronaut cat sticker, die-cut vector')...", + _ => "Describe what you want to create..." + }; + + [RelayCommand] + public void SelectModality(string modality) + { + if (string.IsNullOrWhiteSpace(modality)) return; + SelectedModality = modality; + OnPropertyChanged(nameof(IsImageModalityActive)); + OnPropertyChanged(nameof(IsTextModalityActive)); + OnPropertyChanged(nameof(IsVideoModalityActive)); + OnPropertyChanged(nameof(Is3DModalityActive)); + OnPropertyChanged(nameof(IsAudioModalityActive)); + OnPropertyChanged(nameof(IsStickerModalityActive)); + OnPropertyChanged(nameof(ActiveModelBadge)); + OnPropertyChanged(nameof(ActivePromptPlaceholder)); + } + + [RelayCommand] + public void ToggleParametersFlyout() + { + IsParametersFlyoutOpen = !IsParametersFlyoutOpen; + } + + [RelayCommand] + public void SelectAspectPreset(string preset) + { + if (!string.IsNullOrWhiteSpace(preset)) + { + ActiveAspectPreset = preset; + } + } + + [RelayCommand] + public void AttachImage() + { + HasAttachedImage = !HasAttachedImage; + } + + [RelayCommand] + public void ApplyPromptChip(string prompt) + { + if (!string.IsNullOrWhiteSpace(prompt)) + { + PromptText = prompt; + } + } + + [RelayCommand] + public void GenerateFromDock() + { + Generate(); + } + + [RelayCommand] + public void Generate() + { + IsGenerating = !IsGenerating; + } +} + +/// +/// Partial class extensions to MainViewModel to support the Fluid Studio Canvas & Creative Prompt Dock +/// when EngineStudioTabControl is hosted with DataContext="{Binding}" (MainViewModel). +/// +public partial class MainViewModel +{ + [ObservableProperty] + private EngineStudioViewModel _studio = new(); + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsImageModalityActive))] + [NotifyPropertyChangedFor(nameof(IsTextModalityActive))] + [NotifyPropertyChangedFor(nameof(IsVideoModalityActive))] + [NotifyPropertyChangedFor(nameof(Is3DModalityActive))] + [NotifyPropertyChangedFor(nameof(IsAudioModalityActive))] + [NotifyPropertyChangedFor(nameof(ActiveModelBadge))] + [NotifyPropertyChangedFor(nameof(ActivePromptPlaceholder))] + private string _selectedModality = "Image"; + + public bool IsImageModalityActive => SelectedModality == "Image" || SelectedModality == "Images" || SelectedStudioMode == "Images"; + public bool IsTextModalityActive => SelectedModality == "Text" || SelectedStudioMode == "Text"; + public bool IsVideoModalityActive => SelectedModality == "Video" || SelectedStudioMode == "Video"; + public bool Is3DModalityActive => SelectedModality == "3D Mesh" || SelectedStudioMode == "3D Mesh"; + public bool IsAudioModalityActive => SelectedModality == "Audio" || SelectedStudioMode == "Audio"; + + [ObservableProperty] + private bool _isParametersFlyoutOpen; + + [ObservableProperty] + private string _activeAspectPreset = "16:9"; + + [ObservableProperty] + private int _studioSteps = 30; + + [ObservableProperty] + private double _studioCfgScale = 7.0; + + [ObservableProperty] + private long _studioSeed = -1; + + [ObservableProperty] + private double _studioDenoise = 0.75; + + [ObservableProperty] + private string _promptText = ""; + + [ObservableProperty] + private string _negativePromptText = ""; + + public string ActiveModelBadge => SelectedModality switch + { + "Image" or "Images" => !string.IsNullOrWhiteSpace(SelectedImageWorkflow) ? SelectedImageWorkflow : "SDXL Base 1.0", + "Text" => Ollama?.SelectedInstalledModel?.Name ?? (!string.IsNullOrWhiteSpace(Telemetry?.OllamaModelName) ? Telemetry.OllamaModelName : "llama3.2:latest"), + "Video" => !string.IsNullOrWhiteSpace(SelectedVideoWorkflow) ? SelectedVideoWorkflow : "Wan 2.2 / LTX-2.5", + "3D Mesh" => !string.IsNullOrWhiteSpace(Selected3DFormat) ? $"TRELLIS V2 ({Selected3DFormat})" : "TRELLIS V2 (Gaussian Splat)", + "Audio" => Audio != null ? $"Kokoro TTS ({Audio.VoiceProfile})" : "Kokoro TTS (af_heart)", + _ => "SDXL Base 1.0" + }; + + public string ActivePromptPlaceholder => SelectedModality switch + { + "Image" or "Images" => "Describe an image to generate with SDXL & Forge (e.g. 'cyberpunk street in rain')...", + "Text" => "Enter a prompt or query for the local LLM...", + "Video" => "Describe action and motion for video generation (e.g. 'cinematic drone sweep over mountains')...", + "3D Mesh" => "Describe a 3D object to sculpt or synthesize (e.g. 'ancient stone relic with glowing runes')...", + "Audio" => "Describe audio atmosphere or enter speech to synthesize with Kokoro TTS...", + _ => "Describe what you want to create..." + }; + + [RelayCommand] + public void SelectModality(string modality) + { + if (string.IsNullOrWhiteSpace(modality)) return; + SelectedModality = modality; + if (modality == "Sticker" || modality == "Sticker Studio") + { + IsStickerStudioActive = true; + SelectedStudioMode = "Sticker Studio"; + } + else + { + IsStickerStudioActive = false; + SelectedStudioMode = modality switch + { + "Image" => "Images", + _ => modality + }; + } + Studio.SelectModality(modality); + OnPropertyChanged(nameof(IsImageModalityActive)); + OnPropertyChanged(nameof(IsTextModalityActive)); + OnPropertyChanged(nameof(IsVideoModalityActive)); + OnPropertyChanged(nameof(Is3DModalityActive)); + OnPropertyChanged(nameof(IsAudioModalityActive)); + OnPropertyChanged(nameof(IsStickerModalityActive)); + OnPropertyChanged(nameof(ActiveModelBadge)); + OnPropertyChanged(nameof(ActivePromptPlaceholder)); + } + + [RelayCommand] + public void ToggleParametersFlyout() + { + IsParametersFlyoutOpen = !IsParametersFlyoutOpen; + Studio.IsParametersFlyoutOpen = IsParametersFlyoutOpen; + } + + [RelayCommand] + public void AttachImage() + { + Studio.AttachImage(); + } + + [RelayCommand] + public void SelectAspectPreset(string preset) + { + if (!string.IsNullOrWhiteSpace(preset)) + { + ActiveAspectPreset = preset; + Studio.ActiveAspectPreset = preset; + } + } + + [RelayCommand] + public void ApplyPromptChip(string prompt) + { + if (string.IsNullOrWhiteSpace(prompt)) return; + PromptText = prompt; + Studio.PromptText = prompt; + if (IsImageModalityActive) ImagePrompt = prompt; + else if (IsTextModalityActive) OllamaPrompt = prompt; + else if (IsVideoModalityActive) VideoPrompt = prompt; + else if (Is3DModalityActive) Prompt3D = prompt; + else if (IsAudioModalityActive && Audio != null) Audio.Prompt = prompt; + } + + [RelayCommand] + public void GenerateFromDock() + { + if (IsImageModalityActive) + { + if (!string.IsNullOrWhiteSpace(PromptText)) ImagePrompt = PromptText; + OpenForgeWebUiCommand.Execute(null); + } + else if (IsTextModalityActive) + { + if (!string.IsNullOrWhiteSpace(PromptText)) OllamaPrompt = PromptText; + GenerateOllamaTextCommand.Execute(null); + } + else if (IsVideoModalityActive) + { + if (!string.IsNullOrWhiteSpace(PromptText)) VideoPrompt = PromptText; + GenerateVideoCommand.Execute(null); + } + else if (Is3DModalityActive) + { + if (!string.IsNullOrWhiteSpace(PromptText)) Prompt3D = PromptText; + Generate3DCommand.Execute(null); + } + else if (IsAudioModalityActive && Audio != null) + { + if (!string.IsNullOrWhiteSpace(PromptText)) Audio.Prompt = PromptText; + Audio.GenerateAudioCommand.Execute(null); + } + } +} diff --git a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs index fffcfbe..3a3b7b8 100644 --- a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs @@ -130,6 +130,58 @@ partial void OnSelectedTabIndexChanged(int value) [ObservableProperty] private bool _isAnyDrawerOpen = false; [ObservableProperty] private bool _isDesktopHost = false; + [ObservableProperty] private bool _isCopilotSidebarOpen = false; + [ObservableProperty] private string _selectedCopilotTab = "Assistant"; + + public bool IsAssistantTabActive => SelectedCopilotTab == "Assistant"; + public bool IsDocsTabActive => SelectedCopilotTab == "Docs"; + + [RelayCommand] + public void ToggleCopilotSidebar(string? tab = null) + { + if (tab != null && SelectedCopilotTab != tab && IsCopilotSidebarOpen) + { + SelectedCopilotTab = tab; + OnPropertyChanged(nameof(IsAssistantTabActive)); + OnPropertyChanged(nameof(IsDocsTabActive)); + UpdateIsAnyDrawerOpen(); + return; + } + + if (tab != null) + { + SelectedCopilotTab = tab; + OnPropertyChanged(nameof(IsAssistantTabActive)); + OnPropertyChanged(nameof(IsDocsTabActive)); + } + + IsCopilotSidebarOpen = !IsCopilotSidebarOpen; + + if (IsCopilotSidebarOpen && SelectedCopilotTab == "Assistant" && Assistant.AvailableModelCapabilities.Count <= 1) + { + _ = Assistant.LoadAvailableModelsAsync(); + } + + UpdateIsAnyDrawerOpen(); + } + + [RelayCommand] + public void SelectCopilotTab(string tab) + { + SelectedCopilotTab = tab; + OnPropertyChanged(nameof(IsAssistantTabActive)); + OnPropertyChanged(nameof(IsDocsTabActive)); + if (!IsCopilotSidebarOpen) + { + IsCopilotSidebarOpen = true; + } + + if (SelectedCopilotTab == "Assistant" && Assistant.AvailableModelCapabilities.Count <= 1) + { + _ = Assistant.LoadAvailableModelsAsync(); + } + UpdateIsAnyDrawerOpen(); + } [RelayCommand] public void ToggleDocumentationDrawer() @@ -140,10 +192,7 @@ public void ToggleDocumentationDrawer() return; } - Assistant.IsDrawerOpen = false; - Ollama.IsPullDrawerOpen = false; - Documentation.IsDrawerOpen = !Documentation.IsDrawerOpen; - UpdateIsAnyDrawerOpen(); + ToggleCopilotSidebar("Docs"); } [RelayCommand] @@ -155,28 +204,20 @@ public void ToggleAiAssistDrawer() return; } - Documentation.IsDrawerOpen = false; - Ollama.IsPullDrawerOpen = false; - Assistant.IsDrawerOpen = !Assistant.IsDrawerOpen; - if (Assistant.IsDrawerOpen && Assistant.AvailableModelCapabilities.Count <= 1) - { - _ = Assistant.LoadAvailableModelsAsync(); - } - UpdateIsAnyDrawerOpen(); + ToggleCopilotSidebar("Assistant"); } [RelayCommand] public void CloseDrawers() { - Documentation.IsDrawerOpen = false; - Assistant.IsDrawerOpen = false; + IsCopilotSidebarOpen = false; Ollama.ClosePullDrawer(); UpdateIsAnyDrawerOpen(); } public void UpdateIsAnyDrawerOpen() { - IsAnyDrawerOpen = Documentation.IsDrawerOpen || Assistant.IsDrawerOpen || Ollama.IsPullDrawerOpen; + IsAnyDrawerOpen = IsCopilotSidebarOpen || Ollama.IsPullDrawerOpen; } @@ -222,6 +263,7 @@ public MainViewModel( _canIRunItService = canIRunItService ?? new CanIRunItService(); PresetService = studioPresetService ?? new StudioPresetService(); Telemetry = new TelemetryViewModel(telemetryService) { ApiBase = ApiBase }; + Telemetry.OnManageServiceRequested = (serviceName) => ToggleServiceInteractive(serviceName); HardwareFit = new CanIRunItViewModel(_canIRunItService, telemetryService, httpClient) { ApiBase = ApiBase }; Ollama = new OllamaLibraryViewModel(ollamaModelService, _canIRunItService, telemetryService) { @@ -259,6 +301,11 @@ public MainViewModel( ApiBase = ApiBase }; Documentation.OnNavigateToTabRequested = tab => SelectedTabIndex = tab; + Documentation.OnAskCopilotRequested = prompt => + { + SelectCopilotTab("Assistant"); + Assistant.InputText = prompt; + }; NavigationRail.PropertyChanged += (s, e) => { @@ -385,12 +432,13 @@ public string ApiBase [RelayCommand] public void ToggleServiceInteractive(string serviceName) { - _serviceConfirmTarget = serviceName.ToLowerInvariant(); + _serviceConfirmTarget = serviceName.Trim(); + var lower = _serviceConfirmTarget.ToLowerInvariant(); bool isOnline = false; - if (_serviceConfirmTarget == "ollama") isOnline = Telemetry.IsOllamaOnline; - else if (_serviceConfirmTarget == "forge") isOnline = Telemetry.IsForgeOnline; - else if (_serviceConfirmTarget == "comfy") isOnline = Telemetry.IsComfyOnline; + if (lower.Contains("ollama")) isOnline = Telemetry.IsOllamaOnline; + else if (lower.Contains("forge")) isOnline = Telemetry.IsForgeOnline; + else if (lower.Contains("comfy")) isOnline = Telemetry.IsComfyOnline; string action = isOnline ? "stop" : "start"; ServiceConfirmMessage = $"Are you sure you want to {action} the {serviceName} engine?"; @@ -563,6 +611,7 @@ public void CancelServiceConfirm() public bool IsVideoWorkflowActive => !IsStickerStudioActive && SelectedStudioMode == "Video"; public bool Is3DMeshWorkflowActive => !IsStickerStudioActive && SelectedStudioMode == "3D Mesh"; public bool IsAudioWorkflowActive => !IsStickerStudioActive && SelectedStudioMode == "Audio"; + public bool IsStickerModalityActive => IsStickerStudioActive || SelectedStudioMode == "Sticker Studio" || SelectedModality == "Sticker" || SelectedModality == "Sticker Studio"; [ObservableProperty] private string _ollamaPrompt = "Explain how local LLM quantization works in plain language."; @@ -1178,17 +1227,48 @@ public void NavigateToDocumentationTab() [RelayCommand] public async Task ToggleEngineAsync(string engineName) { + var rawName = engineName.Trim(); + var normalized = rawName.ToLowerInvariant().Replace(" sd", ""); + if (normalized.Contains("comfy")) normalized = "comfy"; + else if (normalized.Contains("forge")) normalized = "forge"; + else if (normalized.Contains("ollama")) normalized = "ollama"; + else if (normalized.Contains("audio")) normalized = "audio"; + + bool isOnline = false; + if (normalized == "ollama") isOnline = Telemetry.IsOllamaOnline; + else if (normalized == "forge") isOnline = Telemetry.IsForgeOnline; + else if (normalized == "comfy") isOnline = Telemetry.IsComfyOnline; + + bool isStart = !isOnline; + string actionWord = isStart ? "start" : "stop"; + + ToastService.Instance.Show($"{(isStart ? "Starting" : "Stopping")} {rawName}...", ToastType.Info, 2500); + try { + var endpoint = isStart ? $"/api/{normalized}/start" : $"/api/{normalized}/stop"; var content = new StringContent( - JsonSerializer.Serialize(new { engine = engineName }), + JsonSerializer.Serialize(new { engine = normalized }), System.Text.Encoding.UTF8, "application/json" ); - await Http.PostAsync($"{ApiBase}/api/comfy/start", content); + var response = await Http.PostAsync($"{ApiBase}{endpoint}", content); + if (response.IsSuccessStatusCode) + { + ToastService.Instance.Show($"{rawName} {(isStart ? "started" : "stopped")} successfully.", ToastType.Success); + } + else + { + var errorText = await response.Content.ReadAsStringAsync(); + var msg = string.IsNullOrWhiteSpace(errorText) ? $"Server returned {response.StatusCode}" : errorText; + ToastService.Instance.Show($"Failed to {actionWord} {rawName}: {msg}", ToastType.Error, 6000); + } await RefreshStatusAsync(); } - catch { } + catch (Exception ex) + { + ToastService.Instance.Show($"Error while attempting to {actionWord} {rawName}: {ex.Message}", ToastType.Error, 6000); + } } [RelayCommand] diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs index 175e90d..98270b6 100644 --- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs @@ -35,6 +35,25 @@ public partial class SettingsViewModel : ObservableObject public bool IsFilterImageActive => SelectedPresetModalityFilter.Equals("Image", StringComparison.OrdinalIgnoreCase); public bool IsFilterAudioActive => SelectedPresetModalityFilter.Equals("Audio", StringComparison.OrdinalIgnoreCase); + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsAppearanceCategoryActive))] + [NotifyPropertyChangedFor(nameof(IsEnginesCategoryActive))] + [NotifyPropertyChangedFor(nameof(IsFeaturePacksCategoryActive))] + [NotifyPropertyChangedFor(nameof(IsNetworkCategoryActive))] + private string _selectedCategory = "Appearance"; + + public bool IsAppearanceCategoryActive => string.IsNullOrWhiteSpace(SelectedCategory) || string.Equals(SelectedCategory, "Appearance", StringComparison.OrdinalIgnoreCase); + public bool IsEnginesCategoryActive => string.Equals(SelectedCategory, "Engines", StringComparison.OrdinalIgnoreCase); + public bool IsFeaturePacksCategoryActive => string.Equals(SelectedCategory, "FeaturePacks", StringComparison.OrdinalIgnoreCase); + public bool IsNetworkCategoryActive => string.Equals(SelectedCategory, "Network", StringComparison.OrdinalIgnoreCase); + + [RelayCommand] + public void SelectCategory(string category) + { + if (string.IsNullOrWhiteSpace(category)) return; + SelectedCategory = category; + } + [ObservableProperty] private string _presetsJson = ""; [ObservableProperty] private string _forgeModelsPath = ""; diff --git a/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs index a0472a4..a4abec0 100644 --- a/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LocalLLMServerManager.Shared.Interfaces; +using LocalLLMServerManager.Shared.Models; using LocalLLMServerManager.Shared.Services; namespace LocalLLMServerManager.Shared.ViewModels; @@ -19,6 +20,16 @@ public partial class TelemetryViewModel : ObservableObject [ObservableProperty] private double _vramPercentage = 0.0; [ObservableProperty] private string _vramStatusText = "0.0 GB / 16.0 GB (0%)"; + [ObservableProperty] private EngineStatusCardModel _ollamaCard = new("ollama", "Ollama", 11434); + [ObservableProperty] private EngineStatusCardModel _comfyUiCard = new("comfyui", "ComfyUI", 8188); + [ObservableProperty] private EngineStatusCardModel _forgeCard = new("forge", "SD Forge", 7860); + [ObservableProperty] private EngineStatusCardModel _kokoroCard = new("kokoro", "Kokoro TTS", 8880); + + [ObservableProperty] private string _ollamaModelName = ""; + [ObservableProperty] private string _comfyModelName = ""; + [ObservableProperty] private string _forgeModelName = ""; + [ObservableProperty] private string _kokoroModelName = ""; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsOllamaOnline))] [NotifyPropertyChangedFor(nameof(OllamaStatusColor))] @@ -34,13 +45,81 @@ public partial class TelemetryViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(ComfyStatusColor))] private string _comfyStatus = "Offline"; - public bool IsOllamaOnline => string.Equals(OllamaStatus, "Online", StringComparison.OrdinalIgnoreCase); - public bool IsForgeOnline => string.Equals(ForgeStatus, "Online", StringComparison.OrdinalIgnoreCase); - public bool IsComfyOnline => string.Equals(ComfyStatus, "Online", StringComparison.OrdinalIgnoreCase); + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsKokoroOnline))] + [NotifyPropertyChangedFor(nameof(KokoroStatusColor))] + private string _kokoroStatus = "Offline"; + + public bool IsOllamaOnline => string.Equals(OllamaStatus, "Online", StringComparison.OrdinalIgnoreCase) || string.Equals(OllamaStatus, "Running", StringComparison.OrdinalIgnoreCase); + public bool IsForgeOnline => string.Equals(ForgeStatus, "Online", StringComparison.OrdinalIgnoreCase) || string.Equals(ForgeStatus, "Running", StringComparison.OrdinalIgnoreCase); + public bool IsComfyOnline => string.Equals(ComfyStatus, "Online", StringComparison.OrdinalIgnoreCase) || string.Equals(ComfyStatus, "Running", StringComparison.OrdinalIgnoreCase); + public bool IsKokoroOnline => string.Equals(KokoroStatus, "Online", StringComparison.OrdinalIgnoreCase) || string.Equals(KokoroStatus, "Running", StringComparison.OrdinalIgnoreCase); public string OllamaStatusColor => IsOllamaOnline ? "#22C55E" : "#64748B"; public string ForgeStatusColor => IsForgeOnline ? "#22C55E" : "#64748B"; public string ComfyStatusColor => IsComfyOnline ? "#22C55E" : "#64748B"; + public string KokoroStatusColor => IsKokoroOnline ? "#22C55E" : "#64748B"; + + partial void OnOllamaStatusChanged(string value) => UpdateCardFromStatus(OllamaCard, value); + partial void OnForgeStatusChanged(string value) => UpdateCardFromStatus(ForgeCard, value); + partial void OnComfyStatusChanged(string value) => UpdateCardFromStatus(ComfyUiCard, value); + partial void OnKokoroStatusChanged(string value) => UpdateCardFromStatus(KokoroCard, value); + + partial void OnOllamaModelNameChanged(string value) + { + if (OllamaCard != null) + { + OllamaCard.ActiveModel = value; + OllamaCard.StatusTooltip = $"{OllamaCard.DisplayName}: {(OllamaCard.IsOnline ? "Online" : "Offline")} (Port {OllamaCard.Port})" + + (!string.IsNullOrEmpty(value) ? $" | Model: {value}" : ""); + } + } + + partial void OnForgeModelNameChanged(string value) + { + if (ForgeCard != null) + { + ForgeCard.ActiveModel = value; + ForgeCard.StatusTooltip = $"{ForgeCard.DisplayName}: {(ForgeCard.IsOnline ? "Online" : "Offline")} (Port {ForgeCard.Port})" + + (!string.IsNullOrEmpty(value) ? $" | Model: {value}" : ""); + } + } + + partial void OnComfyModelNameChanged(string value) + { + if (ComfyUiCard != null) + { + ComfyUiCard.ActiveModel = value; + ComfyUiCard.StatusTooltip = $"{ComfyUiCard.DisplayName}: {(ComfyUiCard.IsOnline ? "Online" : "Offline")} (Port {ComfyUiCard.Port})" + + (!string.IsNullOrEmpty(value) ? $" | Model: {value}" : ""); + } + } + + partial void OnKokoroModelNameChanged(string value) + { + if (KokoroCard != null) + { + KokoroCard.ActiveModel = value; + KokoroCard.StatusTooltip = $"{KokoroCard.DisplayName}: {(KokoroCard.IsOnline ? "Online" : "Offline")} (Port {KokoroCard.Port})" + + (!string.IsNullOrEmpty(value) ? $" | Model: {value}" : ""); + } + } + + private void UpdateCardFromStatus(EngineStatusCardModel? card, string status) + { + if (card == null) return; + var isOnline = string.Equals(status, "Online", StringComparison.OrdinalIgnoreCase) || + string.Equals(status, "Running", StringComparison.OrdinalIgnoreCase); + var isStarting = string.Equals(status, "Starting", StringComparison.OrdinalIgnoreCase); + var isError = string.Equals(status, "Error", StringComparison.OrdinalIgnoreCase) || + string.Equals(status, "Failed", StringComparison.OrdinalIgnoreCase); + + card.IsOnline = isOnline; + card.IsStarting = isStarting; + card.IsError = isError; + card.StatusTooltip = $"{card.DisplayName}: {(isOnline ? "Online" : isStarting ? "Starting..." : isError ? "Error" : "Offline")} (Port {card.Port})" + + (!string.IsNullOrEmpty(card.ActiveModel) ? $" | Model: {card.ActiveModel}" : ""); + } [ObservableProperty] private string _serviceModeText = "Connecting..."; [ObservableProperty] private bool _isServiceRunning = false; @@ -51,9 +130,17 @@ public partial class TelemetryViewModel : ObservableObject [ObservableProperty] private string _manageServiceTarget = ""; [ObservableProperty] private bool _manageServiceIsStart; + public TelemetryViewModel() : this(new TelemetryService()) + { + } + public TelemetryViewModel(ITelemetryService telemetryService) { _telemetryService = telemetryService; + UpdateCardFromStatus(OllamaCard, OllamaStatus); + UpdateCardFromStatus(ComfyUiCard, ComfyStatus); + UpdateCardFromStatus(ForgeCard, ForgeStatus); + UpdateCardFromStatus(KokoroCard, KokoroStatus); } [RelayCommand] @@ -62,14 +149,24 @@ public void ToggleCollapse() IsCollapsed = !IsCollapsed; } + public Action? OnManageServiceRequested { get; set; } + [RelayCommand] public void ManageService(string serviceName) { + if (OnManageServiceRequested != null) + { + OnManageServiceRequested(serviceName); + return; + } + ManageServiceTarget = serviceName; bool isOnline = false; - if (serviceName == "Ollama") isOnline = IsOllamaOnline; - else if (serviceName == "Forge SD") isOnline = IsForgeOnline; - else if (serviceName == "ComfyUI") isOnline = IsComfyOnline; + var lower = (serviceName ?? "").ToLowerInvariant(); + if (lower.Contains("ollama")) isOnline = IsOllamaOnline; + else if (lower.Contains("forge")) isOnline = IsForgeOnline; + else if (lower.Contains("comfy")) isOnline = IsComfyOnline; + else if (lower.Contains("kokoro") || lower.Contains("audio")) isOnline = IsKokoroOnline; ManageServiceIsStart = !isOnline; string action = ManageServiceIsStart ? "start" : "stop"; @@ -87,12 +184,22 @@ public void CancelManageService() public async Task ConfirmManageServiceAsync() { IsManageServiceModalOpen = false; + var serviceName = ManageServiceTarget ?? "Service"; + var isStart = ManageServiceIsStart; + var action = isStart ? "start" : "stop"; + + var lower = serviceName.Trim().ToLowerInvariant(); + string engineName = "ollama"; + if (lower.Contains("comfy")) engineName = "comfy"; + else if (lower.Contains("forge")) engineName = "forge"; + else if (lower.Contains("audio") || lower.Contains("kokoro")) engineName = "audio"; + else if (lower.Contains("ollama")) engineName = "ollama"; + + ToastService.Instance.Show($"{(isStart ? "Starting" : "Stopping")} {serviceName}...", ToastType.Info, 2500); + try { - var actionEndpoint = ManageServiceIsStart ? "/api/comfy/start" : "/api/comfy/stop"; - var engineName = ManageServiceTarget.ToLower().Replace(" sd", ""); - if (ManageServiceTarget == "ComfyUI") engineName = "comfy"; - + var actionEndpoint = isStart ? $"/api/{engineName}/start" : $"/api/{engineName}/stop"; var req = new { engine = engineName }; var content = new StringContent( System.Text.Json.JsonSerializer.Serialize(req), @@ -100,15 +207,190 @@ public async Task ConfirmManageServiceAsync() "application/json" ); using var http = HttpHelper.CreateClient(ApiBase); - await http.PostAsync($"{ApiBase}{actionEndpoint}", content); + var response = await http.PostAsync($"{ApiBase}{actionEndpoint}", content); + if (response.IsSuccessStatusCode) + { + ToastService.Instance.Show($"{serviceName} {(isStart ? "started" : "stopped")} successfully.", ToastType.Success); + } + else + { + var errorText = await response.Content.ReadAsStringAsync(); + var msg = string.IsNullOrWhiteSpace(errorText) ? $"Server returned {response.StatusCode}" : errorText; + ToastService.Instance.Show($"Failed to {action} {serviceName}: {msg}", ToastType.Error, 6000); + } + } + catch (Exception ex) + { + ToastService.Instance.Show($"Error while attempting to {action} {serviceName}: {ex.Message}", ToastType.Error, 6000); } - catch { } finally { await RefreshStatusAsync(); } } + [RelayCommand] + public async Task ToggleEngineCardAsync(string? engineKey) + { + if (string.IsNullOrWhiteSpace(engineKey)) return; + + var key = engineKey.Trim().ToLowerInvariant(); + EngineStatusCardModel? card = key switch + { + "ollama" => OllamaCard, + "comfy" or "comfyui" => ComfyUiCard, + "forge" or "sdforge" => ForgeCard, + "kokoro" or "audio" => KokoroCard, + _ => null + }; + + if (card == null) return; + + if (OnManageServiceRequested != null) + { + OnManageServiceRequested.Invoke(card.DisplayName); + return; + } + + bool isOnline = card.IsOnline; + bool isStarting = !isOnline; + card.IsStarting = isStarting; + card.StatusTooltip = $"{card.DisplayName}: {(isStarting ? "Starting..." : "Stopping...")} (Port {card.Port})"; + + string apiEngine = key switch + { + "kokoro" => "audio", + "comfyui" => "comfy", + _ => key + }; + + var action = isStarting ? "start" : "stop"; + ToastService.Instance.Show($"{(isStarting ? "Starting" : "Stopping")} {card.DisplayName}...", ToastType.Info, 2500); + + try + { + var actionEndpoint = $"/api/{apiEngine}/{action}"; + var req = new { engine = apiEngine }; + var content = new StringContent( + System.Text.Json.JsonSerializer.Serialize(req), + System.Text.Encoding.UTF8, + "application/json" + ); + using var http = HttpHelper.CreateClient(ApiBase); + var response = await http.PostAsync($"{ApiBase}{actionEndpoint}", content); + if (response.IsSuccessStatusCode) + { + ToastService.Instance.Show($"{card.DisplayName} {(isStarting ? "started" : "stopped")} successfully.", ToastType.Success); + } + else + { + var errorText = await response.Content.ReadAsStringAsync(); + var msg = string.IsNullOrWhiteSpace(errorText) ? $"Server returned {response.StatusCode}" : errorText; + ToastService.Instance.Show($"Failed to {action} {card.DisplayName}: {msg}", ToastType.Error, 6000); + } + } + catch (Exception ex) + { + ToastService.Instance.Show($"Error while attempting to {action} {card.DisplayName}: {ex.Message}", ToastType.Error, 6000); + } + finally + { + await RefreshStatusAsync(); + } + } + + public void UpdateFromTelemetry(TelemetryData data) + { + if (data == null) return; + + if (OllamaCard != null) + { + OllamaCard.IsOnline = data.OllamaOnline; + OllamaCard.IsStarting = data.OllamaStarting; + if (!string.IsNullOrEmpty(data.OllamaModel)) + { + OllamaCard.ActiveModel = data.OllamaModel; + OllamaModelName = data.OllamaModel; + } + if (!string.IsNullOrEmpty(data.OllamaMemory)) + { + OllamaCard.MemoryUsage = data.OllamaMemory; + } + OllamaStatus = data.OllamaOnline ? "Online" : (data.OllamaStarting ? "Starting" : "Offline"); + OllamaCard.StatusTooltip = $"{OllamaCard.DisplayName}: {(OllamaCard.IsOnline ? "Online" : OllamaCard.IsStarting ? "Starting..." : "Offline")} (Port {OllamaCard.Port})" + + (!string.IsNullOrEmpty(OllamaCard.ActiveModel) ? $" | Model: {OllamaCard.ActiveModel}" : ""); + } + + if (ComfyUiCard != null) + { + ComfyUiCard.IsOnline = data.ComfyOnline; + ComfyUiCard.IsStarting = data.ComfyStarting; + if (!string.IsNullOrEmpty(data.ComfyModel)) + { + ComfyUiCard.ActiveModel = data.ComfyModel; + ComfyModelName = data.ComfyModel; + } + if (!string.IsNullOrEmpty(data.ComfyMemory)) + { + ComfyUiCard.MemoryUsage = data.ComfyMemory; + } + ComfyStatus = data.ComfyOnline ? "Online" : (data.ComfyStarting ? "Starting" : "Offline"); + ComfyUiCard.StatusTooltip = $"{ComfyUiCard.DisplayName}: {(ComfyUiCard.IsOnline ? "Online" : ComfyUiCard.IsStarting ? "Starting..." : "Offline")} (Port {ComfyUiCard.Port})" + + (!string.IsNullOrEmpty(ComfyUiCard.ActiveModel) ? $" | Model: {ComfyUiCard.ActiveModel}" : ""); + } + + if (ForgeCard != null) + { + ForgeCard.IsOnline = data.ForgeOnline; + ForgeCard.IsStarting = data.ForgeStarting; + if (!string.IsNullOrEmpty(data.ForgeModel)) + { + ForgeCard.ActiveModel = data.ForgeModel; + ForgeModelName = data.ForgeModel; + } + if (!string.IsNullOrEmpty(data.ForgeMemory)) + { + ForgeCard.MemoryUsage = data.ForgeMemory; + } + ForgeStatus = data.ForgeOnline ? "Online" : (data.ForgeStarting ? "Starting" : "Offline"); + ForgeCard.StatusTooltip = $"{ForgeCard.DisplayName}: {(ForgeCard.IsOnline ? "Online" : ForgeCard.IsStarting ? "Starting..." : "Offline")} (Port {ForgeCard.Port})" + + (!string.IsNullOrEmpty(ForgeCard.ActiveModel) ? $" | Model: {ForgeCard.ActiveModel}" : ""); + } + + if (KokoroCard != null) + { + KokoroCard.IsOnline = data.KokoroOnline; + KokoroCard.IsStarting = data.KokoroStarting; + if (!string.IsNullOrEmpty(data.KokoroModel)) + { + KokoroCard.ActiveModel = data.KokoroModel; + KokoroModelName = data.KokoroModel; + } + if (!string.IsNullOrEmpty(data.KokoroMemory)) + { + KokoroCard.MemoryUsage = data.KokoroMemory; + } + KokoroStatus = data.KokoroOnline ? "Online" : (data.KokoroStarting ? "Starting" : "Offline"); + KokoroCard.StatusTooltip = $"{KokoroCard.DisplayName}: {(KokoroCard.IsOnline ? "Online" : KokoroCard.IsStarting ? "Starting..." : "Offline")} (Port {KokoroCard.Port})" + + (!string.IsNullOrEmpty(KokoroCard.ActiveModel) ? $" | Model: {KokoroCard.ActiveModel}" : ""); + } + + if (!string.IsNullOrEmpty(data.GpuName)) + { + GpuName = data.GpuName; + } + if (data.TotalVramGb > 0) + { + VramTotalGb = data.TotalVramGb; + VramUsedGb = data.UsedVramGb; + VramPercentage = data.VramPercent > 0 ? data.VramPercent : Math.Round((VramUsedGb / VramTotalGb) * 100.0, 1); + VramStatusText = $"{VramUsedGb} GB / {VramTotalGb} GB ({VramPercentage}%)"; + } + + IsServiceRunning = (OllamaCard?.IsOnline ?? false) || (ComfyUiCard?.IsOnline ?? false) || (ForgeCard?.IsOnline ?? false) || (KokoroCard?.IsOnline ?? false); + ServiceModeText = IsServiceRunning ? "Service Connected 🟢" : "Connecting..."; + } + [RelayCommand] public async Task RefreshStatusAsync() { @@ -129,7 +411,26 @@ public async Task CheckHealthAsync(string apiBase, string comfyUrl, HttpClient h ForgeStatus = forge ? "Online" : "Offline"; ComfyStatus = comfy ? "Online" : "Offline"; - IsServiceRunning = ollama || forge || comfy; + if (OllamaCard != null) + { + OllamaCard.IsOnline = ollama; + OllamaCard.StatusTooltip = $"{OllamaCard.DisplayName}: {(ollama ? "Online" : "Offline")} (Port {OllamaCard.Port})" + + (!string.IsNullOrEmpty(OllamaCard.ActiveModel) ? $" | Model: {OllamaCard.ActiveModel}" : ""); + } + if (ForgeCard != null) + { + ForgeCard.IsOnline = forge; + ForgeCard.StatusTooltip = $"{ForgeCard.DisplayName}: {(forge ? "Online" : "Offline")} (Port {ForgeCard.Port})" + + (!string.IsNullOrEmpty(ForgeCard.ActiveModel) ? $" | Model: {ForgeCard.ActiveModel}" : ""); + } + if (ComfyUiCard != null) + { + ComfyUiCard.IsOnline = comfy; + ComfyUiCard.StatusTooltip = $"{ComfyUiCard.DisplayName}: {(comfy ? "Online" : "Offline")} (Port {ComfyUiCard.Port})" + + (!string.IsNullOrEmpty(ComfyUiCard.ActiveModel) ? $" | Model: {ComfyUiCard.ActiveModel}" : ""); + } + + IsServiceRunning = ollama || forge || comfy || (KokoroCard?.IsOnline ?? false); ServiceModeText = IsServiceRunning ? "Service Connected 🟢" : "Connecting..."; } diff --git a/LocalLLMServerManager.Shared/Views/Controls/AiAssistantTabControl.axaml.cs b/LocalLLMServerManager.Shared/Views/Controls/AiAssistantTabControl.axaml.cs index 9c51edb..1754b56 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/AiAssistantTabControl.axaml.cs +++ b/LocalLLMServerManager.Shared/Views/Controls/AiAssistantTabControl.axaml.cs @@ -82,7 +82,7 @@ public AiAssistantTabControl() protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - if (DataContext is AiAssistantViewModel vm && vm.IsDrawerOpen && vm.AvailableModelCapabilities.Count <= 1) + if (DataContext is AiAssistantViewModel vm && vm.AvailableModelCapabilities.Count <= 1) { _ = vm.LoadAvailableModelsAsync(); } diff --git a/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml index d55c9cb..4e88b0d 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml @@ -73,6 +73,17 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml index f11e629..7f3ab2d 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml @@ -8,726 +8,647 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.EngineStudioTabControl" x:DataType="vm:MainViewModel"> - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml index 61fdd6c..3288d41 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml @@ -6,10 +6,44 @@ x:DataType="vm:SettingsViewModel"> + + + + + + + + - + - - - - - - - + + + + + + + + + + - + + - + + - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + - - - + + + - - - + + + diff --git a/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml index 3f6e8e6..f766a40 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml @@ -6,9 +6,9 @@ - - - + + + @@ -16,34 +16,62 @@ - - - - - - - - - - - - - + + - - + diff --git a/LocalLLMServerManager.Shared/Views/Controls/TelemetryRibbonControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/TelemetryRibbonControl.axaml index 0f5cbd7..d18ab86 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/TelemetryRibbonControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/TelemetryRibbonControl.axaml @@ -13,8 +13,8 @@ - - + +