< Summary

Information
Class: UIBlazor.Components.AiChatInput
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Components/AiChatInput.razor
Tag: 71_26091983037
Line coverage
96%
Covered lines: 150
Uncovered lines: 6
Coverable lines: 156
Total lines: 363
Line coverage: 96.1%
Branch coverage
90%
Covered branches: 150
Total branches: 165
Branch coverage: 90.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildRenderTree(...)91.66%1212100%
get_SendMessage()100%11100%
get_Cancel()100%11100%
get_IsLoading()100%11100%
get_ChatService()100%11100%
get_VsCodeContextService()100%11100%
get_JsRuntime()100%11100%
get_VsBridge()100%11100%
get_CommonSettingsProvider()100%11100%
.ctor()100%11100%
get_AppModeValues()100%11100%
get_ModePlaceholder()100%44100%
CanSend()100%44100%
HandleInputAsync()75%1616100%
AddFileTokenAsync()100%22100%
RemoveTokenAsync()100%11100%
FocusInputAsync()100%1160%
OnKeyDown()80.76%262695%
OnSendClick()100%1010100%
GetFileIcon(...)97.46%7979100%
OnInitialized()100%11100%
HandlePropertyChanged(...)80%1010100%
OpenRulesAsync()100%11100%
OpenSkillsAsync()100%11100%
Dispose()100%11100%
OnModeChanged()100%210%

File(s)

/home/runner/work/InvAit/InvAit/UIBlazor/Components/AiChatInput.razor

#LineLine coverage
 1@using System.ComponentModel
 2@implements IDisposable
 3
 4<div class="chat-input-wrapper">
 5    @* Контейнер для чипов (над полем ввода) *@
 1506    @if (_tokens.OfType<FileToken>().Any())
 7    {
 8        <div class="chips-list">
 1129            @foreach (var token in _tokens.OfType<FileToken>())
 10            {
 11                <FileChip Token="token"
 12                            Icon="@GetFileIcon(token.FileName)"
 13                            OnRemoveClick="RemoveTokenAsync" />
 14            }
 15        </div>
 16    }
 17    <div class="input-container">
 18        @* Поле ввода *@
 19        <RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.SpaceBetween" AlignItems="Align
 6020            <textarea @ref="_textareaRef"
 6021                      rows="1"
 6022                      class="text-input"
 6023                      placeholder="@ModePlaceholder"
 6024                      @bind="_currentInput"
 6025                      @oninput="HandleInputAsync"
 26                      @onkeydown="OnKeyDown"
 27                      @onkeydown:preventDefault="_shouldPreventDefault"
 28                      disabled="@IsLoading"/>
 29
 30            <div class="misc">
 31                <RadzenToggleButton @bind-Value="CommonSettingsProvider.Current.SendCurrentFile"
 32                                    ButtonStyle="ButtonStyle.Base"
 33                                    Size="ButtonSize.Small"
 34                                    Variant="Variant.Flat"
 35                                    title="@SharedResource.SendCurrentFile">
 36                    <i class="fa-solid fa-file"></i>
 37                </RadzenToggleButton>
 38                <RadzenToggleButton @bind-Value="CommonSettingsProvider.Current.SendSolutionsStricture"
 39                                    ButtonStyle="ButtonStyle.Base"
 40                                    Size="ButtonSize.Small"
 41                                    Variant="Variant.Flat"
 42                                    title="@SharedResource.SendSolutionsStricture">
 43                    <i class="fa-solid fa-folder-tree"></i>
 44                </RadzenToggleButton>
 45            </div>
 46        </RadzenStack>
 47
 48        @* Нижняя панель с кнопками *@
 49        <div class="input-footer">
 50            <RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.SpaceBetween" AlignItems="A
 51                <RadzenStack Orientation="Orientation.Horizontal" Gap="8px" AlignItems="AlignItems.Center">
 52                    <RadzenDropDown TValue="AppMode" class="mode-dropdown" Data="@AppModeValues" @bind-Value="@ChatServi
 53                    <ModelSelector class="model-dropdown" />
 54                </RadzenStack>
 55
 56                <RadzenStack Orientation="Orientation.Horizontal" Gap="4px" JustifyContent="JustifyContent.End" AlignIte
 57                    <RadzenButton Icon="gavel" ButtonStyle="ButtonStyle.Base" Variant="Variant.Text" Size="ButtonSize.Sm
 58                    <RadzenButton Icon="extension" ButtonStyle="ButtonStyle.Base" Variant="Variant.Text" Size="ButtonSiz
 59
 60                    <RadzenStack Orientation="Orientation.Horizontal" Gap="8px" JustifyContent="JustifyContent.End" Alig
 61                        <RadzenButton Icon="send"
 62                                      ButtonStyle="ButtonStyle.Base"
 63                                      Variant="Variant.Text"
 64                                      Visible="@(!IsLoading)"
 65                                      Disabled="@(!CanSend())"
 66                                      Click="@OnSendClick"
 67                                      title="@SharedResource.SendMessage"
 68                                      class="rz-chat-send-btn" />
 69                        <RadzenButton Icon="cancel"
 70                                      ButtonStyle="ButtonStyle.Danger"
 71                                      Variant="Variant.Text"
 72                                      Visible="@IsLoading"
 73                                      Click="@Cancel.InvokeAsync"
 74                                      title="@SharedResource.Cancel"
 75                                      class="rz-chat-cancel-btn" />
 76                    </RadzenStack>
 77                </RadzenStack>
 78            </RadzenStack>
 79        </div>
 80    </div>
 81
 82    @* Всплывающее меню с файлами *@
 15083    @if (_filteredFiles.Any())
 84    {
 85        <div class="hints-menu">
 86            <ul class="hints-list">
 28287                @for (var i = 0; i < _filteredFiles.Count; i++)
 88                {
 10189                    var index = i;
 10190                    var file = _filteredFiles[i];
 10191                    var isSelected = index == _selectedIndex;
 10192                    var fileName = Path.GetFileName(file.Replace('\\', '/'));
 10193                    var fileDir = Path.GetDirectoryName(file.Replace('\\', '/')) ?? "";
 10194                    var icon = GetFileIcon(fileName);
 95                    <li class="hint-item @(isSelected ? "selected" : "")"
 096                        @onclick="() => AddFileTokenAsync(file)">
 10197                        <span class="file-icon">@icon</span>
 10198                        <span class="file-name">@fileName</span>
 10199                        <span class="file-path">@fileDir</span>
 100                    </li>
 101                }
 102            </ul>
 103        </div>
 104    }
 105</div>
 106
 107@code {
 12108    [Parameter] public EventCallback<string> SendMessage { get; set; }
 151109    [Parameter] public EventCallback<string> Cancel { get; set; }
 614110    [Parameter] public bool IsLoading { get; set; }
 111
 780112    [Inject] private IChatService ChatService { get; set; } = null!;
 93113    [Inject] private IVsCodeContextService VsCodeContextService { get; set; } = null!;
 60114    [Inject] private IJSRuntime JsRuntime { get; set; } = null!;
 64115    [Inject] private IVsBridge VsBridge { get; set; } = null!;
 780116    [Inject] private ICommonSettingsProvider CommonSettingsProvider { get; set; } = null!;
 117
 118    private ElementReference _textareaRef;
 60119    private readonly List<InputToken> _tokens = [];
 60120    private string _currentInput = string.Empty;
 60121    private List<string> _filteredFiles = [];
 122    private int _selectedIndex;
 123    private bool _shouldPreventDefault;
 124
 210125    private List<AppMode> AppModeValues { get; } = [.. Enum.GetValues<AppMode>()];
 126
 150127    private string ModePlaceholder => ChatService.Session.Mode switch
 150128    {
 1129        AppMode.Plan => SharedResource.InputPlaceholderPlan,
 4130        AppMode.Agent => SharedResource.InputPlaceholderAgent,
 145131        _ => SharedResource.InputPlaceholder
 150132    };
 133
 134    private bool CanSend()
 135    {
 156136        return !IsLoading && (!string.IsNullOrWhiteSpace(_currentInput) || _tokens.Any());
 137    }
 138
 139    private async Task HandleInputAsync(ChangeEventArgs e)
 140    {
 41141        _currentInput = e.Value?.ToString() ?? string.Empty;
 142
 143        // Проверяем, есть ли @ в конце
 41144        if (_currentInput.EndsWith(" @"))
 145        {
 146            // Показываем все файлы
 12147            var source = VsCodeContextService.CurrentContext?.SolutionFiles ??
 12148                         ["  📄 test1.cs", "    📄 C:\\asdtes\\t2aksjhdgj\\ahsghd hjagshjdg\\ajhsgdahjkg.cs"];
 58149            _filteredFiles = source.Where(f => f.Contains(VsCodeContext.FilePrefix)).Take(10).ToList();
 12150            _selectedIndex = 0;
 151        }
 29152        else if (_currentInput.Contains('@'))
 153        {
 154            // Ищем текст после последнего @
 21155            var lastAtIndex = _currentInput.LastIndexOf('@');
 21156            var query = _currentInput.Substring(lastAtIndex + 1);
 157
 21158            var source = VsCodeContextService.CurrentContext?.SolutionFiles ??
 21159                         ["  📄 test1.cs", "     📄 C:\\asdtes\\t2aksjhdgj\\ahsghd hjagshjdg\\ajhsgdahjkg.cs"];
 160
 21161            _filteredFiles = source
 33162                .Where(f => f.Contains(VsCodeContext.FilePrefix) && f.Contains(query, StringComparison.OrdinalIgnoreCase
 21163                .Take(10)
 21164                .ToList();
 21165            _selectedIndex = 0;
 166        }
 167        else
 168        {
 8169            _filteredFiles.Clear();
 170        }
 41171    }
 172
 173    private async Task AddFileTokenAsync(string filePath)
 174    {
 25175        filePath = filePath.TrimStart($" {VsCodeContext.FilePrefix}".ToCharArray());
 25176        var fileName = Path.GetFileName(filePath.Replace('\\', '/'));
 177
 178        // Удаляем @ и текст после него из currentInput
 25179        var lastAtIndex = _currentInput.LastIndexOf('@');
 25180        if (lastAtIndex >= 0)
 181        {
 25182            _currentInput = _currentInput.Substring(0, lastAtIndex);
 183        }
 184
 185        // Добавляем токен файла
 25186        _tokens.Add(new FileToken
 25187        {
 25188            FilePath = filePath,
 25189            FileName = fileName
 25190        });
 191
 25192        _filteredFiles.Clear();
 25193        _selectedIndex = 0;
 194
 195        // Фокус обратно на input
 25196        await FocusInputAsync();
 25197    }
 198
 199    private async Task RemoveTokenAsync(FileToken token)
 200    {
 1201        _tokens.Remove(token);
 1202        await FocusInputAsync();
 1203    }
 204
 205    private async Task FocusInputAsync()
 206    {
 207        try
 208        {
 26209            await _textareaRef.FocusAsync();
 26210        }
 0211        catch
 212        {
 213            // ignored
 0214        }
 26215    }
 216
 217    private async Task OnKeyDown(KeyboardEventArgs e)
 218    {
 39219        if (_filteredFiles.Any())
 220        {
 33221            switch (e.Key)
 222            {
 223                case "ArrowDown":
 6224                    _selectedIndex = (_selectedIndex + 1) % _filteredFiles.Count;
 6225                    _shouldPreventDefault = true;
 6226                    return;
 227                case "ArrowUp":
 1228                    _selectedIndex = (_selectedIndex - 1 + _filteredFiles.Count) % _filteredFiles.Count;
 1229                    _shouldPreventDefault = true;
 1230                    return;
 231                case "Tab":
 232                case "Enter":
 233                    {
 25234                        _shouldPreventDefault = true;
 25235                        if (_selectedIndex >= 0 && _selectedIndex < _filteredFiles.Count)
 236                        {
 25237                            await AddFileTokenAsync(_filteredFiles[_selectedIndex]);
 238                        }
 25239                        return;
 240                    }
 241                case "Escape":
 1242                    _shouldPreventDefault = true;
 1243                    _filteredFiles.Clear();
 1244                    return;
 245            }
 246        }
 247
 248        // Enter без Shift/Ctrl — отправка
 6249        if (e is { Key: "Enter", CtrlKey: false, ShiftKey: false } && !_filteredFiles.Any())
 250        {
 6251            _shouldPreventDefault = true;
 6252            await OnSendClick();
 253        }
 254        else
 255        {
 0256            _shouldPreventDefault = false;
 257        }
 39258    }
 259
 260    private async Task OnSendClick()
 261    {
 7262        if (!CanSend()) return;
 263
 264        // Загружаем содержимое файлов перед отправкой
 5265        var fileIndex = 0;
 14266        foreach (var fileToken in _tokens.OfType<FileToken>())
 267        {
 2268            if (string.IsNullOrEmpty(fileToken.FileContent))
 269            {
 2270                var readFileTool = await VsBridge.ExecuteToolAsync(
 2271                    BuiltInToolEnum.ReadFiles,
 2272                    new Dictionary<string, object> { { $"file{++fileIndex}", new ReadFileParams { Name = fileToken.FileP
 273
 2274                fileToken.FileContent = readFileTool.Result;
 275            }
 2276        }
 277
 278        // Формируем сообщение для отправки
 5279        var messageBuilder = new StringBuilder();
 280
 281        // Добавляем текущий ввод
 5282        if (!string.IsNullOrWhiteSpace(_currentInput))
 283        {
 5284            messageBuilder.AppendLine(_currentInput.Trim());
 285        }
 286
 287        // Добавляем файлы с их содержимым
 14288        foreach (var fileToken in _tokens.OfType<FileToken>())
 289        {
 2290            messageBuilder.Append(fileToken.FileContent);
 291        }
 292
 5293        var message = messageBuilder.ToString();
 294
 295        // Очищаем состояние
 5296        _currentInput = string.Empty;
 5297        _tokens.Clear();
 5298        _filteredFiles.Clear();
 299
 5300        await SendMessage.InvokeAsync(message);
 6301    }
 302
 303    private string GetFileIcon(string fileName)
 304    {
 129305        var extension = Path.GetExtension(fileName.Replace('\\', '/')).ToLowerInvariant();
 129306        return extension switch
 129307        {
 37308            ".cs" => "📄",
 22309            ".razor" => "⚡",
 2310            ".html" => "🌐",
 2311            ".css" => "🎨",
 2312            ".js" => "📜",
 22313            ".json" => "📋",
 2314            ".xml" => "📋",
 2315            ".txt" => "📝",
 22316            ".md" => "📖",
 4317            ".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" => "🖼️",
 4318            ".dll" or ".exe" => "⚙️",
 2319            ".config" => "🔧",
 4320            ".csproj" or ".sln" => "📦",
 2321            _ => "📄"
 129322        };
 323    }
 324
 325    protected override void OnInitialized()
 326    {
 60327        base.OnInitialized();
 60328        ChatService.SessionChanged += HandlePropertyChanged;
 60329        CommonSettingsProvider.Current.PropertyChanged += HandlePropertyChanged;
 60330    }
 331
 332    private void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
 333    {
 4334        if (e.PropertyName is nameof(UIBlazor.Models.ConversationSession.Mode)
 4335            or nameof(UIBlazor.Models.ConversationSession)
 4336            or nameof(CommonOptions.SendCurrentFile)
 4337            or nameof(CommonOptions.SendSolutionsStricture))
 338        {
 4339            InvokeAsync(StateHasChanged);
 340        }
 4341    }
 342
 343    private async Task OpenRulesAsync()
 344    {
 1345        await VsBridge.ExecuteToolAsync(BasicEnum.OpenFile, new Dictionary<string, object> { { "param1", ".agents/rules.
 1346    }
 347
 348    private async Task OpenSkillsAsync()
 349    {
 1350        await VsBridge.ExecuteToolAsync(BasicEnum.OpenFolder, new Dictionary<string, object> { { "param1", ".agents/skil
 1351    }
 352
 353    public void Dispose()
 354    {
 60355        ChatService.SessionChanged -= HandlePropertyChanged;
 60356        CommonSettingsProvider.Current.PropertyChanged -= HandlePropertyChanged;
 60357    }
 358
 359    private void OnModeChanged()
 360    {
 0361        _ = ChatService.SaveSessionAsync();
 0362    }
 363}