< Summary

Information
Class: UIBlazor.Components.AiChatInput
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Components/AiChatInput.razor
Tag: 14_22728831704
Line coverage
0%
Covered lines: 0
Uncovered lines: 167
Coverable lines: 167
Total lines: 370
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 159
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildRenderTree(...)0%156120%
get_SendMessage()100%210%
get_Cancel()100%210%
get_IsLoading()100%210%
get_ChatService()100%210%
get_VsCodeContextService()100%210%
get_JS()100%210%
get_VsBridge()100%210%
.ctor()100%210%
get_AppModeValues()100%210%
get_ModePlaceholder()0%2040%
get_CurrentMode()0%620%
set_CurrentMode(...)0%620%
CanSend()0%2040%
HandleInput()0%272160%
AddFileToken()0%620%
RemoveToken()100%210%
FocusInput()100%210%
OnKeyDown()0%702260%
OnSendClick()0%156120%
GetFileIcon(...)0%6320790%
Clear()100%210%
OnInitialized()100%210%
HandleModeSwitched(...)100%210%
HandleSessionChanged()100%210%
HandleProfileChanged()100%210%
OpenRules()100%210%
OpenSkills()100%210%
Dispose()100%210%

File(s)

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

#LineLine coverage
 1@using System.ComponentModel
 2@using UIBlazor.Models
 3@inherits RadzenComponent
 4
 5<div class="chat-input-wrapper">
 6    @* Контейнер для чипов (над полем ввода) *@
 07    @if (tokens.OfType<FileToken>().Any())
 8    {
 9        <div class="chips-list">
 010            @foreach (var token in tokens.OfType<FileToken>())
 11            {
 12                <FileChip Token="token"
 13                          Icon="@GetFileIcon(token.FileName)"
 14                          OnRemoveClick="RemoveToken" />
 15            }
 16        </div>
 17    }
 18
 19    <div class="input-container">
 20        @* Поле ввода *@
 021        <textarea @ref="textareaRef"
 022                  rows="1"
 023                  class="text-input"
 024                  placeholder="@ModePlaceholder"
 025                  @bind="currentInput"
 026                  @oninput="HandleInput"
 27                  @onkeydown="OnKeyDown"
 28                  @onkeydown:preventDefault="shouldPreventDefault"
 29                  disabled="@IsLoading"></textarea>
 30
 31        @* Нижняя панель с кнопками *@
 32        <div class="input-footer">
 33            <RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.SpaceBetween" AlignItems="A
 34                <RadzenStack Orientation="Orientation.Horizontal" Gap="8px" AlignItems="AlignItems.Center">
 35                    <RadzenDropDown TValue="AppMode" class="mode-dropdown" Data="@AppModeValues" @bind-Value="@CurrentMo
 36                    <ModelSelector class="model-dropdown" />
 37                </RadzenStack>
 38
 39                <RadzenStack Orientation="Orientation.Horizontal" Gap="4px" JustifyContent="JustifyContent.End" AlignIte
 40                    <RadzenButton Icon="gavel" ButtonStyle="ButtonStyle.Base" Variant="Variant.Text" Size="ButtonSize.Sm
 41                    <RadzenButton Icon="extension" ButtonStyle="ButtonStyle.Base" Variant="Variant.Text" Size="ButtonSiz
 42
 43                    <RadzenStack Orientation="Orientation.Horizontal" Gap="8px" JustifyContent="JustifyContent.End" Alig
 44                        <RadzenButton Icon="send"
 45                                      ButtonStyle="ButtonStyle.Primary"
 46                                      Variant="Variant.Text"
 47                                      Visible="@(!IsLoading)"
 48                                      Disabled="@(!CanSend())"
 49                                      Click="@OnSendClick"
 50                                      title="@SharedResource.SendMessage"
 51                                      class="rz-chat-send-btn" />
 52                        <RadzenButton Icon="dangerous"
 53                                      ButtonStyle="ButtonStyle.Danger"
 54                                      Variant="Variant.Text"
 55                                      Visible="@IsLoading"
 56                                      Click="@Cancel.InvokeAsync"
 57                                      title="@SharedResource.Cancel"
 58                                      class="rz-chat-cancel-btn" />
 59                    </RadzenStack>
 60                </RadzenStack>
 61            </RadzenStack>
 62        </div>
 63    </div>
 64
 65    @* Всплывающее меню с файлами *@
 066    @if (filteredFiles.Any())
 67    {
 68        <div class="hints-menu">
 69            <ul class="hints-list">
 070                @for (int i = 0; i < filteredFiles.Count; i++)
 71                {
 072                    var index = i;
 073                    var file = filteredFiles[i];
 074                    var isSelected = index == selectedIndex;
 075                    var fileName = Path.GetFileName(file.Replace('\\', '/'));
 076                    var fileDir = Path.GetDirectoryName(file.Replace('\\', '/')) ?? "";
 077                    var icon = GetFileIcon(fileName);
 78                    <li class="hint-item @(isSelected ? "selected" : "")"
 079                        @onclick="() => AddFileToken(file)">
 080                        <span class="file-icon">@icon</span>
 081                        <span class="file-name">@fileName</span>
 082                        <span class="file-path">@fileDir</span>
 83                    </li>
 84                }
 85            </ul>
 86        </div>
 87    }
 88</div>
 89
 90@code {
 091    [Parameter] public EventCallback<string> SendMessage { get; set; }
 092    [Parameter] public EventCallback<string> Cancel { get; set; }
 093    [Parameter] public bool IsLoading { get; set; }
 94
 095    [Inject] private ChatService ChatService { get; set; } = null!;
 096    [Inject] private IVsCodeContextService VsCodeContextService { get; set; } = null!;
 097    [Inject] private IJSRuntime JS { get; set; } = null!;
 098    [Inject] private IVsBridge VsBridge { get; set; } = null!;
 99
 100    private ElementReference textareaRef;
 0101    private List<InputToken> tokens = new();
 0102    private string currentInput = string.Empty;
 0103    private List<string> filteredFiles = new();
 104    private int selectedIndex = 0;
 105    private bool shouldPreventDefault;
 106
 0107    private List<AppMode> AppModeValues { get; } = [.. Enum.GetValues<AppMode>()];
 108
 0109    private string ModePlaceholder => CurrentMode switch
 0110    {
 0111        AppMode.Plan => SharedResource.InputPlaceholderPlan,
 0112        AppMode.Agent => SharedResource.InputPlaceholderAgent,
 0113        _ => SharedResource.InputPlaceholder
 0114    };
 115
 116    private AppMode CurrentMode
 117    {
 0118        get => ChatService.Session?.Mode ?? AppMode.Chat;
 0119        set => ChatService.Session?.Mode = value;
 120    }
 121
 122    private bool CanSend()
 123    {
 0124        return !IsLoading && (!string.IsNullOrWhiteSpace(currentInput) || tokens.Any());
 125    }
 126
 127    private async Task HandleInput(ChangeEventArgs e)
 128    {
 0129        currentInput = e.Value?.ToString() ?? string.Empty;
 130
 131        // Автоматически изменяем высоту textarea
 0132        await JS.InvokeVoidAsync("autoResizeTextarea", textareaRef);
 133
 134        // Проверяем, есть ли @ в конце
 0135        if (currentInput.EndsWith(" @"))
 136        {
 137            // Показываем все файлы
 0138            var source = VsCodeContextService.CurrentContext?.SolutionFiles ??
 0139                         ["test1.cs", "C:\\asdtes\\t2aksjhdgj\\ahsghd hjagshjdg\\ajhsgdahjkg.cs"];
 0140            filteredFiles = source.Take(10).ToList();
 0141            selectedIndex = 0;
 142        }
 0143        else if (currentInput.Contains('@'))
 144        {
 145            // Ищем текст после последнего @
 0146            var lastAtIndex = currentInput.LastIndexOf('@');
 0147            var query = currentInput.Substring(lastAtIndex + 1);
 148
 0149            var source = VsCodeContextService.CurrentContext?.SolutionFiles ??
 0150                         ["test1.cs", "C:\\asdtes\\t2aksjhdgj\\ahsghd hjagshjdg\\ajhsgdahjkg.cs"];
 151
 0152            filteredFiles = source
 0153                .Where(f => f.Contains(query, StringComparison.OrdinalIgnoreCase))
 0154                .Take(10)
 0155                .ToList();
 0156            selectedIndex = 0;
 157        }
 158        else
 159        {
 0160            filteredFiles.Clear();
 161        }
 0162    }
 163
 164    private async Task AddFileToken(string filePath)
 165    {
 0166        var fileName = Path.GetFileName(filePath.Replace('\\', '/'));
 167
 168        // Удаляем @ и текст после него из currentInput
 0169        var lastAtIndex = currentInput.LastIndexOf('@');
 0170        if (lastAtIndex >= 0)
 171        {
 0172            currentInput = currentInput.Substring(0, lastAtIndex);
 173        }
 174
 175        // Добавляем токен файла
 0176        tokens.Add(new FileToken
 0177        {
 0178            FilePath = filePath,
 0179            FileName = fileName
 0180        });
 181
 0182        filteredFiles.Clear();
 0183        selectedIndex = 0;
 184
 185        // Фокус обратно на input
 0186        await FocusInput();
 0187    }
 188
 189    private async Task RemoveToken(FileToken token)
 190    {
 0191        tokens.Remove(token);
 0192        await FocusInput();
 0193    }
 194
 195    private async Task FocusInput()
 196    {
 197        try
 198        {
 0199            await textareaRef.FocusAsync();
 0200        }
 0201        catch { }
 0202    }
 203
 204    private async Task OnKeyDown(KeyboardEventArgs e)
 205    {
 0206        if (filteredFiles.Any())
 207        {
 0208            if (e.Key == "ArrowDown")
 209            {
 0210                selectedIndex = (selectedIndex + 1) % filteredFiles.Count;
 0211                shouldPreventDefault = true;
 0212                return;
 213            }
 0214            if (e.Key == "ArrowUp")
 215            {
 0216                selectedIndex = (selectedIndex - 1 + filteredFiles.Count) % filteredFiles.Count;
 0217                shouldPreventDefault = true;
 0218                return;
 219            }
 0220            if (e.Key == "Tab" || e.Key == "Enter")
 221            {
 0222                shouldPreventDefault = true;
 0223                if (selectedIndex >= 0 && selectedIndex < filteredFiles.Count)
 224                {
 0225                    await AddFileToken(filteredFiles[selectedIndex]);
 226                }
 0227                return;
 228            }
 0229            if (e.Key == "Escape")
 230            {
 0231                shouldPreventDefault = true;
 0232                filteredFiles.Clear();
 0233                return;
 234            }
 235        }
 236
 237        // Enter без Shift/Ctrl — отправка
 0238        if (e is { Key: "Enter", CtrlKey: false, ShiftKey: false } && !filteredFiles.Any())
 239        {
 0240            shouldPreventDefault = true;
 0241            await OnSendClick();
 242        }
 243        else
 244        {
 0245            shouldPreventDefault = false;
 246        }
 0247    }
 248
 249    private async Task OnSendClick()
 250    {
 0251        if (!CanSend()) return;
 252
 253        // Загружаем содержимое файлов перед отправкой
 0254        foreach (var fileToken in tokens.OfType<FileToken>())
 255        {
 0256            if (string.IsNullOrEmpty(fileToken.FileContent))
 257            {
 0258                var readFileTool = await VsBridge.ExecuteToolAsync(
 0259                    BuiltInToolEnum.ReadFiles,
 0260                    new Dictionary<string, object> { { "param1", fileToken.FilePath } });
 261
 0262                fileToken.FileContent = readFileTool.Result;
 263            }
 0264        }
 265
 266        // Формируем сообщение для отправки
 0267        var messageBuilder = new System.Text.StringBuilder();
 268
 269        // Добавляем текущий ввод
 0270        if (!string.IsNullOrWhiteSpace(currentInput))
 271        {
 0272            messageBuilder.Append(currentInput.Trim());
 273        }
 274
 275        // Добавляем файлы с их содержимым
 0276        foreach (var fileToken in tokens.OfType<FileToken>())
 277        {
 0278            if (messageBuilder.Length > 0)
 0279                messageBuilder.AppendLine();
 280
 0281            messageBuilder.Append(fileToken.GetLlmText());
 282        }
 283
 0284        var message = messageBuilder.ToString();
 285
 286        // Очищаем состояние
 0287        currentInput = string.Empty;
 0288        tokens.Clear();
 0289        filteredFiles.Clear();
 290
 291        // Сбрасываем высоту textarea
 0292        await JS.InvokeVoidAsync("autoResizeTextarea", textareaRef, true);
 293
 0294        await SendMessage.InvokeAsync(message);
 0295    }
 296
 297    private string GetFileIcon(string fileName)
 298    {
 0299        var extension = Path.GetExtension(fileName.Replace('\\', '/')).ToLowerInvariant();
 0300        return extension switch
 0301        {
 0302            ".cs" => "📄",
 0303            ".razor" => "⚡",
 0304            ".html" => "🌐",
 0305            ".css" => "🎨",
 0306            ".js" => "📜",
 0307            ".json" => "📋",
 0308            ".xml" => "📋",
 0309            ".txt" => "📝",
 0310            ".md" => "📖",
 0311            ".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" => "🖼️",
 0312            ".dll" or ".exe" => "⚙️",
 0313            ".config" => "🔧",
 0314            ".csproj" or ".sln" => "📦",
 0315            _ => "📄"
 0316        };
 317    }
 318
 319    public async Task Clear()
 320    {
 0321        currentInput = string.Empty;
 0322        tokens.Clear();
 0323        filteredFiles.Clear();
 324
 325        // Сбрасываем высоту textarea
 0326        await JS.InvokeVoidAsync("eval", "arguments[0].style.height = 'auto'", textareaRef);
 327
 0328        StateHasChanged();
 0329    }
 330
 331    protected override void OnInitialized()
 332    {
 0333        base.OnInitialized();
 0334        ChatService.OnSessionChanged += HandleSessionChanged;
 0335        VsBridge.OnModeSwitched += HandleModeSwitched;
 0336    }
 337
 338    private void HandleModeSwitched(AppMode mode)
 339    {
 0340        CurrentMode = mode;
 0341        InvokeAsync(StateHasChanged);
 0342    }
 343
 344    private void HandleSessionChanged()
 345    {
 0346        InvokeAsync(StateHasChanged);
 0347    }
 348
 349    private void HandleProfileChanged()
 350    {
 0351        InvokeAsync(StateHasChanged);
 0352    }
 353
 354    private async Task OpenRules()
 355    {
 0356        await VsBridge.ExecuteToolAsync(BasicEnum.OpenFile, new Dictionary<string, object> { { "param1", ".agents/rules.
 0357    }
 358
 359    private async Task OpenSkills()
 360    {
 0361        await VsBridge.ExecuteToolAsync(BasicEnum.OpenFolder, new Dictionary<string, object> { { "param1", ".agents/skil
 0362    }
 363
 364    public override void Dispose()
 365    {
 0366        base.Dispose();
 0367        ChatService.OnSessionChanged -= HandleSessionChanged;
 0368        VsBridge.OnModeSwitched -= HandleModeSwitched;
 0369    }
 370}