391 lines
17 KiB
Plaintext
391 lines
17 KiB
Plaintext
@using RobotNet10.NavigationTune.Shared.Models
|
|
@using RobotNet10.NavigationTuneUI.Services
|
|
@using RobotNet10.NavigationTuneUI.Helpers
|
|
@inject TuningApiService ApiService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudCard Style="width: 100%; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box;">
|
|
<MudCardHeader Style="flex-shrink: 0; box-sizing: border-box;">
|
|
<CardHeaderContent>
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
<MudIcon Icon="@Icons.Material.Filled.AutoFixHigh" />
|
|
<MudText Typo="Typo.h6">Tuning Advisor</MudText>
|
|
@if (Report != null && Report.HasCriticalIssues)
|
|
{
|
|
<MudChip T="string" Color="Color.Error" Size="Size.Small">Critical</MudChip>
|
|
}
|
|
else if (Report != null && Report.HighPriorityCount > 0)
|
|
{
|
|
<MudChip T="string" Color="Color.Warning" Size="Size.Small">@Report.HighPriorityCount High</MudChip>
|
|
}
|
|
else if (Report != null && Report.Suggestions.Count > 0)
|
|
{
|
|
<MudChip T="string" Color="Color.Info" Size="Size.Small">@Report.Suggestions.Count Suggestions</MudChip>
|
|
}
|
|
</MudStack>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
@if (Report != null)
|
|
{
|
|
<MudTooltip Text="Re-analyze">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
|
OnClick="HandleReAnalyze"
|
|
Disabled="@_isProcessing" />
|
|
</MudTooltip>
|
|
}
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
|
|
<MudCardContent Style="flex: 1; overflow-y: auto; overflow-x: hidden; box-sizing: border-box;">
|
|
@if (Report == null)
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
|
Chưa có dữ liệu phân tích. Chạy test để nhận gợi ý tuning tự động.
|
|
</MudText>
|
|
}
|
|
else
|
|
{
|
|
@* Overall Assessment *@
|
|
<MudAlert Severity="@GetOverallSeverity()" Dense="true" Class="mb-3">
|
|
@Report.OverallAssessment
|
|
</MudAlert>
|
|
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-2">
|
|
Controller: @Report.ControllerType | Samples: @Report.TelemetrySamplesAnalyzed | @Report.GeneratedAt.ToString("HH:mm:ss")
|
|
</MudText>
|
|
|
|
@* Detected Patterns *@
|
|
@if (Report.DetectedPatterns.Count > 0)
|
|
{
|
|
<MudExpansionPanels Elevation="0" Class="mb-3">
|
|
<MudExpansionPanel Text="@($"Detected Patterns ({Report.DetectedPatterns.Count})")"
|
|
Expanded="false"
|
|
Dense="true">
|
|
@foreach (var pattern in Report.DetectedPatterns.OrderByDescending(p => p.Severity))
|
|
{
|
|
<MudAlert Severity="@GetPatternSeverity(pattern.Severity)"
|
|
Dense="true" Class="mb-1" NoIcon="false">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
<MudChip T="string" Size="Size.Small"
|
|
Color="@GetPatternChipColor(pattern.Severity)">
|
|
@pattern.Category
|
|
</MudChip>
|
|
<MudText Typo="Typo.body2">@pattern.Description</MudText>
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
(Severity: @pattern.Severity.ToString("F2"))
|
|
</MudText>
|
|
</MudStack>
|
|
</MudAlert>
|
|
}
|
|
</MudExpansionPanel>
|
|
</MudExpansionPanels>
|
|
}
|
|
|
|
@* Suggestions Table *@
|
|
@if (Report.Suggestions.Count > 0)
|
|
{
|
|
<MudText Typo="Typo.subtitle2" Class="mb-2">Suggestions</MudText>
|
|
<MudTable Items="@Report.Suggestions"
|
|
Dense="true"
|
|
Hover="true"
|
|
Bordered="false"
|
|
Striped="true"
|
|
Elevation="0"
|
|
T="TuningSuggestion">
|
|
<HeaderContent>
|
|
<MudTh Style="width: 40px;"></MudTh>
|
|
<MudTh>Priority</MudTh>
|
|
<MudTh>Parameter</MudTh>
|
|
<MudTh Style="text-align: right;">Current</MudTh>
|
|
<MudTh Style="text-align: center;"></MudTh>
|
|
<MudTh Style="text-align: right;">Suggested</MudTh>
|
|
<MudTh Style="text-align: right;">Change</MudTh>
|
|
<MudTh Style="text-align: center;">Confidence</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>
|
|
<MudCheckBox T="bool"
|
|
Value="@IsSelected(context)"
|
|
ValueChanged="@(v => ToggleSelection(context, v))"
|
|
Dense="true"
|
|
Color="Color.Primary" />
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudChip T="string" Size="Size.Small" Color="@GetPriorityColor(context.Priority)">
|
|
@context.Priority
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudTooltip Text="@context.Reason">
|
|
<MudText Typo="Typo.body2">@context.ParameterDisplayName</MudText>
|
|
</MudTooltip>
|
|
</MudTd>
|
|
<MudTd Style="text-align: right;">
|
|
<MudText Typo="Typo.body2">@context.CurrentValue.ToString("F4")</MudText>
|
|
</MudTd>
|
|
<MudTd Style="text-align: center;">
|
|
<MudIcon Icon="@Icons.Material.Filled.ArrowForward" Size="Size.Small" />
|
|
</MudTd>
|
|
<MudTd Style="text-align: right;">
|
|
<MudText Typo="Typo.body2" Color="Color.Primary">
|
|
<b>@context.SuggestedValue.ToString("F4")</b>
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd Style="text-align: right;">
|
|
<MudText Typo="Typo.body2"
|
|
Color="@(context.ChangePercent > 0 ? Color.Success : Color.Error)">
|
|
@(context.ChangePercent > 0 ? "+" : "")@context.ChangePercent.ToString("F1")%
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd Style="text-align: center;">
|
|
<MudProgressLinear Value="@(context.Confidence * 100)"
|
|
Color="@GetConfidenceColor(context.Confidence)"
|
|
Style="width: 60px; height: 8px; display: inline-block;" />
|
|
<MudText Typo="Typo.caption">@((context.Confidence * 100).ToString("F0"))%</MudText>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<ChildRowContent>
|
|
<MudGrid Class="pa-2">
|
|
<MudItem xs="12" sm="6">
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">Reason</MudText>
|
|
<MudText Typo="Typo.body2">@context.Reason</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6">
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">Expected Impact</MudText>
|
|
<MudText Typo="Typo.body2">@context.ExpectedImpact</MudText>
|
|
</MudItem>
|
|
@if (context.RelatedPatterns.Count > 0)
|
|
{
|
|
<MudItem xs="12">
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">Related Patterns</MudText>
|
|
<MudStack Row="true" Spacing="1">
|
|
@foreach (var pattern in context.RelatedPatterns)
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined">@pattern</MudChip>
|
|
}
|
|
</MudStack>
|
|
</MudItem>
|
|
}
|
|
</MudGrid>
|
|
</ChildRowContent>
|
|
</MudTable>
|
|
|
|
@* Action Buttons *@
|
|
<MudStack Row="true" Spacing="2" Class="mt-3" Justify="Justify.FlexEnd">
|
|
<MudButton Variant="Variant.Text"
|
|
Size="Size.Small"
|
|
OnClick="SelectAll">
|
|
Select All
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Text"
|
|
Size="Size.Small"
|
|
OnClick="DeselectAll">
|
|
Deselect All
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Preview"
|
|
OnClick="HandleApplySelected"
|
|
Disabled="@(_selectedIds.Count == 0 || _isProcessing)"
|
|
Size="Size.Small">
|
|
Apply Selected (Preview)
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Save"
|
|
OnClick="HandleApplyAndSave"
|
|
Disabled="@(_selectedIds.Count == 0 || _isProcessing)"
|
|
Size="Size.Small">
|
|
Apply & Save as New
|
|
</MudButton>
|
|
</MudStack>
|
|
}
|
|
else
|
|
{
|
|
<MudAlert Severity="Severity.Success" Dense="true">
|
|
Không phát hiện vấn đề. Tham số hiện tại hoạt động tốt.
|
|
</MudAlert>
|
|
}
|
|
}
|
|
|
|
@if (_isProcessing)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mt-2" />
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
|
|
@code {
|
|
[Parameter] public TuningReport? Report { get; set; }
|
|
[Parameter] public NavigationParameterSet? CurrentParameters { get; set; }
|
|
[Parameter] public EventCallback<NavigationParameterSet> OnApplySuggestions { get; set; }
|
|
[Parameter] public EventCallback<NavigationParameterSet> OnSaveNewParameterSet { get; set; }
|
|
[Parameter] public Guid? TestRunId { get; set; }
|
|
|
|
private HashSet<Guid> _selectedIds = new();
|
|
private bool _isProcessing;
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
// Auto-select high+ priority suggestions when new report arrives
|
|
if (Report != null && _selectedIds.Count == 0)
|
|
{
|
|
_selectedIds = Report.Suggestions
|
|
.Where(s => s.Priority >= SuggestionPriority.High)
|
|
.Select(s => s.Id)
|
|
.ToHashSet();
|
|
}
|
|
}
|
|
|
|
private bool IsSelected(TuningSuggestion suggestion) => _selectedIds.Contains(suggestion.Id);
|
|
|
|
private void ToggleSelection(TuningSuggestion suggestion, bool selected)
|
|
{
|
|
if (selected)
|
|
_selectedIds.Add(suggestion.Id);
|
|
else
|
|
_selectedIds.Remove(suggestion.Id);
|
|
}
|
|
|
|
private void SelectAll()
|
|
{
|
|
if (Report != null)
|
|
_selectedIds = Report.Suggestions.Select(s => s.Id).ToHashSet();
|
|
}
|
|
|
|
private void DeselectAll() => _selectedIds.Clear();
|
|
|
|
private async Task HandleApplySelected()
|
|
{
|
|
if (Report == null || CurrentParameters == null || _selectedIds.Count == 0)
|
|
return;
|
|
|
|
_isProcessing = true;
|
|
try
|
|
{
|
|
var modified = await ApiService.ApplySuggestionsAsync(
|
|
CurrentParameters.Id, _selectedIds.ToList(), Report);
|
|
|
|
if (modified != null)
|
|
{
|
|
await OnApplySuggestions.InvokeAsync(modified);
|
|
Snackbar.Add($"Đã áp dụng {_selectedIds.Count} gợi ý (preview)", Severity.Success);
|
|
}
|
|
}
|
|
catch (ApiException ex)
|
|
{
|
|
Snackbar.Add($"Lỗi áp dụng gợi ý: {ex.Message}", Severity.Error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Lỗi: {ErrorHelper.GetErrorMessage(ex)}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_isProcessing = false;
|
|
}
|
|
}
|
|
|
|
private async Task HandleApplyAndSave()
|
|
{
|
|
if (Report == null || CurrentParameters == null || _selectedIds.Count == 0)
|
|
return;
|
|
|
|
_isProcessing = true;
|
|
try
|
|
{
|
|
var saved = await ApiService.ApplyAndSaveSuggestionsAsync(
|
|
CurrentParameters.Id, _selectedIds.ToList(), Report);
|
|
|
|
if (saved != null)
|
|
{
|
|
await OnSaveNewParameterSet.InvokeAsync(saved);
|
|
Snackbar.Add($"Đã lưu bộ thông số mới: {saved.Name}", Severity.Success);
|
|
}
|
|
}
|
|
catch (ApiException ex)
|
|
{
|
|
Snackbar.Add($"Lỗi lưu bộ thông số: {ex.Message}", Severity.Error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Lỗi: {ErrorHelper.GetErrorMessage(ex)}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_isProcessing = false;
|
|
}
|
|
}
|
|
|
|
private async Task HandleReAnalyze()
|
|
{
|
|
if (TestRunId == null)
|
|
return;
|
|
|
|
_isProcessing = true;
|
|
try
|
|
{
|
|
var newReport = await ApiService.AnalyzeTestRunAsync(TestRunId.Value);
|
|
if (newReport != null)
|
|
{
|
|
Report = newReport;
|
|
_selectedIds = newReport.Suggestions
|
|
.Where(s => s.Priority >= SuggestionPriority.High)
|
|
.Select(s => s.Id)
|
|
.ToHashSet();
|
|
Snackbar.Add("Phân tích lại hoàn tất", Severity.Success);
|
|
}
|
|
}
|
|
catch (ApiException ex)
|
|
{
|
|
Snackbar.Add($"Lỗi phân tích: {ex.Message}", Severity.Error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Lỗi: {ErrorHelper.GetErrorMessage(ex)}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_isProcessing = false;
|
|
}
|
|
}
|
|
|
|
private Severity GetOverallSeverity()
|
|
{
|
|
if (Report == null) return Severity.Info;
|
|
if (Report.HasCriticalIssues) return Severity.Error;
|
|
if (Report.HighPriorityCount > 0) return Severity.Warning;
|
|
if (Report.Suggestions.Count > 0) return Severity.Info;
|
|
return Severity.Success;
|
|
}
|
|
|
|
private static Severity GetPatternSeverity(double severity)
|
|
{
|
|
if (severity >= 0.7) return Severity.Error;
|
|
if (severity >= 0.4) return Severity.Warning;
|
|
return Severity.Info;
|
|
}
|
|
|
|
private static Color GetPatternChipColor(double severity)
|
|
{
|
|
if (severity >= 0.7) return Color.Error;
|
|
if (severity >= 0.4) return Color.Warning;
|
|
return Color.Info;
|
|
}
|
|
|
|
private static Color GetPriorityColor(SuggestionPriority priority) => priority switch
|
|
{
|
|
SuggestionPriority.Critical => Color.Error,
|
|
SuggestionPriority.High => Color.Warning,
|
|
SuggestionPriority.Medium => Color.Info,
|
|
_ => Color.Default
|
|
};
|
|
|
|
private static Color GetConfidenceColor(double confidence)
|
|
{
|
|
if (confidence >= 0.7) return Color.Success;
|
|
if (confidence >= 0.4) return Color.Warning;
|
|
return Color.Error;
|
|
}
|
|
}
|