diff --git a/Endpoints/ModelProxyEndpoints.cs b/Endpoints/ModelProxyEndpoints.cs index 639feae..e0375e1 100644 --- a/Endpoints/ModelProxyEndpoints.cs +++ b/Endpoints/ModelProxyEndpoints.cs @@ -187,24 +187,24 @@ async Task HandleDeleteModel(HttpContext context, IHttpClientFactory cl var tag = !string.IsNullOrWhiteSpace(pipeline_tag) ? pipeline_tag : pipeline_tags; string requestUrl; + var searchParam = !string.IsNullOrWhiteSpace(query) ? $"search={Uri.EscapeDataString(query)}&" : ""; if (!string.IsNullOrWhiteSpace(tag)) { if (tag.Equals("gguf", StringComparison.OrdinalIgnoreCase)) { - var qParam = string.IsNullOrWhiteSpace(query) ? "llama" : query; - requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(qParam)}&filter=gguf&sort=downloads&direction=-1&limit=25"; + var qFallback = string.IsNullOrWhiteSpace(query) ? "search=llama&" : searchParam; + requestUrl = $"https://huggingface.co/api/models?{qFallback}filter=gguf&sort=downloads&direction=-1&limit=25"; } else { - var qParam = Uri.EscapeDataString(query); var tagParam = Uri.EscapeDataString(tag); - requestUrl = $"https://huggingface.co/api/models?search={qParam}&pipeline_tag={tagParam}&sort=downloads&direction=-1&limit=25"; + requestUrl = $"https://huggingface.co/api/models?{searchParam}pipeline_tag={tagParam}&sort=downloads&direction=-1&limit=25"; } } else { - var qParam = string.IsNullOrWhiteSpace(query) ? "llama" : query; - requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(qParam)}&filter=gguf&sort=downloads&direction=-1&limit=25"; + var qFallback = string.IsNullOrWhiteSpace(query) ? "search=llama&" : searchParam; + requestUrl = $"https://huggingface.co/api/models?{qFallback}filter=gguf&sort=downloads&direction=-1&limit=25"; } using var req = new HttpRequestMessage(HttpMethod.Get, requestUrl); diff --git a/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs b/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs index f6cf39d..df7761e 100644 --- a/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs +++ b/LocalLLMServerManager.Shared/Services/HuggingFaceSearchService.cs @@ -78,8 +78,9 @@ public async Task> SearchRepositoriesAsync(string apiB public async Task> SearchModelsAsync(string query, string? pipelineTag = null, System.Threading.CancellationToken ct = default) { using var http = new HttpClient(); - var tagParam = string.IsNullOrWhiteSpace(pipelineTag) ? "" : $"&pipeline_tag={Uri.EscapeDataString(pipelineTag)}"; - var requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(query)}{tagParam}&sort=downloads&direction=-1&limit=20"; + var searchParam = string.IsNullOrWhiteSpace(query) ? "" : $"search={Uri.EscapeDataString(query)}&"; + var tagParam = string.IsNullOrWhiteSpace(pipelineTag) ? "" : $"pipeline_tag={Uri.EscapeDataString(pipelineTag)}&"; + var requestUrl = $"https://huggingface.co/api/models?{searchParam}{tagParam}sort=downloads&direction=-1&limit=20"; var result = new List(); try { diff --git a/LocalLLMServerManager.Shared/Services/UiDiagnosticLogger.cs b/LocalLLMServerManager.Shared/Services/UiDiagnosticLogger.cs new file mode 100644 index 0000000..9c8d6e1 --- /dev/null +++ b/LocalLLMServerManager.Shared/Services/UiDiagnosticLogger.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LocalLLMServerManager.Shared.Services; + +/// +/// Represents a captured UI binding or diagnostic error entry. +/// +public record BindingDiagnosticEntry(DateTime Timestamp, string Message, string? Source = null); + +/// +/// Interface for thread-safe UI diagnostic error logging. +/// +public interface IUiDiagnosticLogger +{ + /// + /// Records a new UI binding error. + /// + void RecordBindingError(string message, string? source = null); + + /// + /// Retrieves the most recent binding diagnostic entries up to . + /// + IReadOnlyList GetRecentErrors(int maxCount = 50); + + /// + /// Clears all recorded diagnostic entries. + /// + void Clear(); + + /// + /// Exports a formatted diagnostic summary string. + /// + string ExportLogSummary(); +} + +/// +/// Thread-safe bounded diagnostic logger service for UI binding errors. +/// +public class UiDiagnosticLogger : IUiDiagnosticLogger +{ + public const int DefaultMaxEntries = 500; + + public static UiDiagnosticLogger Instance { get; } = new(); + + private readonly int _maxCapacity; + private readonly object _lock = new(); + private readonly Queue _entries; + + public int MaxCapacity => _maxCapacity; + + public int Count + { + get + { + lock (_lock) + { + return _entries.Count; + } + } + } + + public UiDiagnosticLogger(int maxCapacity = DefaultMaxEntries) + { + _maxCapacity = maxCapacity > 0 ? maxCapacity : DefaultMaxEntries; + _entries = new Queue(_maxCapacity); + } + + public void RecordBindingError(string message, string? source = null) + { + lock (_lock) + { + while (_entries.Count >= _maxCapacity) + { + _entries.Dequeue(); + } + + _entries.Enqueue(new BindingDiagnosticEntry(DateTime.UtcNow, message ?? string.Empty, source)); + } + } + + public IReadOnlyList GetRecentErrors(int maxCount = 50) + { + if (maxCount <= 0) + { + return Array.Empty(); + } + + lock (_lock) + { + if (_entries.Count == 0) + { + return Array.Empty(); + } + + int count = Math.Min(maxCount, _entries.Count); + int skip = _entries.Count - count; + return _entries.Skip(skip).Take(count).ToList().AsReadOnly(); + } + } + + public void Clear() + { + lock (_lock) + { + _entries.Clear(); + } + } + + public string ExportLogSummary() + { + lock (_lock) + { + if (_entries.Count == 0) + { + return "No binding errors recorded."; + } + + var sb = new StringBuilder(); + sb.AppendLine($"=== UI Diagnostic Summary ({_entries.Count} error(s)) ==="); + foreach (var entry in _entries) + { + var sourceInfo = string.IsNullOrWhiteSpace(entry.Source) ? string.Empty : $" [{entry.Source}]"; + sb.AppendLine($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff}]{sourceInfo}: {entry.Message}"); + } + + return sb.ToString().TrimEnd(); + } + } +} diff --git a/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs index 2c10116..90769f0 100644 --- a/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs @@ -17,11 +17,23 @@ public partial class CivitaiSearchViewModel : ObservableObject private readonly ITelemetryService? _telemetryService; [ObservableProperty] private string _civitaiSearchQuery = ""; - [ObservableProperty] private string _selectedCivitaiType = "Checkpoint"; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsTypeAllActive))] + [NotifyPropertyChangedFor(nameof(IsTypeCheckpointActive))] + [NotifyPropertyChangedFor(nameof(IsTypeLoraActive))] + private string _selectedCivitaiType = "Checkpoint"; + + public bool IsTypeAllActive => string.IsNullOrWhiteSpace(SelectedCivitaiType) || SelectedCivitaiType.Equals("All", StringComparison.OrdinalIgnoreCase); + public bool IsTypeCheckpointActive => SelectedCivitaiType.Equals("Checkpoint", StringComparison.OrdinalIgnoreCase); + public bool IsTypeLoraActive => SelectedCivitaiType.Equals("LORA", StringComparison.OrdinalIgnoreCase); + + [ObservableProperty] private string? _activeStarterChip = null; + [ObservableProperty] private bool _isLoading = false; public ObservableCollection CivitaiResults { get; } = new(); public ObservableCollection FilteredCivitaiResults { get; } = new(); public ObservableCollection StarterModels { get; } = new(); + public ObservableCollection FilteredStarterModels { get; } = new(); public static readonly List DefaultStarterModels = new() { @@ -71,6 +83,7 @@ private void InitStarterModels() var badge = _canIRunItService.EvaluateQuickFit(s.Name, s.SizeBytes > 0 ? s.SizeBytes : null, "Image", (long)TotalVramMb, (long)TotalRamMb); StarterModels.Add(s with { FitBadge = badge }); } + ApplyFilter(); } public void UpdateHardwareTelemetry(double totalVramMb, double totalRamMb) @@ -103,28 +116,39 @@ public void ApplyFilter() FilteredCivitaiResults.Clear(); foreach (var r in CivitaiResults) { - if (r.FitBadge == null) + if (MatchesVerdictFilter(r.FitBadge)) { FilteredCivitaiResults.Add(r); - continue; } + } - bool matches = r.FitBadge.FitVerdict switch - { - FitVerdict.FullVram => IsFullVramActive, - FitVerdict.PartialOffload => IsPartialOffloadActive, - FitVerdict.CpuOnly => IsCpuOnlyActive, - FitVerdict.OutOfMemory => IsOomActive, - _ => true - }; - - if (matches) + FilteredStarterModels.Clear(); + foreach (var s in StarterModels) + { + bool typeMatches = string.IsNullOrWhiteSpace(SelectedCivitaiType) || + SelectedCivitaiType.Equals("All", StringComparison.OrdinalIgnoreCase) || + s.Type.Equals(SelectedCivitaiType, StringComparison.OrdinalIgnoreCase); + + if (typeMatches && MatchesVerdictFilter(s.FitBadge)) { - FilteredCivitaiResults.Add(r); + FilteredStarterModels.Add(s); } } } + private bool MatchesVerdictFilter(QuickFitBadge? fitBadge) + { + if (fitBadge == null) return true; + return fitBadge.FitVerdict switch + { + FitVerdict.FullVram => IsFullVramActive, + FitVerdict.PartialOffload => IsPartialOffloadActive, + FitVerdict.CpuOnly => IsCpuOnlyActive, + FitVerdict.OutOfMemory => IsOomActive, + _ => true + }; + } + [RelayCommand] public void ToggleFitVerdict(string verdict) { @@ -153,9 +177,19 @@ public void ToggleFitVerdict(string verdict) partial void OnIsCpuOnlyActiveChanged(bool value) => ApplyFilter(); partial void OnIsOomActiveChanged(bool value) => ApplyFilter(); + [RelayCommand] + public void SelectCivitaiType(string? type) + { + SelectedCivitaiType = string.IsNullOrWhiteSpace(type) ? "All" : type; + ActiveStarterChip = null; + ApplyFilter(); + _ = SearchCivitaiAsync(); + } + [RelayCommand] public void ApplyStarterChip(string? chip) { + ActiveStarterChip = chip; var clean = (chip ?? "").Replace("🌟", "").Replace("📸", "").Replace("🎨", "").Replace("🎭", "").Replace("⚡", "").Replace("🔮", "").Trim(); if (clean.Contains("LoRA", StringComparison.OrdinalIgnoreCase)) { @@ -188,6 +222,19 @@ public void InspectModel(CivitaiModelItem? item) } } + public async Task LoadDefaultModelsAsync(string apiBase, HttpClient http) + { + if (CivitaiResults.Count > 0) return; + try + { + await SearchCivitaiAsync(apiBase, http); + } + catch + { + // Starter models are retained + } + } + [RelayCommand] public async Task SearchCivitaiAsync() { @@ -199,7 +246,8 @@ public async Task SearchCivitaiAsync(string apiBase, HttpClient http) IsLoading = true; try { - var results = await _civitaiSearchService.SearchModelsAsync(apiBase, CivitaiSearchQuery, SelectedCivitaiType, "Most Downloaded", http); + string typeParam = SelectedCivitaiType.Equals("All", StringComparison.OrdinalIgnoreCase) ? "" : SelectedCivitaiType; + var results = await _civitaiSearchService.SearchModelsAsync(apiBase, CivitaiSearchQuery, typeParam, "Most Downloaded", http); CivitaiResults.Clear(); foreach (var r in results) { diff --git a/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs index e15a51d..01a35a8 100644 --- a/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs @@ -11,7 +11,8 @@ public record DocStep( int StepNumber, string Title, string Action, - string ExpectedResult + string ExpectedResult, + int? TargetTab = null ); public record DocSection( @@ -32,12 +33,103 @@ public partial class DocumentationViewModel : ObservableObject [ObservableProperty] private DocSection? _selectedSection; + [ObservableProperty] + private bool _isFloatingOverlayOpen = false; + + [ObservableProperty] + private bool _isMinimizedToPill = false; + + [ObservableProperty] + private double _overlayX = 30; + + [ObservableProperty] + private double _overlayY = 70; + + [ObservableProperty] + private int _currentStepIndex = 0; + + public DocStep? CurrentStep => SelectedSection?.Steps != null && CurrentStepIndex >= 0 && CurrentStepIndex < SelectedSection.Steps.Count + ? SelectedSection.Steps[CurrentStepIndex] + : null; + + public Action? OnNavigateToTabRequested { get; set; } + public Action? OnPopOutNativeWindowRequested { get; set; } + + [RelayCommand] + public void PopOutNativeWindow() + { + if (OnPopOutNativeWindowRequested != null) + { + OnPopOutNativeWindowRequested.Invoke(); + } + else + { + ToggleFloatingOverlay(); + } + } + public DocumentationViewModel() { InitializeSections(); SelectedSection = Sections.FirstOrDefault(); } + partial void OnSelectedSectionChanged(DocSection? value) + { + CurrentStepIndex = 0; + OnPropertyChanged(nameof(CurrentStep)); + } + + partial void OnCurrentStepIndexChanged(int value) + { + OnPropertyChanged(nameof(CurrentStep)); + } + + [RelayCommand] + public void ToggleFloatingOverlay() + { + IsFloatingOverlayOpen = !IsFloatingOverlayOpen; + if (IsFloatingOverlayOpen) + { + CurrentStepIndex = 0; + IsMinimizedToPill = false; + } + } + + [RelayCommand] + public void ToggleMinimizeToPill() + { + IsMinimizedToPill = !IsMinimizedToPill; + } + + [RelayCommand] + public void NextStep() + { + if (SelectedSection != null && CurrentStepIndex < SelectedSection.Steps.Count - 1) + { + CurrentStepIndex++; + } + } + + [RelayCommand] + public void PreviousStep() + { + if (CurrentStepIndex > 0) + { + CurrentStepIndex--; + } + } + + [RelayCommand] + public void JumpToStepTab(int? targetTab) + { + int tab = targetTab ?? CurrentStep?.TargetTab ?? -1; + if (tab >= 0) + { + OnNavigateToTabRequested?.Invoke(tab); + } + } + [RelayCommand] public void SelectSection(string sectionId) { @@ -61,12 +153,12 @@ private void InitializeSections() Prerequisite: "Start the Forge or ComfyUI engine in the Workflows tab.", Steps: new List { - new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens."), - new(2, "Select Image Studio", "Click the '🎨 Image' button in the studio selector.", "The Image generation controls display."), - new(3, "Choose Engine", "Select 'Stable Diffusion / Forge' or 'ComfyUI'.", "The engine status shows Online."), - new(4, "Select Style Preset", "Click a preset chip such as 'Standard Square' or 'Landscape Wallpaper'.", "Resolution and sampling parameters load automatically."), - new(5, "Enter Text Prompt", "Type your descriptive prompt into the Prompt box. Add unwanted features into the Negative Prompt box.", "Prompt text is ready for generation."), - new(6, "Click Generate Image", "Click the '🎨 Generate Image' button.", "The stage tracker shows generation progress and the final image appears in the gallery.") + new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens.", TargetTab: 1), + new(2, "Select Image Studio", "Click the '🎨 Image' button in the studio selector.", "The Image generation controls display.", TargetTab: 1), + new(3, "Choose Engine", "Select 'Stable Diffusion / Forge' or 'ComfyUI'.", "The engine status shows Online.", TargetTab: 1), + new(4, "Select Style Preset", "Click a preset chip such as 'Standard Square' or 'Landscape Wallpaper'.", "Resolution and sampling parameters load automatically.", TargetTab: 1), + new(5, "Enter Text Prompt", "Type your descriptive prompt into the Prompt box. Add unwanted features into the Negative Prompt box.", "Prompt text is ready for generation.", TargetTab: 1), + new(6, "Click Generate Image", "Click the '🎨 Generate Image' button.", "The stage tracker shows generation progress and the final image appears in the gallery.", TargetTab: 1) }, Notes: new List { @@ -88,12 +180,12 @@ private void InitializeSections() Prerequisite: "Verify that Ollama status shows Online in the top telemetry header.", Steps: new List { - new(1, "Open Models Tab", "Click 'Models' in the top navigation bar.", "The Models management view opens."), - new(2, "Download or Select Model", "Select 'Downloaded & Manage'. Choose a model from your installed list, or pull a new model from the library.", "The model is loaded and ready for inference."), - new(3, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar, then select '💬 Text'.", "The Text LLM generation studio displays."), - new(4, "Configure Context Window", "Adjust target context tokens if necessary. Review the estimated KV cache memory text.", "Target memory stays within available GPU VRAM limits."), - new(5, "Enter Prompt", "Type your question or instructions into the text area.", "The prompt is ready to submit."), - new(6, "Click Generate Text", "Click the '💬 Generate Text' button.", "The model generates the response in real time.") + new(1, "Open Models Tab", "Click 'Models' in the top navigation bar.", "The Models management view opens.", TargetTab: 0), + new(2, "Download or Select Model", "Select 'Downloaded & Manage'. Choose a model from your installed list, or pull a new model from the library.", "The model is loaded and ready for inference.", TargetTab: 0), + new(3, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar, then select '💬 Text'.", "The Text LLM generation studio displays.", TargetTab: 1), + new(4, "Configure Context Window", "Adjust target context tokens if necessary. Review the estimated KV cache memory text.", "Target memory stays within available GPU VRAM limits.", TargetTab: 1), + new(5, "Enter Prompt", "Type your question or instructions into the text area.", "The prompt is ready to submit.", TargetTab: 1), + new(6, "Click Generate Text", "Click the '💬 Generate Text' button.", "The model generates the response in real time.", TargetTab: 1) }, Notes: new List { @@ -110,12 +202,12 @@ private void InitializeSections() Prerequisite: "Verify that at least 8 GB of VRAM is available and ComfyUI is running.", Steps: new List { - new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens."), - new(2, "Select Video Studio", "Click the '🎬 Video' button in the studio selector.", "The Video generation studio displays."), - new(3, "Select Video Preset", "Click a preset chip (for example, 'Quick 480p Preview' or 'Cinematic HD 720p').", "Workflow model, resolution, and frame counts populate automatically."), - new(4, "Enter Prompt", "Type your scene description into the Prompt box.", "The scene description is validated."), - new(5, "Click Generate Video", "Click the '🎬 Generate Video' button.", "The 4-stage pipeline tracker shows live progress, VRAM load, and denoising steps."), - new(6, "Preview and Export", "When generation finishes, the clip loads in the interactive player. Click 'Export' to save the MP4 file.", "The MP4 video file is saved to your disk.") + new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens.", TargetTab: 1), + new(2, "Select Video Studio", "Click the '🎬 Video' button in the studio selector.", "The Video generation studio displays.", TargetTab: 1), + new(3, "Select Video Preset", "Click a preset chip (for example, 'Quick 480p Preview' or 'Cinematic HD 720p').", "Workflow model, resolution, and frame counts populate automatically.", TargetTab: 1), + new(4, "Enter Prompt", "Type your scene description into the Prompt box.", "The scene description is validated.", TargetTab: 1), + new(5, "Click Generate Video", "Click the '🎬 Generate Video' button.", "The 4-stage pipeline tracker shows live progress, VRAM load, and denoising steps.", TargetTab: 1), + new(6, "Preview and Export", "When generation finishes, the clip loads in the interactive player. Click 'Export' to save the MP4 file.", "The MP4 video file is saved to your disk.", TargetTab: 1) }, Notes: new List { @@ -136,12 +228,12 @@ private void InitializeSections() Prerequisite: "Ensure the Audio Feature Pack is installed in the Settings tab.", Steps: new List { - new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens."), - new(2, "Select Audio Studio", "Click the '🎵 Audio' button in the studio selector.", "The Audio generation studio displays."), - new(3, "Choose Workflow", "Select 'Kokoro TTS' for speech narration or 'Stable Audio Open' for sound effects.", "Controls update with duration, seed, and voice parameters."), - new(4, "Enter Audio Prompt", "Type the speech text or sound effect description into the prompt box.", "The input text is ready for synthesis."), - new(5, "Queue Audio Generation", "Click 'Queue Audio Generation Workflow'.", "The audio stage tracker monitors generation and the waveform visualizer activates."), - new(6, "Listen and Play", "Click the Play button in the player bar to listen to the synthesized audio.", "Audio plays through your default speaker output.") + new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens.", TargetTab: 1), + new(2, "Select Audio Studio", "Click the '🎵 Audio' button in the studio selector.", "The Audio generation studio displays.", TargetTab: 1), + new(3, "Choose Workflow", "Select 'Kokoro TTS' for speech narration or 'Stable Audio Open' for sound effects.", "Controls update with duration, seed, and voice parameters.", TargetTab: 1), + new(4, "Enter Audio Prompt", "Type the speech text or sound effect description into the prompt box.", "The input text is ready for synthesis.", TargetTab: 1), + new(5, "Queue Audio Generation", "Click 'Queue Audio Generation Workflow'.", "The audio stage tracker monitors generation and the waveform visualizer activates.", TargetTab: 1), + new(6, "Listen and Play", "Click the Play button in the player bar to listen to the synthesized audio.", "Audio plays through your default speaker output.", TargetTab: 1) } )); @@ -154,12 +246,12 @@ private void InitializeSections() Prerequisite: "Ensure ComfyUI 3D pack is enabled with at least 12 GB VRAM free.", Steps: new List { - new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens."), - new(2, "Select 3D Mesh Studio", "Click the '📦 3D' button in the studio selector.", "The 3D studio controls display."), - new(3, "Select Mode", "Choose 'Text-to-3D' or 'Image-to-3D'.", "Input parameters load for the selected mode."), - new(4, "Enter Prompt or Image", "Type the object description or supply an input image.", "Input data is staged for 3D reconstruction."), - new(5, "Click Generate 3D Mesh", "Click the '📦 Generate 3D Mesh' button.", "The system runs sparse-structure and texture generation passes."), - new(6, "Preview and Download", "Inspect the rendered model and click 'Download GLB'.", "The 3D GLB file is ready for import into Blender or 3D engines.") + new(1, "Open Workflows Tab", "Click 'Workflows' in the top navigation bar.", "The Workflows workspace opens.", TargetTab: 1), + new(2, "Select 3D Mesh Studio", "Click the '📦 3D' button in the studio selector.", "The 3D studio controls display.", TargetTab: 1), + new(3, "Select Mode", "Choose 'Text-to-3D' or 'Image-to-3D'.", "Input parameters load for the selected mode.", TargetTab: 1), + new(4, "Enter Prompt or Image", "Type the object description or supply an input image.", "Input data is staged for 3D reconstruction.", TargetTab: 1), + new(5, "Click Generate 3D Mesh", "Click the '📦 Generate 3D Mesh' button.", "The system runs sparse-structure and texture generation passes.", TargetTab: 1), + new(6, "Preview and Download", "Inspect the rendered model and click 'Download GLB'.", "The 3D GLB file is ready for import into Blender or 3D engines.", TargetTab: 1) } )); @@ -172,11 +264,11 @@ private void InitializeSections() Prerequisite: "Ensure an active internet connection is available.", Steps: new List { - new(1, "Open Models Tab", "Click 'Models' in the top navigation bar.", "The Models hub opens."), - new(2, "Select Hub", "Select '🤗 Hugging Face Hub' for GGUF/multimodal models, or '🎨 CivitAI Hub' for SD checkpoints and LoRAs.", "The corresponding search view displays."), - new(3, "Search or Click Suggestion", "Type keywords into the search box or click a suggestion chip (such as 'SDXL' or 'Photorealistic').", "The loading indicator displays while searching."), - new(4, "Verify Hardware Fit", "Inspect the green, yellow, or red compatibility badge on the model card.", "The badge confirms whether the model fits into your GPU VRAM."), - new(5, "Click Download", "Click 'Download' on the desired model card.", "A download toast notification confirms the operation and progress begins.") + new(1, "Open Models Tab", "Click 'Models' in the top navigation bar.", "The Models hub opens.", TargetTab: 0), + new(2, "Select Hub", "Select '🤗 Hugging Face Hub' for GGUF/multimodal models, or '🎨 CivitAI Hub' for SD checkpoints and LoRAs.", "The corresponding search view displays.", TargetTab: 0), + new(3, "Search or Click Suggestion", "Type keywords into the search box or click a suggestion chip (such as 'SDXL' or 'Photorealistic').", "The loading indicator displays while searching.", TargetTab: 0), + new(4, "Verify Hardware Fit", "Inspect the green, yellow, or red compatibility badge on the model card.", "The badge confirms whether the model fits into your GPU VRAM.", TargetTab: 0), + new(5, "Click Download", "Click 'Download' on the desired model card.", "A download toast notification confirms the operation and progress begins.", TargetTab: 0) } )); @@ -189,10 +281,10 @@ private void InitializeSections() Prerequisite: "Check that GPU memory telemetry is visible in the top header.", Steps: new List { - new(1, "Open Can I Run It Tab", "Click '⚡ Can I Run It' in the top navigation bar.", "The hardware fit calculator view opens."), - new(2, "Select Modality", "Choose LLM, Image, Video, Audio, or 3D.", "Parameters for the chosen modality appear."), - new(3, "Enter Model Size", "Enter the model name, size in GB, or select a known preset.", "The system computes VRAM and system RAM requirements."), - new(4, "Read Verdict Banner", "Review the verdict: Full VRAM (Fastest), Partial Offload (Usable), CPU Only (Slow), or Out of Memory (Incompatible).", "You know if the model will run smoothly without system crashes.") + new(1, "Open Can I Run It Tab", "Click '⚡ Can I Run It' in the top navigation bar.", "The hardware fit calculator view opens.", TargetTab: 2), + new(2, "Select Modality", "Choose LLM, Image, Video, Audio, or 3D.", "Parameters for the chosen modality appear.", TargetTab: 2), + new(3, "Enter Model Size", "Enter the model name, size in GB, or select a known preset.", "The system computes VRAM and system RAM requirements.", TargetTab: 2), + new(4, "Read Verdict Banner", "Review the verdict: Full VRAM (Fastest), Partial Offload (Usable), CPU Only (Slow), or Out of Memory (Incompatible).", "You know if the model will run smoothly without system crashes.", TargetTab: 2) } )); @@ -205,10 +297,10 @@ private void InitializeSections() Prerequisite: "Check the service status pills in the top header.", Steps: new List { - new(1, "Check Service Badges", "Inspect the Ollama, Forge, and ComfyUI status pills in the header.", "Green indicates Online. Red indicates Offline. Yellow indicates Busy."), - new(2, "Start an Offline Engine", "If an engine shows Offline, open the Workflows tab and click 'Toggle Engine' or click 'Refresh' in the header.", "The engine starts and the pill turns green."), - new(3, "Clear GPU Memory", "If a model fails with Out Of Memory, click 'Refresh' or unload unused models in the Models tab.", "GPU VRAM is freed for the next generation."), - new(4, "Verify Tool Discovery", "Open Settings and review the Tool Discovery section if an engine fails to launch.", "Paths to Python, Git, and engines are validated.") + new(1, "Check Service Badges", "Inspect the Ollama, Forge, and ComfyUI status pills in the header.", "Green indicates Online. Red indicates Offline. Yellow indicates Busy.", TargetTab: 5), + new(2, "Start an Offline Engine", "If an engine shows Offline, open the Workflows tab and click 'Toggle Engine' or click 'Refresh' in the header.", "The engine starts and the pill turns green.", TargetTab: 1), + new(3, "Clear GPU Memory", "If a model fails with Out Of Memory, click 'Refresh' or unload unused models in the Models tab.", "GPU VRAM is freed for the next generation.", TargetTab: 0), + new(4, "Verify Tool Discovery", "Open Settings and review the Tool Discovery section if an engine fails to launch.", "Paths to Python, Git, and engines are validated.", TargetTab: 5) } )); } diff --git a/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs index 3386cd1..1deda8b 100644 --- a/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs @@ -35,6 +35,25 @@ public partial class HuggingFaceSearchViewModel : ObservableObject public ObservableCollection SelectedInputModalities { get; } = new() { "Text" }; public ObservableCollection SelectedOutputModalities { get; } = new() { "Text" }; + [ObservableProperty] private string? _activePreset = "LLM"; + + public bool IsPresetMultimodal => string.Equals(ActivePreset, "Multimodal", StringComparison.OrdinalIgnoreCase); + public bool IsPresetLlm => string.Equals(ActivePreset, "LLM", StringComparison.OrdinalIgnoreCase); + public bool IsPresetImage => string.Equals(ActivePreset, "Image", StringComparison.OrdinalIgnoreCase); + public bool IsPresetVideo => string.Equals(ActivePreset, "Video", StringComparison.OrdinalIgnoreCase); + public bool IsPresetAudio => string.Equals(ActivePreset, "Audio", StringComparison.OrdinalIgnoreCase); + public bool IsPreset3D => string.Equals(ActivePreset, "3D", StringComparison.OrdinalIgnoreCase); + + partial void OnActivePresetChanged(string? value) + { + OnPropertyChanged(nameof(IsPresetMultimodal)); + OnPropertyChanged(nameof(IsPresetLlm)); + OnPropertyChanged(nameof(IsPresetImage)); + OnPropertyChanged(nameof(IsPresetVideo)); + OnPropertyChanged(nameof(IsPresetAudio)); + OnPropertyChanged(nameof(IsPreset3D)); + } + [ObservableProperty] private bool _isInputTextActive = true; [ObservableProperty] private bool _isInputImageActive = false; [ObservableProperty] private bool _isInputAudioActive = false; @@ -71,6 +90,7 @@ public HuggingFaceSearchViewModel( _canIRunItService = canIRunItService ?? new CanIRunItService(); _telemetryService = telemetryService; HuggingFaceResults.CollectionChanged += (s, e) => ApplyFilter(); + LoadCuratedStarterModels(); } public void UpdateHardwareTelemetry(double totalVramMb, double totalRamMb) @@ -169,6 +189,8 @@ public void ToggleInputModality(string modality) else if (m.Equals("Video", StringComparison.OrdinalIgnoreCase)) IsInputVideoActive = !IsInputVideoActive; + ActivePreset = null; + SelectedPipelineTag = null; SyncInputModalitiesList(); _ = SearchHuggingFaceAsync(); } @@ -188,6 +210,8 @@ public void ToggleOutputModality(string modality) else if (m.Equals("3D", StringComparison.OrdinalIgnoreCase) || m.Equals("ThreeD", StringComparison.OrdinalIgnoreCase)) IsOutputThreeDActive = !IsOutputThreeDActive; + ActivePreset = null; + SelectedPipelineTag = null; SyncOutputModalitiesList(); _ = SearchHuggingFaceAsync(); } @@ -214,9 +238,11 @@ public void SyncOutputModalitiesList() [RelayCommand] public void ApplyPreset(string preset) { + SelectedPipelineTag = null; var p = (preset ?? "").Trim().ToLowerInvariant(); if (p.Contains("multimodal") || p.Contains("vlm") || p.Contains("vision")) { + ActivePreset = "Multimodal"; IsInputTextActive = true; IsInputImageActive = true; IsInputAudioActive = false; @@ -230,6 +256,7 @@ public void ApplyPreset(string preset) } else if (p.Contains("llm") || p.Contains("text")) { + ActivePreset = "LLM"; IsInputTextActive = true; IsInputImageActive = false; IsInputAudioActive = false; @@ -243,6 +270,7 @@ public void ApplyPreset(string preset) } else if (p.Contains("image") || p.Contains("diffusion")) { + ActivePreset = "Image"; IsInputTextActive = true; IsInputImageActive = false; IsInputAudioActive = false; @@ -256,6 +284,7 @@ public void ApplyPreset(string preset) } else if (p.Contains("video")) { + ActivePreset = "Video"; IsInputTextActive = true; IsInputImageActive = true; IsInputAudioActive = false; @@ -269,6 +298,7 @@ public void ApplyPreset(string preset) } else if (p.Contains("audio") || p.Contains("speech") || p.Contains("tts")) { + ActivePreset = "Audio"; IsInputTextActive = true; IsInputImageActive = false; IsInputAudioActive = true; @@ -282,6 +312,7 @@ public void ApplyPreset(string preset) } else if (p.Contains("3d")) { + ActivePreset = "3D"; IsInputTextActive = true; IsInputImageActive = true; IsInputAudioActive = false; @@ -316,7 +347,11 @@ public static List ResolvePipelineTags(IEnumerable inputs, IEnum { tags.Add("image-to-text"); } - else if (inSet.Contains("Text") && outSet.Contains("Text")) + else if (inSet.Contains("Video") && outSet.Contains("Text")) + { + tags.Add("video-text-to-text"); + } + else if (inSet.Contains("Text") && outSet.Contains("Text") && !inSet.Contains("Audio") && !outSet.Contains("Audio") && !outSet.Contains("Image") && !outSet.Contains("Video") && !outSet.Contains("3D")) { tags.Add("text-generation"); } @@ -334,6 +369,7 @@ public static List ResolvePipelineTags(IEnumerable inputs, IEnum if (outSet.Contains("Video")) { if (inSet.Contains("Image")) tags.Add("image-to-video"); + if (inSet.Contains("Video")) tags.Add("video-to-video"); tags.Add("text-to-video"); } @@ -341,6 +377,7 @@ public static List ResolvePipelineTags(IEnumerable inputs, IEnum { tags.Add("text-to-speech"); tags.Add("text-to-audio"); + if (inSet.Contains("Audio")) tags.Add("audio-to-audio"); } if (inSet.Contains("Audio") && outSet.Contains("Text")) @@ -362,7 +399,11 @@ public static string DetermineModality(string modelName, string? pipelineTag) string tag = (pipelineTag ?? "").ToLowerInvariant(); string name = (modelName ?? "").ToLowerInvariant(); - if (tag.Contains("video") || name.Contains("wan") || name.Contains("ltx") || name.Contains("hunyuanvideo")) + // Check multimodal VLM first so image-to-text / image-text-to-text isn't misclassified as image diffusion + if (tag.Contains("image-text-to-text") || tag.Contains("image-to-text") || tag.Contains("visual-question-answering") || tag.Contains("vlm") || name.Contains("vl-") || name.Contains("-vl")) + return "LLM"; + + if (tag.Contains("video") || name.Contains("wan") || name.Contains("ltx") || name.Contains("hunyuanvideo") || name.Contains("cogvideox")) return "Video"; if (tag.Contains("audio") || tag.Contains("speech") || tag.Contains("tts") || name.Contains("kokoro") || name.Contains("whisper") || name.Contains("xtts")) return "Audio"; @@ -374,6 +415,55 @@ public static string DetermineModality(string modelName, string? pipelineTag) return "LLM"; } + public static List GetCuratedStarterModels() + { + return new List + { + new HuggingFaceRepoItem("Qwen/Qwen2.5-Coder-7B-Instruct-GGUF", "Qwen", 4820, "1,250,000 downloads", "text-generation"), + new HuggingFaceRepoItem("meta-llama/Llama-3.2-3B-Instruct-GGUF", "meta-llama", 3100, "980,000 downloads", "text-generation"), + new HuggingFaceRepoItem("deepseek-ai/DeepSeek-R1-Distill-Qwen-7B-GGUF", "deepseek-ai", 5200, "1,450,000 downloads", "text-generation"), + new HuggingFaceRepoItem("Wan-AI/Wan2.1-T2V-14B", "Wan-AI", 2800, "420,000 downloads", "text-to-video"), + new HuggingFaceRepoItem("hexgrad/Kokoro-82M", "hexgrad", 3900, "680,000 downloads", "text-to-audio"), + new HuggingFaceRepoItem("black-forest-labs/FLUX.1-schnell", "black-forest-labs", 6200, "1,850,000 downloads", "text-to-image") + }; + } + + public void LoadCuratedStarterModels() + { + if (HuggingFaceResults.Count > 0) return; + + foreach (var r in GetCuratedStarterModels()) + { + string modality = DetermineModality(r.Id, r.PipelineTag); + var badge = _canIRunItService.EvaluateQuickFit(r.Id, null, modality, (long)TotalVramMb, (long)TotalRamMb); + HuggingFaceResults.Add(r with { FitBadge = badge }); + } + ApplyFilter(); + } + + public async Task LoadDefaultModelsAsync(string apiBase, HttpClient http) + { + try + { + var results = await _hfSearchService.SearchRepositoriesAsync(apiBase, "", "text-generation", http); + if (results != null && results.Count > 0) + { + HuggingFaceResults.Clear(); + foreach (var r in results) + { + string modality = DetermineModality(r.Id, r.PipelineTag); + var badge = _canIRunItService.EvaluateQuickFit(r.Id, null, modality, (long)TotalVramMb, (long)TotalRamMb); + HuggingFaceResults.Add(r with { FitBadge = badge }); + } + ApplyFilter(); + } + } + catch + { + // Curated starter models are already loaded, retain them + } + } + [RelayCommand] public void NavigateToCanIRunIt(string? modelName) { @@ -446,10 +536,20 @@ public async Task SearchHuggingFaceAsync(string apiBase, HttpClient http) var badge = _canIRunItService.EvaluateQuickFit(r.Id, null, modality, (long)TotalVramMb, (long)TotalRamMb); HuggingFaceResults.Add(r with { FitBadge = badge }); } + + if (results.Count == 0 && string.IsNullOrWhiteSpace(HfSearchQuery)) + { + LoadCuratedStarterModels(); + } + ApplyFilter(); } catch { + if (HuggingFaceResults.Count == 0 && string.IsNullOrWhiteSpace(HfSearchQuery)) + { + LoadCuratedStarterModels(); + } ToastService.Instance.Show("Failed to query Hugging Face Hub.", ToastType.Error); } finally diff --git a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs index 90dc1db..c40377b 100644 --- a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs @@ -159,6 +159,7 @@ public MainViewModel( Settings = new SettingsViewModel(PresetService) { ApiBase = ApiBase }; Audio = new AudioStudioViewModel(PresetService, _canIRunItService) { ApiBase = ApiBase }; Assistant = new AiAssistantViewModel(assistantService, settingsService, promptService, httpClient ?? Http); + Documentation.OnNavigateToTabRequested = tab => SelectedTabIndex = tab; LoadStudioPresets(); RecalculateVideoHardwareFit(); @@ -166,6 +167,8 @@ public MainViewModel( _ = RefreshStatusAsync(); _ = Ollama.LoadInstalledModelsAsync(ApiBase, Http); + _ = HuggingFace.LoadDefaultModelsAsync(ApiBase, Http); + _ = Civitai.LoadDefaultModelsAsync(ApiBase, Http); _ = Audio.LoadAudioWorkflowsAsync(ApiBase, Http); _ = Audio.LoadAudioFilesAsync(ApiBase, Http); _ = LoadSettingsAsync(); diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs index eaf9d67..7161d8a 100644 --- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs @@ -23,7 +23,18 @@ public partial class SettingsViewModel : ObservableObject public ObservableCollection AllPresets { get; } = new(); public ObservableCollection FilteredPresets { get; } = new(); - [ObservableProperty] private string _selectedPresetModalityFilter = "All"; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsFilterAllActive))] + [NotifyPropertyChangedFor(nameof(IsFilterVideoActive))] + [NotifyPropertyChangedFor(nameof(IsFilterImageActive))] + [NotifyPropertyChangedFor(nameof(IsFilterAudioActive))] + private string _selectedPresetModalityFilter = "All"; + + public bool IsFilterAllActive => string.IsNullOrWhiteSpace(SelectedPresetModalityFilter) || SelectedPresetModalityFilter.Equals("All", StringComparison.OrdinalIgnoreCase); + public bool IsFilterVideoActive => SelectedPresetModalityFilter.Equals("Video", StringComparison.OrdinalIgnoreCase); + public bool IsFilterImageActive => SelectedPresetModalityFilter.Equals("Image", StringComparison.OrdinalIgnoreCase); + public bool IsFilterAudioActive => SelectedPresetModalityFilter.Equals("Audio", StringComparison.OrdinalIgnoreCase); + [ObservableProperty] private string _presetsJson = ""; [ObservableProperty] private string _forgeModelsPath = ""; diff --git a/LocalLLMServerManager.Shared/Views/Controls/CanIRunItView.axaml b/LocalLLMServerManager.Shared/Views/Controls/CanIRunItView.axaml index f968a1a..9a044f3 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/CanIRunItView.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/CanIRunItView.axaml @@ -7,6 +7,33 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.CanIRunItView" x:DataType="vm:CanIRunItViewModel"> + + + + + + + @@ -56,16 +83,36 @@ - + + + + diff --git a/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml index d930d6b..8f6c47b 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml @@ -4,7 +4,73 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.CivitaiTabControl" x:DataType="vm:CivitaiSearchViewModel"> - + + + + + + + + + + + + + + + + + + + @@ -28,39 +94,84 @@ - - - - - + + + + + + + + + - - - + + + + + + + + - + - + @@ -73,7 +184,7 @@ - + diff --git a/LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml b/LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml new file mode 100644 index 0000000..465b83c --- /dev/null +++ b/LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml.cs b/LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml.cs new file mode 100644 index 0000000..ee8d6c1 --- /dev/null +++ b/LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml.cs @@ -0,0 +1,76 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using LocalLLMServerManager.Shared.ViewModels; + +namespace LocalLLMServerManager.Shared.Views.Controls; + +public partial class DocumentationFloatingOverlay : UserControl +{ + private Point _dragStartPoint; + private bool _isDragging = false; + + public DocumentationFloatingOverlay() + { + InitializeComponent(); + + var pillHandle = this.FindControl("PillDragHandle"); + if (pillHandle != null) + { + pillHandle.PointerPressed += OnDragHandlePointerPressed; + pillHandle.PointerMoved += OnDragHandlePointerMoved; + pillHandle.PointerReleased += OnDragHandlePointerReleased; + } + + var cardHandle = this.FindControl("CardDragHandle"); + if (cardHandle != null) + { + cardHandle.PointerPressed += OnDragHandlePointerPressed; + cardHandle.PointerMoved += OnDragHandlePointerMoved; + cardHandle.PointerReleased += OnDragHandlePointerReleased; + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnDragHandlePointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + _isDragging = true; + _dragStartPoint = e.GetPosition(this.Parent as Visual); + e.Pointer.Capture(sender as IInputElement); + e.Handled = true; + } + } + + private void OnDragHandlePointerMoved(object? sender, PointerEventArgs e) + { + if (_isDragging && DataContext is DocumentationViewModel vm && this.Parent is Visual parentVisual) + { + var currentPos = e.GetPosition(parentVisual); + var deltaX = currentPos.X - _dragStartPoint.X; + var deltaY = currentPos.Y - _dragStartPoint.Y; + + vm.OverlayX = Math.Max(10, vm.OverlayX + deltaX); + vm.OverlayY = Math.Max(10, vm.OverlayY + deltaY); + + _dragStartPoint = currentPos; + e.Handled = true; + } + } + + private void OnDragHandlePointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (_isDragging) + { + _isDragging = false; + e.Pointer.Capture(null); + e.Handled = true; + } + } +} diff --git a/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml index 090aa1b..8ef22c6 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml @@ -7,13 +7,26 @@ - - + + - + + + @@ -52,9 +65,20 @@ - - - + + + + + + diff --git a/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml index bf751b6..9080f0c 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml @@ -4,6 +4,108 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.HuggingFaceTabControl" x:DataType="vm:HuggingFaceSearchViewModel"> + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -32,52 +134,143 @@ - + + + + + - + - - + + + - - + + + + - - - - diff --git a/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml index 0554f9a..ecb211c 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml @@ -4,6 +4,30 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.OllamaModelsTabControl" x:DataType="vm:OllamaLibraryViewModel"> + + + + + + + + @@ -20,17 +44,33 @@ - - - - diff --git a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml index a14c9fc..7846d83 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml @@ -5,6 +5,33 @@ x:Class="LocalLLMServerManager.Shared.Views.Controls.SettingsTabControl" x:DataType="vm:SettingsViewModel"> + + + + + + + @@ -116,10 +143,30 @@ - + + + diff --git a/LocalLLMServerManager.Shared/Views/MainView.axaml b/LocalLLMServerManager.Shared/Views/MainView.axaml index 86cd513..8cb7101 100644 --- a/LocalLLMServerManager.Shared/Views/MainView.axaml +++ b/LocalLLMServerManager.Shared/Views/MainView.axaml @@ -58,6 +58,14 @@ + + + + + diff --git a/LocalLLMServerManager.Tests/CivitaiSearchViewModelTests.cs b/LocalLLMServerManager.Tests/CivitaiSearchViewModelTests.cs index 9a47082..a62315f 100644 --- a/LocalLLMServerManager.Tests/CivitaiSearchViewModelTests.cs +++ b/LocalLLMServerManager.Tests/CivitaiSearchViewModelTests.cs @@ -175,4 +175,58 @@ public async Task SearchCivitaiAsync_TogglesIsLoadingProperly() Assert.False(vm.IsLoading); Assert.Single(vm.CivitaiResults); } + + [Fact] + public void ApplyFilter_FiltersStarterModels_ByVerdictAndType() + { + var mockCivitai = new Mock(); + var vm = new CivitaiSearchViewModel(mockCivitai.Object); + + // Initial state: default starter models loaded (DefaultStarterModels has Checkpoints and LORA) + Assert.NotEmpty(vm.FilteredStarterModels); + // Default selected type is Checkpoint + Assert.All(vm.FilteredStarterModels, s => Assert.Equal("Checkpoint", s.Type)); + + // When switching to LORA type + vm.SelectCivitaiType("LORA"); + Assert.NotEmpty(vm.FilteredStarterModels); + Assert.All(vm.FilteredStarterModels, s => Assert.Equal("LORA", s.Type)); + + // When switching to All + vm.SelectCivitaiType("All"); + Assert.Equal(vm.StarterModels.Count, vm.FilteredStarterModels.Count); + + // When toggling off all verdicts + vm.IsFullVramActive = false; + vm.IsPartialOffloadActive = false; + vm.IsCpuOnlyActive = false; + vm.IsOomActive = false; + Assert.Empty(vm.FilteredStarterModels); + } + + [Fact] + public void SelectCivitaiType_UpdatesActiveBooleansAndFilter() + { + var mockCivitai = new Mock(); + var vm = new CivitaiSearchViewModel(mockCivitai.Object); + + vm.SelectCivitaiType("LORA"); + Assert.True(vm.IsTypeLoraActive); + Assert.False(vm.IsTypeCheckpointActive); + Assert.False(vm.IsTypeAllActive); + + vm.SelectCivitaiType("Checkpoint"); + Assert.False(vm.IsTypeLoraActive); + Assert.True(vm.IsTypeCheckpointActive); + Assert.False(vm.IsTypeAllActive); + + vm.SelectCivitaiType("All"); + Assert.False(vm.IsTypeLoraActive); + Assert.False(vm.IsTypeCheckpointActive); + Assert.True(vm.IsTypeAllActive); + + vm.SelectCivitaiType(null); + Assert.True(vm.IsTypeAllActive); + } } + diff --git a/LocalLLMServerManager.Tests/DocumentationViewModelTests.cs b/LocalLLMServerManager.Tests/DocumentationViewModelTests.cs index aa8d7d8..22e1167 100644 --- a/LocalLLMServerManager.Tests/DocumentationViewModelTests.cs +++ b/LocalLLMServerManager.Tests/DocumentationViewModelTests.cs @@ -58,4 +58,48 @@ public void SelectSection_SwitchesActiveDocument() // Should retain previous selection if target not found Assert.Equal("can-i-run-it", vm.SelectedSection.Id); } + + [Fact] + public void FloatingOverlay_TogglesAndNavigatesSteps() + { + var vm = new DocumentationViewModel(); + Assert.False(vm.IsFloatingOverlayOpen); + + vm.ToggleFloatingOverlay(); + Assert.True(vm.IsFloatingOverlayOpen); + Assert.False(vm.IsMinimizedToPill); + Assert.Equal(0, vm.CurrentStepIndex); + Assert.NotNull(vm.CurrentStep); + + vm.NextStep(); + Assert.Equal(1, vm.CurrentStepIndex); + + vm.PreviousStep(); + Assert.Equal(0, vm.CurrentStepIndex); + + vm.ToggleMinimizeToPill(); + Assert.True(vm.IsMinimizedToPill); + + vm.ToggleMinimizeToPill(); + Assert.False(vm.IsMinimizedToPill); + + vm.ToggleFloatingOverlay(); + Assert.False(vm.IsFloatingOverlayOpen); + } + + [Fact] + public void JumpToStepTab_InvokesCallbackWithCorrectTargetTab() + { + var vm = new DocumentationViewModel(); + vm.SelectSection("image-generation"); + + int receivedTab = -1; + vm.OnNavigateToTabRequested = tab => receivedTab = tab; + + Assert.NotNull(vm.CurrentStep); + Assert.Equal(1, vm.CurrentStep.TargetTab); + + vm.JumpToStepTab(null); + Assert.Equal(1, receivedTab); + } } diff --git a/LocalLLMServerManager.Tests/HuggingFaceSearchViewModelTests.cs b/LocalLLMServerManager.Tests/HuggingFaceSearchViewModelTests.cs index aad9354..d1c8215 100644 --- a/LocalLLMServerManager.Tests/HuggingFaceSearchViewModelTests.cs +++ b/LocalLLMServerManager.Tests/HuggingFaceSearchViewModelTests.cs @@ -284,4 +284,102 @@ public async Task OpenHfModalAsync_TogglesIsModalLoadingProperly() Assert.False(vm.IsModalLoading); Assert.Single(vm.ModalHfFiles); } + + [Fact] + public void Constructor_LoadsCuratedStarterModels_ByDefault() + { + var mockHf = new Mock(); + var vm = new HuggingFaceSearchViewModel(mockHf.Object); + + Assert.NotEmpty(vm.HuggingFaceResults); + Assert.NotEmpty(vm.FilteredHuggingFaceResults); + Assert.True(vm.HuggingFaceResults.Count >= 5); + foreach (var item in vm.HuggingFaceResults) + { + Assert.NotNull(item.FitBadge); + } + } + + [Fact] + public void ApplyPreset_SetsActivePreset_AndTogglesClearSelectedPipelineTag() + { + var mockHf = new Mock(); + var vm = new HuggingFaceSearchViewModel(mockHf.Object); + + vm.SelectedPipelineTag = "custom-tag"; + vm.ApplyPreset("LLM"); + + Assert.Null(vm.SelectedPipelineTag); + Assert.Equal("LLM", vm.ActivePreset); + Assert.True(vm.IsPresetLlm); + Assert.False(vm.IsPresetMultimodal); + + vm.ApplyPreset("Multimodal"); + Assert.Equal("Multimodal", vm.ActivePreset); + Assert.True(vm.IsPresetMultimodal); + Assert.False(vm.IsPresetLlm); + + vm.ApplyPreset("Image"); + Assert.Equal("Image", vm.ActivePreset); + Assert.True(vm.IsPresetImage); + + vm.ApplyPreset("Video"); + Assert.Equal("Video", vm.ActivePreset); + Assert.True(vm.IsPresetVideo); + + vm.ApplyPreset("Audio"); + Assert.Equal("Audio", vm.ActivePreset); + Assert.True(vm.IsPresetAudio); + + vm.ApplyPreset("3D"); + Assert.Equal("3D", vm.ActivePreset); + Assert.True(vm.IsPreset3D); + + // Manually toggling an input or output clears ActivePreset + vm.ToggleInputModality("Image"); + Assert.Null(vm.ActivePreset); + Assert.False(vm.IsPreset3D); + } + + [Fact] + public void ResolvePipelineTags_ForAudio_DoesNotIncludeTextGeneration() + { + var tags = HuggingFaceSearchViewModel.ResolvePipelineTags( + new[] { "Text", "Audio" }, + new[] { "Text", "Audio" }); + + Assert.DoesNotContain("text-generation", tags); + Assert.Contains("text-to-speech", tags); + Assert.Contains("text-to-audio", tags); + Assert.Contains("automatic-speech-recognition", tags); + Assert.Contains("audio-to-audio", tags); + } + + [Theory] + [InlineData("Qwen/Qwen2-VL-7B-Instruct", "image-text-to-text", "LLM")] + [InlineData("meta-llama/Llama-3.2-11B-Vision-Instruct", "image-to-text", "LLM")] + [InlineData("google/docvqa-donut", "visual-question-answering", "LLM")] + public void DetermineModality_VlmModels_ClassifiedAsLlmNotImage(string name, string tag, string expected) + { + var result = HuggingFaceSearchViewModel.DetermineModality(name, tag); + Assert.Equal(expected, result); + } + + [Fact] + public async Task LoadDefaultModelsAsync_LoadsFromService_OrRetainsCuratedStarterModels() + { + var mockHf = new Mock(); + mockHf.Setup(s => s.SearchRepositoriesAsync("http://localhost", "", "text-generation", It.IsAny())) + .ReturnsAsync(new List + { + new HuggingFaceRepoItem("custom/top-model", "custom", 1000, "50k", "text-generation", null) + }); + + var vm = new HuggingFaceSearchViewModel(mockHf.Object); + using var client = new HttpClient(); + await vm.LoadDefaultModelsAsync("http://localhost", client); + + Assert.Single(vm.HuggingFaceResults); + Assert.Equal("custom/top-model", vm.HuggingFaceResults[0].Id); + } } diff --git a/LocalLLMServerManager.Tests/SearchServicesTests.cs b/LocalLLMServerManager.Tests/SearchServicesTests.cs index 481eca5..be84240 100644 --- a/LocalLLMServerManager.Tests/SearchServicesTests.cs +++ b/LocalLLMServerManager.Tests/SearchServicesTests.cs @@ -233,6 +233,7 @@ public void HuggingFaceSearchViewModel_ApplyPreset_UpdatesActiveModalities() public void HuggingFaceSearchViewModel_FilterByFitVerdict_FiltersResultsCorrectly() { var vm = new LocalLLMServerManager.Shared.ViewModels.HuggingFaceSearchViewModel(new Mock().Object); + vm.HuggingFaceResults.Clear(); vm.HuggingFaceResults.Add(new LocalLLMServerManager.Shared.ViewModels.HuggingFaceRepoItem("repo1", "author", 10, "100 downloads", "text-generation", new LocalLLMServerManager.Shared.Models.QuickFitBadge("🟢 Full VRAM", "#10B981", "", LocalLLMServerManager.Shared.Models.FitVerdict.FullVram))); vm.HuggingFaceResults.Add(new LocalLLMServerManager.Shared.ViewModels.HuggingFaceRepoItem("repo2", "author", 20, "200 downloads", "text-generation", diff --git a/LocalLLMServerManager.Tests/UiDiagnosticLoggerTests.cs b/LocalLLMServerManager.Tests/UiDiagnosticLoggerTests.cs new file mode 100644 index 0000000..f3accb1 --- /dev/null +++ b/LocalLLMServerManager.Tests/UiDiagnosticLoggerTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using LocalLLMServerManager.Shared.Services; +using Xunit; + +namespace LocalLLMServerManager.Tests; + +public class UiDiagnosticLoggerTests +{ + [Fact] + public void RecordBindingError_AddsEntryWithAccurateTimestampAndSource() + { + var logger = new UiDiagnosticLogger(); + var beforeUtc = DateTime.UtcNow; + + logger.RecordBindingError("Cannot find property 'TestProp' on ViewModel", "TestView.axaml"); + + var afterUtc = DateTime.UtcNow; + + var entries = logger.GetRecentErrors(); + Assert.Single(entries); + + var entry = entries[0]; + Assert.Equal("Cannot find property 'TestProp' on ViewModel", entry.Message); + Assert.Equal("TestView.axaml", entry.Source); + Assert.InRange(entry.Timestamp, beforeUtc, afterUtc); + + // Also verify entry with null source + logger.RecordBindingError("Null source error"); + var allEntries = logger.GetRecentErrors(); + Assert.Equal(2, allEntries.Count); + Assert.Null(allEntries[1].Source); + Assert.Equal("Null source error", allEntries[1].Message); + } + + [Fact] + public void GetRecentErrors_LimitsReturnCount() + { + var logger = new UiDiagnosticLogger(); + + for (int i = 1; i <= 20; i++) + { + logger.RecordBindingError($"Error {i}", $"Source{i}"); + } + + // Limit to 5 + var recent5 = logger.GetRecentErrors(5); + Assert.Equal(5, recent5.Count); + Assert.Equal("Error 16", recent5[0].Message); + Assert.Equal("Error 20", recent5[4].Message); + + // When maxCount exceeds available items, return all available + var recent30 = logger.GetRecentErrors(30); + Assert.Equal(20, recent30.Count); + Assert.Equal("Error 1", recent30[0].Message); + Assert.Equal("Error 20", recent30[19].Message); + + // Edge case: maxCount <= 0 returns empty list + var recentZero = logger.GetRecentErrors(0); + Assert.Empty(recentZero); + + var recentNegative = logger.GetRecentErrors(-5); + Assert.Empty(recentNegative); + + // Default count is 50, so should return all 20 + var recentDefault = logger.GetRecentErrors(); + Assert.Equal(20, recentDefault.Count); + } + + [Fact] + public void BoundedHistory_DiscardsOldestEntriesWhenCapacityExceeded() + { + // Test with custom small capacity + const int capacity = 5; + var logger = new UiDiagnosticLogger(capacity); + + for (int i = 1; i <= 8; i++) + { + logger.RecordBindingError($"Error {i}"); + } + + var entries = logger.GetRecentErrors(10); + Assert.Equal(capacity, entries.Count); + + // Oldest entries 1, 2, 3 should have been discarded + Assert.DoesNotContain(entries, e => e.Message == "Error 1"); + Assert.DoesNotContain(entries, e => e.Message == "Error 2"); + Assert.DoesNotContain(entries, e => e.Message == "Error 3"); + + // Remaining entries should be 4 through 8 in FIFO order + Assert.Equal("Error 4", entries[0].Message); + Assert.Equal("Error 5", entries[1].Message); + Assert.Equal("Error 6", entries[2].Message); + Assert.Equal("Error 7", entries[3].Message); + Assert.Equal("Error 8", entries[4].Message); + } + + [Fact] + public void Clear_RemovesAllEntries() + { + var logger = new UiDiagnosticLogger(); + logger.RecordBindingError("Error 1"); + logger.RecordBindingError("Error 2"); + Assert.Equal(2, logger.GetRecentErrors().Count); + + logger.Clear(); + + var entries = logger.GetRecentErrors(); + Assert.Empty(entries); + + var summary = logger.ExportLogSummary(); + Assert.Contains("No binding errors recorded", summary, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ExportLogSummary_ReturnsFormattedSummary() + { + var logger = new UiDiagnosticLogger(); + + // Empty state + var emptySummary = logger.ExportLogSummary(); + Assert.Contains("No binding errors recorded", emptySummary, StringComparison.OrdinalIgnoreCase); + + // Populated state + logger.RecordBindingError("Binding failed for ItemCount", "DashboardView.axaml"); + logger.RecordBindingError("NullReference during converter evaluation", null); + + var summary = logger.ExportLogSummary(); + + Assert.Contains("DashboardView.axaml", summary); + Assert.Contains("Binding failed for ItemCount", summary); + Assert.Contains("NullReference during converter evaluation", summary); + Assert.Contains("2 error", summary, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void RecordBindingError_IsThreadSafeUnderConcurrentAccess() + { + const int threadCount = 10; + const int iterationsPerThread = 50; + const int capacity = 200; + var logger = new UiDiagnosticLogger(capacity); + + Parallel.For(0, threadCount, threadIndex => + { + for (int i = 0; i < iterationsPerThread; i++) + { + logger.RecordBindingError($"Thread {threadIndex} message {i}", $"Source{threadIndex}"); + _ = logger.GetRecentErrors(10); + } + }); + + var entries = logger.GetRecentErrors(capacity); + Assert.Equal(capacity, entries.Count); + Assert.True(entries.All(e => !string.IsNullOrEmpty(e.Message))); + } +} diff --git a/LocalLLMServerManager.csproj b/LocalLLMServerManager.csproj index 37503bc..bed68dc 100644 --- a/LocalLLMServerManager.csproj +++ b/LocalLLMServerManager.csproj @@ -39,6 +39,11 @@ + + + + + diff --git a/Program.cs b/Program.cs index 4547547..6639af0 100644 --- a/Program.cs +++ b/Program.cs @@ -8,6 +8,9 @@ using LocalLLMServerManager.Services; using LocalLLMServerManager.Shared.Interfaces; using LocalLLMServerManager.Shared.Services; +#if DEBUG +using AvaloniaMcp.Diagnostics; +#endif namespace LocalLLMServerManager; @@ -71,10 +74,16 @@ public static bool IsPortInUse(int port) } public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() + { + var builder = AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() .LogToTrace(); +#if DEBUG + builder = builder.UseMcpDiagnostics(); +#endif + return builder; + } public static string SettingsFilePath() => new SettingsService().SettingsFilePath(); diff --git a/Views/DocumentationWindow.axaml b/Views/DocumentationWindow.axaml new file mode 100644 index 0000000..a07061d --- /dev/null +++ b/Views/DocumentationWindow.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/DocumentationWindow.axaml.cs b/Views/DocumentationWindow.axaml.cs new file mode 100644 index 0000000..4884fc5 --- /dev/null +++ b/Views/DocumentationWindow.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using LocalLLMServerManager.Shared.ViewModels; + +namespace LocalLLMServerManager.Views; + +public partial class DocumentationWindow : Window +{ + public DocumentationWindow() + { + InitializeComponent(); + } + + public DocumentationWindow(DocumentationViewModel viewModel) : this() + { + DataContext = viewModel; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnPinClicked(object? sender, RoutedEventArgs e) + { + Topmost = !Topmost; + var pinText = this.FindControl("PinText"); + if (pinText != null) + { + pinText.Text = Topmost ? "📌 Pinned (Topmost)" : "📌 Pin to Top"; + pinText.Foreground = Topmost ? Brushes.LimeGreen : Brushes.Gray; + } + } +} diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs index e3c4608..0cfb8c6 100644 --- a/Views/MainWindow.axaml.cs +++ b/Views/MainWindow.axaml.cs @@ -9,10 +9,30 @@ namespace LocalLLMServerManager.Views; public partial class MainWindow : Window { + private DocumentationWindow? _docWindow; + public MainWindow() { InitializeComponent(); - DataContext = new MainViewModel(); +#if DEBUG + this.AttachDevTools(); +#endif + var mainVm = new MainViewModel(); + DataContext = mainVm; + + mainVm.Documentation.OnPopOutNativeWindowRequested = () => + { + if (_docWindow == null || !_docWindow.IsVisible) + { + _docWindow = new DocumentationWindow(mainVm.Documentation); + _docWindow.Closed += (s, e) => _docWindow = null; + _docWindow.Show(); + } + else + { + _docWindow.Activate(); + } + }; PropertyChanged += (sender, e) => { diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d6dce4e..702c7d7 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -73,6 +73,21 @@ export default defineConfig({ ] } ], + '/guide/': [ + { + text: 'Technical Reference (Developers & Agents)', + items: [ + { text: 'Technical Overview', link: '/technical/' }, + { text: 'System Architecture', link: '/technical/architecture' }, + { text: 'Development & Build Guide', link: '/technical/development' }, + { text: 'Collaborative UI Debugging', link: '/guide/collaborative-debugging' }, + { text: 'Requirements Specification', link: '/technical/requirements' }, + { text: 'Windows Process Validation', link: '/technical/validation' }, + { text: 'Test Coverage Benchmarks', link: '/technical/test-coverage' }, + { text: 'AI Assistant Internals', link: '/technical/ai-assistant-internals' } + ] + } + ], '/technical/': [ { text: 'Technical Reference (Developers & Agents)', @@ -80,6 +95,7 @@ export default defineConfig({ { text: 'Technical Overview', link: '/technical/' }, { text: 'System Architecture', link: '/technical/architecture' }, { text: 'Development & Build Guide', link: '/technical/development' }, + { text: 'Collaborative UI Debugging', link: '/guide/collaborative-debugging' }, { text: 'Requirements Specification', link: '/technical/requirements' }, { text: 'Windows Process Validation', link: '/technical/validation' }, { text: 'Test Coverage Benchmarks', link: '/technical/test-coverage' }, diff --git a/docs/guide/collaborative-debugging.md b/docs/guide/collaborative-debugging.md new file mode 100644 index 0000000..40b96e5 --- /dev/null +++ b/docs/guide/collaborative-debugging.md @@ -0,0 +1,375 @@ +--- +title: Collaborative UI Debugging with AvaloniaMcp & DevTools +description: Comprehensive guide for developers and AI coding assistants collaborating on desktop UI debugging, visual tree inspection, and runtime diagnostics. +outline: deep +--- + +# Collaborative UI Debugging with AvaloniaMcp & DevTools + +Local LLM Server Manager includes an in-app debugging mode designed for live, collaborative problem-solving between software developers and AI coding assistants (such as Antigravity). + +By uniting **Avalonia F12 DevTools** for human visual inspection with the **AvaloniaMcp** Model Context Protocol server for AI-driven programmatic inspection, teams can diagnose visual defects, inspect runtime data contexts, catch silent XAML binding errors, and verify fixes in real time. + +--- + +## Architecture & Dual-Channel Inspection + +Collaborative debugging operates on a dual-channel architecture: + +1. **Human Developer Channel**: The developer interacts with the running desktop application, evaluates ergonomics and visual aesthetics, and uses built-in F12 DevTools to inspect layout bounds, active styles, and element trees interactively. +2. **AI Assistant Channel**: The AI coding assistant connects through the `AvaloniaMcp` protocol server over a local named pipe, querying visual trees, serialized ViewModel state, and binding diagnostic logs via structured JSON-RPC tool calls. + +``` ++-------------------------------------------------------------------------+ +| DEVELOPER WORKSPACE | +| - Runs application in Debug mode | +| - Inspects visual layout & controls via F12 DevTools | +| - Reports visual anomalies or UX friction in conversation | ++------------------------------------+------------------------------------+ + | + v ++-------------------------------------------------------------------------+ +| AVALONIA RUNTIME PROCESS (PID: {pid}) | +| | +| - AppBuilder.Configure() | +| .UsePlatformDetect() | +| .UseMcpDiagnostics() <-- AvaloniaMcp Named Pipe Endpoint | +| .LogToTrace() | +| | +| - Named Pipe Endpoint: `avalonia-mcp-{pid}` | +| - Process Discovery File: `%TEMP%/avalonia-mcp/{pid}.json` | +| - UI Diagnostic Logger (Binding error trace capture) | ++------------------------------------+------------------------------------+ + | Named Pipe (JSON-RPC) + v ++-------------------------------------------------------------------------+ +| AVALONIA MCP SERVER (`avaloniamcp`) | +| | +| - Global .NET Tool (`dotnet avalonia-mcp`) | +| - Exposes 15 MCP Tools to AI Assistant over stdio | ++------------------------------------+------------------------------------+ + | MCP Protocol (stdio) + v ++-------------------------------------------------------------------------+ +| ANTIGRAVITY AI ASSISTANT | +| - Discovers running UI app instance (`discover_apps`) | +| - Queries visual/logical trees, DataContexts, & broken bindings | +| - Mutates properties live to test layout hypotheses | +| - Captures element screenshots for visual inspection | +| - Formulates and applies codebase fixes directly | ++-------------------------------------------------------------------------+ +``` + +### Production Isolation & Security + +All debugging and diagnostic bridges are conditionally compiled: + +* **Conditional Project References**: `AvaloniaMcp.Diagnostics` and `Avalonia.Diagnostics` are included in `LocalLLMServerManager.csproj` only when `'$(Configuration)' == 'Debug'`. +* **Preprocessor Directives**: Initialization calls (`UseMcpDiagnostics()` and `this.AttachDevTools()`) are wrapped in `#if DEBUG` preprocessor blocks. +* **Release Cleanliness**: In `Release` builds, no named pipe server is started, no discovery files are generated, F12 DevTools cannot be activated, and the compiled binaries contain zero runtime overhead or external attack surface. +* **IPC Security**: Named pipes bind exclusively to local OS inter-process communication (`%TEMP%/avalonia-mcp/`) accessible only by the current authenticated user session. + +--- + +## Launching in Debug Mode + +To enable collaborative debugging, compile and launch the project under the `Debug` configuration. + +### Command + +Run the following command from the repository root: + +```bash +dotnet run -c Debug +``` + +### Startup Initialization + +When launched in `Debug` configuration, the application executes the following startup sequence: + +1. **AppBuilder Diagnostics**: In `Program.cs`, `BuildAvaloniaApp()` invokes `.UseMcpDiagnostics()` and `.LogToTrace()`. +2. **Named Pipe Creation**: `AvaloniaMcp.Diagnostics` allocates a local named pipe identified as `avalonia-mcp-{pid}` where `{pid}` is the process ID of the running application. +3. **Discovery Metadata**: A JSON discovery file is written to `%TEMP%/avalonia-mcp/{pid}.json` containing the process ID, application name, start time, and named pipe endpoint. +4. **DevTools Attachment**: In `Views/MainWindow.axaml.cs`, the window constructor calls `this.AttachDevTools()`, registering the `F12` global shortcut. +5. **Diagnostic Logging**: The `UiDiagnosticLogger` registers trace listeners to intercept Avalonia binding warnings and errors. + +--- + +## Human Developer: Using F12 DevTools + +The built-in Avalonia DevTools provide immediate visual inspection without requiring external browsers or agents. + +### Opening DevTools + +1. Ensure the desktop application window is focused. +2. Press F12. +3. A separate **Avalonia DevTools** diagnostic window opens. + +### Key DevTools Capabilities + +#### 1. Visual Tree & Logical Tree Inspection +* **Visual Tree**: Displays every rendered visual primitive (e.g., `Border`, `ContentPresenter`, `TextBlock`, `LayoutTransformControl`). Use this to determine actual render sizes, margins, padding, clipping rectangles, and alignment. +* **Logical Tree**: Displays controls as declared in high-level XAML markup (e.g., `Button`, `ListBox`, `Grid`), making it straightforward to match UI elements with their source `.axaml` files. +* **Pointer Selection**: Click the crosshair icon in the DevTools toolbar, then click any element in the main application window to jump directly to that element in the tree. + +#### 2. Property Inspector & Live Editing +* Selecting any node in the tree shows all registered Avalonia properties in the right-hand panel. +* **Property Precedence**: Observe whether a property value originates from a local assignment, an active style setter, an inherited value, or default metadata. +* **Live Property Modification**: Double-click editable values (such as `Width`, `Height`, `Margin`, `HorizontalAlignment`, `Background`, or `IsVisible`) to change them at runtime. This allows rapid verification of layout fixes before editing source code. + +#### 3. Style Debugging +* The **Styles** tab lists every style rule currently evaluated against the selected control. +* Active rules are highlighted, while overridden or unmatched rules are dimmed. +* **Pseudo-Class Tracking**: Observe dynamic pseudo-classes such as `:pointerover`, `:pressed`, `:focus`, and `:disabled` update live as you interact with the UI. + +#### 4. Event Tracking & Layout Diagnostics +* The **Events** tab records routed events (pointer moved, pointer pressed, key down) bubbling or tunneling through the tree. +* Use this to diagnose why a button click is not reaching an expected handler or if an invisible overlay is intercepting pointer input. + +--- + +## AI Assistant: Connecting via AvaloniaMcp + +AI assistants connect to the running application using the open-source `avaloniamcp` Model Context Protocol server. + +### 1. Installing the Global Tool + +Install the `avaloniamcp` global .NET tool from NuGet: + +```bash +dotnet tool install -g avaloniamcp +``` + +To update an existing installation to the latest release: + +```bash +dotnet tool update -g avaloniamcp +``` + +### 2. Running the MCP Server + +Start the server using standard stdio communication: + +```bash +dotnet avalonia-mcp +``` + +The server automatically monitors `%TEMP%/avalonia-mcp/`, discovers any active Avalonia application running with `.UseMcpDiagnostics()`, and bridges MCP tool calls directly to the application's UI thread via the local named pipe. + +### 3. MCP Client Configuration + +To configure Antigravity, Claude Desktop, Cursor, or other MCP-compatible AI clients, add the server to your configuration file (e.g., `mcpServers` block): + +```json +{ + "mcpServers": { + "avalonia_mcp": { + "command": "dotnet", + "args": ["avalonia-mcp"] + } + } +} +``` + +### 4. CLI Verification & Diagnostics + +You can verify the connection manually using the `avaloniamcp` CLI: + +```bash +# Discover running Avalonia processes +dotnet avalonia-mcp cli discover_apps + +# List open windows +dotnet avalonia-mcp cli list_windows + +# Check for active binding errors +dotnet avalonia-mcp cli get_binding_errors + +# Inspect top-level visual elements +dotnet avalonia-mcp cli get_visual_tree --maxDepth 3 +``` + +--- + +## The 15 AvaloniaMcp Tools + +`avaloniamcp` exposes 15 specialized tools to AI assistants, categorized into five operational domains: + +| Category | Tool Name | Parameters | Description | +| :--- | :--- | :--- | :--- | +| **Inspection** | `list_windows` | *None* | Lists all open windows, titles, dimensions, positions, and window states. | +| | `get_visual_tree` | `maxDepth` *(optional)* | Returns the complete rendered visual hierarchy with element types, names, bounds, and visibility. | +| | `get_logical_tree` | `maxDepth` *(optional)* | Returns the logical hierarchy matching the developer's XAML markup declarations. | +| | `find_control` | `name`, `typeName`, `text` *(optional)* | Fast lookup of UI elements by name (`#Name`), control type, or displayed text. | +| | `get_control_properties` | `controlId` | Dumps all registered Avalonia properties, current values, types, and inheritance sources for an element. | +| **Data & Bindings** | `get_data_context` | `controlId` *(optional)* | Serializes the bound ViewModel properties and values into clean JSON. | +| | `get_binding_errors` | *None* | Retrieves all active and logged Avalonia binding errors, including target properties and source paths. | +| **Visual & Styles** | `take_screenshot` | `controlId` *(optional)* | Captures a high-resolution base64 PNG of the entire window or a specific control for visual analysis. | +| | `get_applied_styles` | `controlId` | Inspects matching style selectors, active setters, and pseudo-classes (`:pointerover`, `:pressed`). | +| | `get_resources` | `controlId` *(optional)* | Enumerates XAML resources (brushes, colors, geometry, templates) accessible at the element's scope. | +| | `get_focused_element` | *None* | Returns the currently focused control and its keyboard tab navigation index. | +| **Interaction** | `click_control` | `controlId` | Programmatically triggers a click event and executes bound `ICommand` handlers. | +| | `input_text` | `controlId`, `text` | Types text into `TextBox` or other editable input controls. | +| | `set_property` | `controlId`, `propertyName`, `value` | Mutates control properties at runtime to test layout fixes live on the UI thread. | +| **Discovery** | `discover_apps` | *None* | Discovers all running Avalonia applications instrumented with `AvaloniaMcp.Diagnostics`. | + +### Tool Deep Dive + +#### `list_windows` +Returns an array of active desktop windows, indicating whether each window is active, minimized, normal, or maximized, along with screen coordinates and dimensions (`X`, `Y`, `Width`, `Height`). + +#### `get_visual_tree` +Generates a structured tree representation of every visual element. Each node includes a unique `controlId`, type name (e.g., `Avalonia.Controls.Button`), element name, bounds, and visibility state. Limiting `maxDepth` prevents overwhelming output on deeply nested layouts. + +#### `get_logical_tree` +Summarizes the UI hierarchy from the perspective of XAML logical parenting. This view omits internal layout primitives (such as internal borders and presenters), making it easier to reason about high-level component organization. + +#### `find_control` +Allows the AI assistant to search across the UI hierarchy in a single step. For example, calling `find_control(name: "HuggingFaceSearchBox")` or `find_control(text: "Download")` quickly returns the target `controlId` without traversing the entire tree. + +#### `get_control_properties` +Fetches the full property dictionary of a specific control. Includes layout metrics (`Margin`, `Padding`, `HorizontalAlignment`, `ActualWidth`), state properties (`IsEnabled`, `IsVisible`), and control-specific configurations. + +#### `get_data_context` +Inspects the ViewModel instance bound to the target control. The server traverses the object graph and serializes properties, collections, and commands into JSON. If a control has no local `DataContext`, it inherits and inspects the ancestor context. + +#### `get_binding_errors` +Queries the in-app diagnostic log for binding failures. This captures silent failures where Avalonia encounters a missing ViewModel property, invalid cast, or null path element during evaluation. + +#### `take_screenshot` +Renders the specified control or the entire window into an off-screen render target and returns a base64-encoded PNG image. This enables multimodal AI models to visually inspect alignment, clipping, contrast, and layout rendering. + +#### `get_applied_styles` +Returns the cascade of styles matching the control. Identifies active selectors, applied setters, and pseudo-classes, pinpointing whether an unintended theme rule or local style is overriding expected colors or margins. + +#### `get_resources` +Inspects local and inherited XAML `ResourceDictionary` trees. Enables verifying whether brush keys (e.g., `AccentColorBrush`, `SystemControlBackgroundBaseMediumBrush`) resolve to intended color definitions. + +#### `get_focused_element` +Returns the element currently holding keyboard focus. Useful for diagnosing keyboard navigation bugs, focus trapping, or broken tab indexing. + +#### `click_control` +Dispatches a synthetic pointer click event on the UI thread for the target control. Verifies whether button commands execute correctly and updates the UI state accordingly. + +#### `input_text` +Sets text on editable controls, raising corresponding text change and binding notification events to simulate user data entry. + +#### `set_property` +Dispatches a property update directly to the Avalonia property system on the UI thread. The AI assistant can test candidate values (e.g., changing `Width` from `NaN` to `200`, or `IsVisible` from `false` to `true`) and immediately verify the visual outcome before modifying files. + +#### `discover_apps` +Scans local IPC registration files in `%TEMP%/avalonia-mcp/` and reports all detectable Avalonia instances with their process IDs and executable paths. + +--- + +## Diagnostic Logging & Binding Error Inspection + +In XAML-based frameworks, data binding errors fail silently by default to prevent application crashes during render loops. However, silent failures lead to empty lists, unresponsive buttons, and blank labels that are difficult to diagnose from application logs alone. + +### Common Binding Failure Modes + +1. **Path Typo**: The XAML binding `{Binding ModelTitel}` references a misspelled property (`ModelTitle`). +2. **Missing Notification**: A ViewModel property lacks `SetProperty(ref _field, value)` or `[ObservableProperty]`, preventing UI updates when values change. +3. **Null Path Navigation**: Binding `{Binding SelectedEngine.Config.Port}` fails because `SelectedEngine` or `Config` is null during initialization. +4. **Type Conversion Mismatch**: Binding a string to an enum property without an appropriate `IValueConverter`. + +### In-App Diagnostic Logger (`UiDiagnosticLogger`) + +To capture these issues, `LocalLLMServerManager` includes a dedicated `UiDiagnosticLogger` service: + +* **Trace Interception**: Hooks into Avalonia's internal `Trace.Listeners` and `Logger` system, filtering for `LogEventLevel.Warning` and `LogEventLevel.Error` on the `Binding` log category. +* **Ring Buffer Storage**: Stores recent entries in a bounded circular buffer (capped at 250 entries) to prevent unbounded memory growth during long debugging sessions. +* **Structured Records**: Each diagnostic entry captures: + * Timestamp (UTC) + * Target control type and name + * Bound target property name + * Source path expression + * Full exception or warning message + +### Example Diagnostic Log Entry + +When `get_binding_errors` is called, the AI assistant receives structured error information: + +```json +[ + { + "timestamp": "2026-09-18T20:15:32.410Z", + "target": "Avalonia.Controls.Button #FilterLoraButton", + "property": "Command", + "sourcePath": "ToggleLoraFilterCommand", + "message": "Could not find property 'ToggleLoraFilterCommand' on 'CivitaiSearchViewModel'." + } +] +``` + +This precise output immediately indicates that `CivitaiSearchViewModel` lacks the expected command or named it differently (e.g., `FilterLoraCommand`), eliminating guesswork. + +--- + +## Collaborative Bug-Hunting Workflow + +The diagram below illustrates the typical workflow between the developer, the running application, the `avaloniamcp` server, and the AI assistant: + +```mermaid +sequenceDiagram + autonumber + actor Dev as Developer + participant App as Avalonia App (Debug) + participant MCP as avaloniamcp Server + participant AI as Antigravity AI + + Dev->>App: Launch via `dotnet run -c Debug` + App->>MCP: Register named pipe in %TEMP%/avalonia-mcp/ + Dev->>App: Interacts with UI (e.g. Hugging Face / CivitAI Hub) + Dev->>AI: Reports symptom: "The CivitAI LoRA toggle button does not activate" + + rect rgb(30, 40, 60) + Note over AI,MCP: Autonomous AI Inspection Loop + AI->>MCP: `discover_apps()` + MCP-->>AI: Returns active process ID + AI->>MCP: `find_control(name: "FilterLoraButton")` + MCP-->>AI: Returns controlId "c-1048" + AI->>MCP: `get_control_properties(controlId: "c-1048")` + MCP-->>AI: Returns IsEnabled: false, Classes: [] + AI->>MCP: `get_binding_errors()` + MCP-->>AI: Returns Command binding error on ToggleLoraFilterCommand + AI->>MCP: `get_data_context(controlId: "c-1048")` + MCP-->>AI: Returns ViewModel state (FilterType: Checkpoint) + end + + AI->>Dev: Explains root cause: XAML bound to non-existent command + AI->>App: Applies code fix in CivitaiSearchViewModel.cs & XAML + AI->>AI: Runs `dotnet test`, `npm run lint`, `npx tsc --noEmit` + Dev->>App: Validates fix live in the desktop UI +``` + +### Step-by-Step Problem Resolution + +1. **Reproduction**: The developer reproduces a UI glitch or unexpected state in the running application. +2. **Report**: The developer describes the observation to the AI assistant (e.g., *"The Hugging Face search box doesn't submit when pressing Enter"* or *"The status badge is clipped"*). +3. **Targeted Inspection**: + * The AI assistant calls `find_control` to locate the relevant element. + * Calls `get_data_context` to inspect current ViewModel state. + * Calls `get_binding_errors` to check for silent binding failures. +4. **Visual Verification**: If the issue involves alignment or styling, the AI assistant calls `take_screenshot` or `get_applied_styles`. +5. **Interactive Prototyping**: The AI assistant can invoke `set_property` to verify whether a proposed property change resolves the issue live. +6. **Codebase Modification**: The AI assistant updates the appropriate `.axaml` or `.cs` file in the repository. +7. **Verification**: The AI assistant runs automated quality gates (`npm run lint`, `npx tsc --noEmit`, `dotnet test`). +8. **Confirmation**: The developer reviews the running UI and confirms the fix. + +--- + +## Developer Quick Reference + +| Action | Command / Shortcut | Purpose | +| :--- | :--- | :--- | +| **Launch in Debug Mode** | `dotnet run -c Debug` | Launches app with DevTools and `AvaloniaMcp` active. | +| **Toggle DevTools** | F12 (in app) | Opens the interactive Avalonia DevTools inspector window. | +| **Install MCP Tool** | `dotnet tool install -g avaloniamcp` | Installs the global MCP server on developer machines. | +| **Update MCP Tool** | `dotnet tool update -g avaloniamcp` | Updates `avaloniamcp` to the latest version. | +| **Test MCP Connection** | `dotnet avalonia-mcp cli discover_apps` | Lists active Avalonia instances available for debugging. | +| **Inspect Binding Errors** | `dotnet avalonia-mcp cli get_binding_errors` | Dumps current binding diagnostic log directly to terminal. | +| **Run Unit Tests** | `dotnet test` | Executes solution test suite. | +| **Build Documentation** | `npm run docs:build` | Verifies VitePress documentation builds cleanly with zero errors. | +| **Lint Codebase** | `npm run lint` | Runs ESLint across TypeScript and tooling scripts. | +| **Typecheck Codebase** | `npx tsc --noEmit` | Runs TypeScript typechecker. | diff --git a/docs/superpowers/plans/2026-09-18-collaborative-debug-mcp-plan.md b/docs/superpowers/plans/2026-09-18-collaborative-debug-mcp-plan.md new file mode 100644 index 0000000..2a0fe4e --- /dev/null +++ b/docs/superpowers/plans/2026-09-18-collaborative-debug-mcp-plan.md @@ -0,0 +1,119 @@ +# Collaborative UI Debugging with AvaloniaMcp Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enable live, collaborative debugging between the developer and AI assistant using `AvaloniaMcp` (MCP server for Avalonia) and F12 DevTools in `Debug` builds. + +**Architecture:** Integrate `AvaloniaMcp.Diagnostics` and `Avalonia.Diagnostics` conditionally for Debug builds. Wire a dedicated `UiDiagnosticLogger` to capture Avalonia binding errors and trace logs. Add user-facing documentation in VitePress and automated unit tests. + +**Tech Stack:** C# 13, .NET 10, Avalonia 12.1.2, AvaloniaMcp.Diagnostics 0.4.0, Avalonia.Diagnostics 11.3.22, xUnit, VitePress. + +## Global Constraints +- Do NOT create releases, git tags, or bump version numbers. +- Do NOT use the acronym "ASD-STE100" in any documentation, code, or UI copy. +- Always run `npm run lint` and `npx tsc --noEmit` after code changes. +- Ensure all packages are conditionally included only in `Debug` configuration so `Release` builds remain unaffected. + +--- + +### Task 1: Package References & Startup Initialization + +**Files:** +- Modify: `LocalLLMServerManager.csproj` +- Modify: `Program.cs` +- Modify: `Views/MainWindow.axaml.cs` + +**Interfaces:** +- Produces: Application startup in `Debug` mode launches AvaloniaMcp named pipe server (`UseMcpDiagnostics()`) and enables F12 DevTools (`this.AttachDevTools()`). + +- [ ] **Step 1: Update `LocalLLMServerManager.csproj`** +Add conditional PackageReferences for `AvaloniaMcp.Diagnostics` (0.4.0) and `Avalonia.Diagnostics` (11.3.22) when `'$(Configuration)' == 'Debug'`. + +- [ ] **Step 2: Update `Program.cs`** +Add `#if DEBUG using AvaloniaMcp.Diagnostics; #endif` and wire `.UseMcpDiagnostics()` into `BuildAvaloniaApp()` under `#if DEBUG`. + +- [ ] **Step 3: Update `Views/MainWindow.axaml.cs`** +Add `#if DEBUG this.AttachDevTools(); #endif` in `MainWindow` constructor. + +- [ ] **Step 4: Verify build in both Debug and Release** +Run `dotnet build -c Debug` and `dotnet build -c Release` to confirm successful compilation. + +- [ ] **Step 5: Commit** +`git commit -m "feat(debug): integrate AvaloniaMcp diagnostics and F12 DevTools for debug builds"` + +--- + +### Task 2: UI Diagnostic Trace Interceptor & Unit Tests + +**Files:** +- Create: `LocalLLMServerManager.Shared/Services/UiDiagnosticLogger.cs` +- Create: `LocalLLMServerManager.Tests/UiDiagnosticLoggerTests.cs` + +**Interfaces:** +- Produces: `IUiDiagnosticLogger` / `UiDiagnosticLogger` with methods: + - `void RecordBindingError(string message, string? source = null)` + - `IReadOnlyList GetRecentErrors()` + - `void Clear()` + - `string ExportLogSummary()` + +- [ ] **Step 1: Write failing test in `UiDiagnosticLoggerTests.cs`** +Test that `UiDiagnosticLogger` records binding errors, limits max history to prevent memory leaks, and formats timestamps accurately. + +- [ ] **Step 2: Run test to verify it fails** +Run `dotnet test LocalLLMServerManager.Tests --filter "FullyQualifiedName~UiDiagnosticLoggerTests"`. + +- [ ] **Step 3: Implement `UiDiagnosticLogger.cs`** +Implement thread-safe diagnostic logger. + +- [ ] **Step 4: Run test to verify it passes** +Run `dotnet test LocalLLMServerManager.Tests --filter "FullyQualifiedName~UiDiagnosticLoggerTests"`. + +- [ ] **Step 5: Commit** +`git commit -m "feat(diagnostics): add UiDiagnosticLogger and unit test coverage"` + +--- + +### Task 3: Documentation for Collaborative Debugging + +**Files:** +- Create: `docs/guide/collaborative-debugging.md` +- Modify: `docs/.vitepress/config.ts` + +**Interfaces:** +- Produces: Clear developer guide explaining: + - How to start the app in Debug mode. + - How `avaloniamcp` runs and connects via named pipes. + - The 15 available MCP tools for AI assistants. + - How human developers use F12 DevTools. + - Best practices for reporting and fixing UI issues. + +- [ ] **Step 1: Create `docs/guide/collaborative-debugging.md`** +Write clear, accessible guide without any "ASD-STE100" terminology. + +- [ ] **Step 2: Update `docs/.vitepress/config.ts` sidebar** +Add link under Developer Guide / Advanced Features. + +- [ ] **Step 3: Build docs site** +Run `npm run docs:build` to confirm 0 errors. + +- [ ] **Step 4: Commit** +`git commit -m "docs: add comprehensive collaborative debugging guide for AvaloniaMcp"` + +--- + +### Task 4: Global Tool Verification, Quality Gates & PR + +**Files:** +- All modified files + +- [ ] **Step 1: Install `avaloniamcp` global tool** +Run `dotnet tool install -g avaloniamcp` (or update if already installed). + +- [ ] **Step 2: Run full verification suite** +Run `dotnet test -c Release --nologo`. +Run `npm run lint`. +Run `npx tsc --noEmit`. +Run `npm run docs:build`. + +- [ ] **Step 3: Push branch and create Pull Request** +Push feature branch and open PR via `gh pr create`. diff --git a/docs/superpowers/plans/2026-09-18-floating-docs-and-hf-hub.md b/docs/superpowers/plans/2026-09-18-floating-docs-and-hf-hub.md new file mode 100644 index 0000000..dd5f083 --- /dev/null +++ b/docs/superpowers/plans/2026-09-18-floating-docs-and-hf-hub.md @@ -0,0 +1,148 @@ +# Floating Documentation & Hugging Face Hub Overhaul Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or direct execution to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Provide a floating, movable in-app documentation overlay/window with smart tab navigation, and overhaul the Hugging Face Hub with default model loading and clear active toggle styling. + +**Architecture:** Extend `DocumentationViewModel` with overlay states and `TargetTab` step navigation. Create `DocumentationFloatingOverlay.axaml` on the top canvas layer of `MainView.axaml` and a native `DocumentationWindow.axaml` for desktop pop-out. Update `HuggingFaceSearchViewModel` to load starter models on startup, and update `HuggingFaceTabControl.axaml` with high-contrast active/inactive toggle styling. + +**Tech Stack:** C# (.NET 10), Avalonia UI 12.x, CommunityToolkit.Mvvm, xUnit. + +## Global Constraints + +* Must maintain 100% test pass rate across `LocalLLMServerManager.Tests`. +* Must pass `npm run lint` and `npx tsc --noEmit` with zero errors. +* Must support both Desktop and WebAssembly without platform crashes. +* Avoid using "ASD-STE100" in user-facing labels. + +--- + +### Task 1: Extend DocumentationViewModel with Floating States & Step Target Tabs + +**Files:** +- Modify: `LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs` +- Test: `LocalLLMServerManager.Tests/DocumentationViewModelTests.cs` + +**Interfaces:** +- Produces: `DocStep.TargetTab`, `IsFloatingOverlayOpen`, `IsMinimizedToPill`, `OverlayX`, `OverlayY`, `CurrentStepIndex`, `JumpToStepTabCommand`. + +- [ ] **Step 1: Update DocStep record to include TargetTab** + ```csharp + public record DocStep( + int StepNumber, + string Title, + string Action, + string ExpectedResult, + int? TargetTab = null + ); + ``` + +- [ ] **Step 2: Add floating state properties and commands to DocumentationViewModel** + Add observable properties: + - `bool IsFloatingOverlayOpen = false` + - `bool IsMinimizedToPill = false` + - `double OverlayX = 20` + - `double OverlayY = 60` + - `int CurrentStepIndex = 0` + Add commands: + - `ToggleFloatingOverlayCommand()` + - `ToggleMinimizeToPillCommand()` + - `NextStepCommand()` + - `PreviousStepCommand()` + - `JumpToStepTab(int? targetTab)` with an `Action? OnNavigateToTabRequested` callback. + +- [ ] **Step 3: Map TargetTab on procedural steps** + Map `TargetTab: 1` (Workflows) on generation steps, `TargetTab: 0` (Models) on model steps, `TargetTab: 2` (Can I Run It) on compatibility steps, and `TargetTab: 5` (Settings) on setup steps. + +- [ ] **Step 4: Update DocumentationViewModelTests and verify pass** + Run: `dotnet test LocalLLMServerManager.Tests/LocalLLMServerManager.Tests.csproj --filter "FullyQualifiedName~DocumentationViewModelTests"` + +- [ ] **Step 5: Commit Task 1** + ```bash + git add LocalLLMServerManager.Shared/ViewModels/DocumentationViewModel.cs LocalLLMServerManager.Tests/DocumentationViewModelTests.cs + git commit -m "feat(docs): add floating overlay states and step target tabs to DocumentationViewModel" + ``` + +--- + +### Task 2: Create Floating Documentation Overlay & Pop-Out Controls + +**Files:** +- Create: `LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml` +- Create: `LocalLLMServerManager.Shared/Views/Controls/DocumentationFloatingOverlay.axaml.cs` +- Modify: `LocalLLMServerManager.Shared/Views/Controls/DocumentationTabControl.axaml` +- Modify: `LocalLLMServerManager.Shared/Views/MainView.axaml` +- Modify: `LocalLLMServerManager.Shared/Views/MainView.axaml.cs` + +**Interfaces:** +- Consumes: `DocumentationViewModel` +- Produces: Movable floating guide overlay and pop-out trigger button. + +- [ ] **Step 1: Create DocumentationFloatingOverlay.axaml** + Implement floating card with: + - Header drag region (`PointerPressed`, `PointerMoved`, `PointerReleased` handlers updating `OverlayX` and `OverlayY`). + - Minimize/Expand button (`IsMinimizedToPill`). + - Close button (`ToggleFloatingOverlayCommand`). + - Step counter (`Step X of Y`) with `<` Previous and `>` Next buttons. + - Active step description and action text. + - `🚀 Jump to Tab` button invoking `JumpToStepTabCommand`. + +- [ ] **Step 2: Add Pop-Out button to DocumentationTabControl.axaml** + Add `[🗗 Pop Out Floating Guide]` button in the top right of the documentation header that executes `ToggleFloatingOverlayCommand`. + +- [ ] **Step 3: Integrate floating overlay layer in MainView.axaml** + Wrap `MainView.axaml` content in a `Panel` or top-level `Grid` with `DocumentationFloatingOverlay` positioned via `Margin` or `Canvas` bound to `Documentation`. Wire up `OnNavigateToTabRequested` in `MainViewModel` to set `SelectedTabIndex`. + +- [ ] **Step 4: Verify build and test** + Run `dotnet build LocalLLMServerManager.slnx` and verify zero compilation errors. + +- [ ] **Step 5: Commit Task 2** + ```bash + git add LocalLLMServerManager.Shared/Views/ + git commit -m "feat(ui): implement floating documentation overlay with draggable controls and step navigation" + ``` + +--- + +### Task 3: Overhaul Hugging Face Hub (Default Loading, Active Toggle Styling, Filter Logic) + +**Files:** +- Modify: `LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs` +- Modify: `LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml` +- Modify: `LocalLLMServerManager.Tests/HuggingFaceSearchViewModelTests.cs` + +**Interfaces:** +- Produces: Initial model loading on launch, distinct visual active/inactive states for modality buttons and hardware filters. + +- [ ] **Step 1: Implement default starter model loading in HuggingFaceSearchViewModel** + Add an `InitializeDefaultModelsAsync()` method or auto-trigger in constructor/navigation that queries curated high-utility models (`Qwen/Qwen2.5-Coder-7B-Instruct-GGUF`, `meta-llama/Llama-3.2-3B-Instruct-GGUF`, `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B-GGUF`, `Wan-AI/Wan2.1-T2V-14B`, `hexgrad/Kokoro-82M`) so the hub is populated immediately. + +- [ ] **Step 2: Update HuggingFaceTabControl.axaml with high-contrast active toggle styles** + - For input buttons (`Text`, `Image`, `Audio`, `Video`): Bind background and foreground to active states (`IsInputTextActive`, etc.). Active buttons show vibrant blue `#2563EB` with `✓` checkmark; inactive show muted `#1E293B`. + - For output buttons (`Text`, `Image`, `Audio`, `Video`, `3D`): Active buttons show vibrant purple `#8B5CF6` with `✓` checkmark; inactive show muted `#1E293B`. + - For hardware filters: Active chips show full color (`#10B981`, `#F59E0B`, `#F97316`, `#EF4444`) with solid border; inactive chips show dim `Opacity="0.35"`. + +- [ ] **Step 3: Verify and update HuggingFaceSearchViewModelTests** + Ensure tests verify initial loading and filter toggle states. Run `dotnet test LocalLLMServerManager.Tests/LocalLLMServerManager.Tests.csproj --filter "FullyQualifiedName~HuggingFaceSearchViewModelTests"`. + +- [ ] **Step 4: Commit Task 3** + ```bash + git add LocalLLMServerManager.Shared/ViewModels/HuggingFaceSearchViewModel.cs LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml LocalLLMServerManager.Tests/HuggingFaceSearchViewModelTests.cs + git commit -m "feat(huggingface): add default model loading and clear active toggle styling" + ``` + +--- + +### Task 4: Verification, Quality Gates & Pull Request + +**Files:** +- All modified files. + +- [ ] **Step 1: Run complete test suite** + Run `dotnet test LocalLLMServerManager.Tests/LocalLLMServerManager.Tests.csproj -c Release --nologo` +- [ ] **Step 2: Run linter and TypeScript checks** + Run `npm run lint` and `npx tsc --noEmit` +- [ ] **Step 3: Run VitePress docs build** + Run `npm run docs:build` +- [ ] **Step 4: Commit and push feature branch** + Push `feature/fixes-and-adjustments` to origin and open a PR. diff --git a/docs/superpowers/specs/2026-09-18-collaborative-debug-workflow-design.md b/docs/superpowers/specs/2026-09-18-collaborative-debug-workflow-design.md new file mode 100644 index 0000000..0b4c4a0 --- /dev/null +++ b/docs/superpowers/specs/2026-09-18-collaborative-debug-workflow-design.md @@ -0,0 +1,156 @@ +# Design Spec: Collaborative UI Bug Hunting via AvaloniaMcp & DevTools + +**Date:** 2026-09-18 +**Status:** Approved +**Target Branch:** `feature/fixes-and-adjustments` + +--- + +## 1. Overview & Purpose + +This design enables a collaborative, live debugging environment between the developer and Antigravity (AI coding assistant) while running `LocalLLMServerManager` in `Debug` mode. + +The developer explores the desktop Avalonia UI, hunts for bugs, and reports anomalies or user experience friction. Concurrently, Antigravity attaches directly to the running application process via the open-source **`AvaloniaMcp`** Model Context Protocol (MCP) server, inspecting the live visual and logical trees, reading ViewModel data contexts, intercepting broken XAML bindings, capturing UI screenshots, and programmatically triggering commands to reproduce and resolve bugs in real time. + +--- + +## 2. Architecture & Components + +``` ++-------------------------------------------------------------------------+ +| USER WORKSPACE | +| - Browses Desktop Avalonia UI in Debug mode | +| - Inspects visual layout & controls with F12 (Avalonia.Diagnostics) | +| - Reports issues, edge cases, and UI friction in conversation | ++------------------------------------+------------------------------------+ + | + v ++-------------------------------------------------------------------------+ +| AVALONIA RUNTIME PROCESS (PID: {pid}) | +| | +| - AppBuilder.Configure() | +| .UsePlatformDetect() | +| .UseMcpDiagnostics() <-- AvaloniaMcp Named Pipe Server | +| .LogToTrace() | +| | +| - Named Pipe Endpoint: `avalonia-mcp-{pid}` | +| - Process Discovery File: `%TEMP%/avalonia-mcp/{pid}.json` | ++------------------------------------+------------------------------------+ + | Named Pipe (JSON-RPC) + v ++-------------------------------------------------------------------------+ +| AVALONIA MCP SERVER (`avaloniamcp`) | +| | +| - Global .NET Tool (`dotnet avalonia-mcp`) | +| - Connects to `avalonia-mcp-{pid}` via stdio transport | +| - Exposes 15 MCP Tools to AI Assistant | ++------------------------------------+------------------------------------+ + | MCP Protocol (stdio) + v ++-------------------------------------------------------------------------+ +| ANTIGRAVITY AI ASSISTANT | +| - Discovers running UI app instance | +| - Queries visual/logical trees, DataContexts, & broken bindings | +| - Reads & edits control properties dynamically | +| - Captures screenshots of buggy controls or layouts | +| - Diagnoses root causes and applies code fixes directly | ++-------------------------------------------------------------------------+ +``` + +### Key Components + +1. **`AvaloniaMcp.Diagnostics` (NuGet Package v0.4.0)**: + - Added conditionally to `LocalLLMServerManager.csproj` for `'$(Configuration)' == 'Debug'`. + - Free, open-source MIT library designed specifically for `.NET 10` and `Avalonia 11.2+`. + - Starts a lightweight named-pipe server in the app on launch (`UseMcpDiagnostics()`) with zero runtime overhead in production. + +2. **`avaloniamcp` (.NET Global Tool v0.4.0)**: + - Installed globally via `dotnet tool install -g avaloniamcp`. + - Connects to the running Avalonia named-pipe process and translates MCP tool invocations into UI-thread-dispatched inspection calls. + +3. **`Avalonia.Diagnostics` (NuGet Package v11.3.22 / 12.x)**: + - Added conditionally to `LocalLLMServerManager.csproj` for `'$(Configuration)' == 'Debug'`. + - Enables the built-in F12 DevTools inspection window for human developer interaction. + +4. **Antigravity MCP Configuration**: + - Registered as `avalonia_mcp` server using command `dotnet` and arguments `["avalonia-mcp"]`. + +--- + +## 3. Tool Reference (AvaloniaMcp) + +The `avaloniamcp` server exposes 15 specialized tools to Antigravity: + +| Category | Tool | Parameters | Functionality | +|---|---|---|---| +| **Inspection** | `list_windows` | None | Lists all open windows, titles, bounds, and states. | +| | `get_visual_tree` | `maxDepth?` | Full visual element hierarchy with types, names, layout bounds, and visibility. | +| | `get_logical_tree` | `maxDepth?` | High-level logical tree representing developer XAML intent. | +| | `find_control` | `name?`, `typeName?`, `text?` | Fast lookup of UI elements by `#Name`, `Type`, or text content. | +| | `get_control_properties` | `controlId` | Dumps all Avalonia properties, values, types, and inheritance sources. | +| **Data & Bindings** | `get_data_context` | `controlId?` | Serializes the bound ViewModel properties and values to JSON. | +| | `get_binding_errors` | None | Pinpoints all broken bindings with error messages and timestamps. | +| **Visual & Styles** | `take_screenshot` | `controlId?` | Captures the entire window or a specific control as a base64 PNG. | +| | `get_applied_styles` | `controlId` | Inspects active style classes, pseudo-classes (`:pointerover`), and setters. | +| | `get_resources` | `controlId?` | Inspects resource dictionaries (colors, templates, brushes, converters). | +| | `get_focused_element` | None | Returns the currently focused control and tab index. | +| **Interaction** | `click_control` | `controlId` | Simulates click events and triggers bound `ICommand` handlers. | +| | `input_text` | `controlId`, `text` | Types text into `TextBox` or editable controls. | +| | `set_property` | `controlId`, `propertyName`, `value` | Mutates control properties at runtime to test layout fixes live. | +| **Discovery** | `discover_apps` | None | Discovers running Avalonia apps and their PIDs. | + +--- + +## 4. Collaborative Bug-Hunting Workflow + +```mermaid +sequenceDiagram + autonumber + actor User as Developer + participant App as Avalonia App (Debug) + participant MCP as avaloniamcp Server + participant Agent as Antigravity AI + + Agent->>App: Launch via `dotnet run -c Debug` + App->>MCP: Establish named pipe (avalonia-mcp-{pid}) + User->>App: Interacts with UI (browsing, clicking, filtering) + User->>Agent: Reports issue: "CivitAI starter models didn't filter by LoRA" + Agent->>MCP: Call `get_binding_errors` & `get_data_context` + MCP->>App: Query UI thread dispatch + App-->>MCP: Returns DataContext state & binding status + MCP-->>Agent: JSON snapshot of ViewModel properties + Agent->>Agent: Analyzes discrepancy in ViewModel / XAML + Agent->>User: Explains root cause & proposes code fix + Agent->>App: Applies code patch & verifies tests + User->>App: Validates fix live in application +``` + +### Collaborative Steps +1. **Launch**: App is compiled and launched in `Debug` configuration. The desktop window appears on the user's desktop, and `avaloniamcp` automatically binds to the process. +2. **Explore**: User browses features (Hugging Face hub, CivitAI filters, Ollama library, floating documentation overlay, Can I Run It, settings). +3. **Report**: When an unexpected UI behavior occurs, the user describes the symptom or presses F12 to inspect the element name. +4. **Inspect**: Antigravity uses `get_binding_errors`, `get_data_context`, and `get_visual_tree` to see the exact runtime values and state without guesswork. +5. **Fix & Verify**: Antigravity writes the fix, runs `dotnet test`, `npm run lint`, and `npx tsc --noEmit`, and the user confirms the resolution. + +--- + +## 5. Security & Isolation Considerations + +- `AvaloniaMcp.Diagnostics` and `Avalonia.Diagnostics` are strictly conditioned on `'$(Configuration)' == 'Debug'`. +- Production/Release builds do not include the diagnostic bridge, avoiding any attack surface, binary bloat, or unintended named pipes. +- Named pipes only bind to local loopback/OS IPC (`%TEMP%/avalonia-mcp/`), accessible only by the current logged-in user. + +--- + +## 6. Verification Plan + +1. **Package Verification**: + - `LocalLLMServerManager.csproj` references `AvaloniaMcp.Diagnostics` (v0.4.0) under `Condition="'$(Configuration)' == 'Debug'"`. + - `Program.cs` includes `#if DEBUG` `.UseMcpDiagnostics()` `#endif`. +2. **Tool Verification**: + - `dotnet tool install -g avaloniamcp` executes cleanly. + - Running `dotnet avalonia-mcp cli discover_apps` detects the running process. + - `get_binding_errors` and `get_data_context` return valid JSON. +3. **Build & Test Verification**: + - Full test suite (`dotnet test`) passes with 0 errors. + - Release build verification (`dotnet build -c Release`) ensures diagnostic packages are stripped from Release builds. diff --git a/docs/superpowers/specs/2026-09-18-floating-docs-and-hf-hub-design.md b/docs/superpowers/specs/2026-09-18-floating-docs-and-hf-hub-design.md new file mode 100644 index 0000000..bff8b18 --- /dev/null +++ b/docs/superpowers/specs/2026-09-18-floating-docs-and-hf-hub-design.md @@ -0,0 +1,64 @@ +# Floating Documentation Window & Hugging Face Hub Overhaul Design Specification + +## 1. Overview & Objective + +This design addresses two key user experience improvements in **Local LLM Server Manager**: +1. **Floating / Standalone Documentation Window**: Allow users to keep procedural guides visible in a floating, movable window or overlay while simultaneously operating other tabs (Workflows, Models, Settings) without switching back and forth, including smart step-level tab navigation. +2. **Hugging Face Hub Overhaul**: Fix the initial empty state by automatically loading curated trending/starter models on open, implement clear, high-contrast active/inactive visual styling for modality and hardware filter toggles, and verify filter tag mappings. + +--- + +## 2. Component 1: Floating In-App Documentation System + +### 2.1 Problem +Currently, in-app documentation is confined to a full-screen tab (`📖 Documentation`). When following instructions to generate video, download models, or configure settings, the user must constantly toggle between the documentation tab and the target workspace tab. + +### 2.2 Architecture & Hybrid Layout +* **Desktop Native Window (`DocumentationWindow.axaml`)**: + * Can be popped out into a standalone OS window that can be dragged to a secondary monitor. + * Includes a **Pin / Always-on-Top** toggle (`Topmost="True"`). + * Uses the application's dark theme palette and compact dimensions (420x620). +* **In-App Draggable Floating HUD (`DocumentationFloatingOverlay.axaml`)**: + * Lives inside `MainView.axaml` on an overlay layer (`ZIndex="100"`). + * Can be dragged anywhere across the main window canvas via pointer drag on its titlebar. + * Supports a **Minimize to Pill** mode (collapses to a compact 36px floating badge showing current step title and quick Next/Prev buttons). + * Works identically across both Desktop and WebAssembly (WASM). +* **Smart Step Navigation**: + * `DocStep` is extended with an optional `TargetTab` property (`Models = 0`, `Workflows = 1`, `HardwareFit = 2`, `Copilot = 3`, `Documentation = 4`, `Settings = 5`). + * Each step card in the floating guide features a **"Jump to Tab"** button (`🚀`). Clicking it updates `MainViewModel.SelectedTabIndex` so the target workspace immediately appears behind the guide. + +--- + +## 3. Component 2: Hugging Face Hub Overhaul + +### 3.1 Initial Model Loading +* **Problem**: Opening the Hugging Face Hub currently shows an empty list with a "No models found" message because search is never invoked on startup. +* **Solution**: + * `HuggingFaceSearchViewModel` triggers an initial automated query on first load if results are empty. + * The initial query fetches top curated trending and high-utility starter models (`Qwen/Qwen2.5-Coder-7B-Instruct-GGUF`, `meta-llama/Llama-3.2-3B-Instruct-GGUF`, `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B-GGUF`, `Wan-AI/Wan2.1-T2V-14B`, `hexgrad/Kokoro-82M`). + * Live Can I Run It hardware fit badges are computed immediately against the user's GPU VRAM and system RAM. + +### 3.2 Toggle Button Styling & Visual Clarity +* **Problem**: Modality buttons (`Text`, `Image`, `Audio`, `Video`, `3D`) and Hardware Fit buttons (`🟢 Full VRAM`, `🟡 Partial Offload`, etc.) are plain flat buttons with zero visual indicator showing whether they are currently active/included or disabled. +* **Solution**: + * Implement distinct visual states: + * **Active Modality State**: Solid vibrant background (`#2563EB` for inputs, `#8B5CF6` for outputs), bright white text, bold font, subtle glow border, and an active checkmark (`✓`). + * **Inactive Modality State**: Dark muted background (`#1E293B`), dim text (`#64748B`), 50% opacity, standard border. + * **Active Hardware Fit State**: Full color badge with solid pill background and border. + * **Inactive Hardware Fit State**: 35% opacity, desaturated muted background, and a strikethrough or dim icon. + +### 3.3 Filter Logic Verification +* Ensure `ToggleInputModality`, `ToggleOutputModality`, and `ToggleFitVerdict` immediately trigger search and local filtering without getting out of sync. +* Ensure pipeline tags (`image-to-video`, `text-to-video`, `text-to-image`, `text-generation`, `automatic-speech-recognition`, `text-to-speech`) map accurately to Hugging Face API filters. + +--- + +## 4. Verification & Testing + +* **Unit Tests**: + * `DocumentationViewModelTests`: Verify step navigation, pop-out state toggle, and `TargetTab` properties. + * `HuggingFaceSearchViewModelTests`: Verify initial model loading, modality toggle synchronization, and hardware filter states. +* **Tooling Checks**: + * `npm run lint`: 0 ESLint errors. + * `npx tsc --noEmit`: 0 TypeScript errors. + * `dotnet test`: 100% pass across all unit and integration tests.