< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 328
Coverable lines: 328
Total lines: 695
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 112
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1@using UIBlazor.Components.Chat
 2@inherits RadzenComponent
 3
 4@inject ChatService ChatService
 5@inject DialogService DialogService
 6
 07<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" style="@Style" id="@GetId()" >
 08    <!-- Chat Header -->
 09    <RadzenStack class="rz-chat-header" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyConte
 010        <UsageIndicators Messages="@Messages" />
 011        <RadzenStack class="rz-chat-header-actions" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" 
 012            @if (ProfileManager.Current.Profiles.Count > 0)
 13            {
 14                <RadzenDropDown TValue="string" Data="@ProfileManager.Current.Profiles" TextProperty="Name" ValuePropert
 15                                Value="@ProfileManager.Current.ActiveProfileId" Change="@OnProfileChangeAsync"
 16                                Placeholder="Select Profile"
 17                                Style="width: 180px;" />
 18            }
 19            <RadzenButton Icon="add_comment" Variant="Variant.Text" ButtonStyle="ButtonStyle.Base"
 20                            Click="@NewSessionAsync" Title="New session" class="rz-chat-header-new" />
 21            <RadzenButton Icon="history" Disabled="@(_recentSessions.Count == 0)" Variant="Variant.Text" ButtonStyle="Bu
 22                            Click="@OpenSessionsDialogAsync" Title="Session history" class="rz-chat-header-history" />
 23            <RadzenButton Icon="settings" Variant="Variant.Text" ButtonStyle="ButtonStyle.Base"
 24                            Click="@OnShowSettingsAsync" Title="Settings" class="rz-chat-header-settings" />
 25        </RadzenStack>
 26    </RadzenStack>
 27
 28    <!-- Chat Messages -->
 29    <div class="rz-chat-messages" id="chat-messages">
 030        @if (Messages.Count == 0)
 31        {
 32            <RecentSessionsPicker ShowTitle=true />
 33        }
 34        else
 35        {
 036            @foreach (var message in Messages)
 37            {
 38                <ChatMessageView
 39                    @key="message.Id"
 40                    Message="@message"
 41                    IsLast="@(message == Messages.LastOrDefault())"
 42                    IsLoading="@IsLoading"
 43                    OnToolApproval="@HandleToolApprovalAsync"
 44                    OnExecutePlan="@ExecutePlanAsync"
 045                    OnEdit="@((m) => OnEditMessage(m))"
 046                    OnCancelEdit="@((m) => OnCancelEdit(m))"
 47                    OnSaveEdit="@OnSaveEditAsync"
 48                    OnDelete="@OnDeleteMessageAsync"
 49                    OnRegenerate="@OnRegenerateLastAsync" />
 50            }
 51        }
 52    </div>
 53
 54    <!-- Chat Input -->
 55    <AiChatInput
 56        IsLoading="IsLoading"
 57        SendMessage="SendMessageAsync"
 58        Cancel="CancelResponceAsync">
 59    </AiChatInput>
 60</div>

/home/runner/work/InvAit/InvAit/UIBlazor/Components/AIChat.razor.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using Microsoft.AspNetCore.Components;
 3using Microsoft.JSInterop;
 4using Radzen;
 5using Radzen.Blazor.Rendering;
 6using UIBlazor.Constants;
 7using UIBlazor.Services;
 8using UIBlazor.Services.Settings;
 9
 10namespace UIBlazor.Components;
 11
 12public partial class AiChat : RadzenComponent
 13{
 14    // TODO использовать из ChatService.Session.Messages
 15    // для этого нужно переработать сохранение сообщений
 16    // сейчас тулзы сохраняются в сессии как отдельные Messages. А тут они идут в массив ToolMessages
 017    private List<VisualChatMessage> Messages { get; set; } = [];
 18
 019    private List<SessionSummary> _recentSessions = [];
 20
 021    private bool IsLoading { get; set; }
 22
 23    private DotNetObjectReference<AiChat>? _dotNetRef;
 24
 025    private readonly ConcurrentDictionary<string, TaskCompletionSource<ToolApprovalStatus>> _approvalWaiters = [];
 26
 027    private CancellationTokenSource _cts = new();
 28
 029    [Inject] private NotificationService NotificationService { get; set; } = null!;
 30
 031    [Inject] private IToolManager ToolManager { get; set; } = null!;
 32
 033    [Inject] private IProfileManager ProfileManager { get; set; } = null!;
 34
 035    [Inject] private ICommonSettingsProvider CommonSettingsProvider { get; set; } = null!;
 36
 037    [Inject] private IVsBridge VsBridge { get; set; } = null!;
 38
 039    [Inject] private IJSRuntime JsRuntime { get; set; } = null!;
 40
 041    [Inject] private IMessageParser MessageParser { get; set; } = null!;
 42
 043    [Inject] private ILogger<AiChat> Logger { get; set; } = null!;
 44
 45    /// <summary>
 46    /// Adds a message to the chat.
 47    /// </summary>
 48    public VisualChatMessage AddVisualMessage(VisualChatMessage chatMessage, bool updateState = true)
 49    {
 50        // если не обновляем состояние - то это загрузка истории
 051        MessageParser.UpdateSegments(chatMessage.Content, chatMessage, isHistory: !updateState);
 052        Messages.Add(chatMessage);
 53
 54        // Limit the number of messages
 055        if (Messages.Count > ChatService.Options.MaxMessages)
 56        {
 057            Messages.RemoveAt(0);
 58        }
 59
 060        if (updateState)
 61        {
 062            InvokeAsync(StateHasChanged);
 63        }
 064        return chatMessage;
 65    }
 66
 67    /// <summary>
 68    /// Starts a new session.
 69    /// </summary>
 70    public async Task NewSessionAsync()
 71    {
 072        Messages.Clear();
 073        await ChatService.NewSessionAsync();
 074        _recentSessions = await ChatService.GetRecentSessionsAsync(3);
 075        await InvokeAsync(StateHasChanged);
 076    }
 77
 78    /// <summary>
 79    /// Opens the sessions dialog to view or load history.
 80    /// </summary>
 81    public async Task OpenSessionsDialogAsync()
 82    {
 083        await DialogService.OpenAsync<RecentSessionsPicker>(SharedResource.SessionsTitle,
 084            options: new DialogOptions {
 085                Width = "500px",
 086                Height = "400px",
 087                AutoFocusFirstElement = true,
 088                CloseDialogOnOverlayClick = true
 089            });
 90
 091        await InvokeAsync(StateHasChanged);
 092    }
 93
 94    /// <summary>
 95    /// Sends a message programmatically.
 96    /// </summary>
 97    /// <param name="content">The message content to send.</param>
 98    public async Task SendMessageAsync(string content)
 99    {
 0100        if (string.IsNullOrWhiteSpace(content) || IsLoading)
 0101            return;
 102
 103        // Process the CurrentInput (HTML) to extract chip data and fetch content
 0104        var processedContent = await ProcessMessageContentAsync(content);
 105
 106        // Add user message
 0107        var userMessage = new VisualChatMessage
 0108        {
 0109            Content = processedContent,
 0110            Role = ChatMessageRole.User,
 0111            IsExpanded = IsShortMessage(processedContent)
 0112        };
 0113        AddVisualMessage(userMessage);
 0114        await ChatService.AddMessageAsync(userMessage);
 115
 116        // Get AI response
 0117        await GetAiResponseAsync();
 0118    }
 119
 120    private async Task<string> ProcessMessageContentAsync(string htmlContent)
 121    {
 0122        var processedContentBuilder = new StringBuilder();
 0123        var lastIndex = 0;
 124
 125        // Regex to find chip spans: <span contenteditable="false" class="chip" data-path="path_to_file">display_text</s
 0126        var chipRegex = new Regex("<span\\s+contenteditable=\"false\"\\s+class=\"chip\"\\s+data-path=\"([^\"]+)\"[^>]*>.
 127
 0128        foreach (Match match in chipRegex.Matches(htmlContent))
 129        {
 130            // Append text before the current chip
 0131            processedContentBuilder.Append(htmlContent.Substring(lastIndex, match.Index - lastIndex));
 132
 0133            var dataPath = match.Groups[1].Value;
 134
 135            // Call the tool to get the content
 0136            var tool = ToolManager.GetTool(BuiltInToolEnum.ReadOpenFile.ToString());
 0137            if (tool != null)
 138            {
 0139                var args = new Dictionary<string, object> { { "path", dataPath } };
 0140                var vsToolResult = await tool.ExecuteAsync(args);
 141
 0142                if (vsToolResult.Success)
 143                {
 0144                    processedContentBuilder.Append($"<file_content path=\"{dataPath}\">\n{vsToolResult.Result}\n</file_c
 145                }
 146                else
 147                {
 0148                    processedContentBuilder.Append($"<error_reading_file path=\"{dataPath}\">{vsToolResult.ErrorMessage}
 149                }
 150            }
 151            else
 152            {
 0153                processedContentBuilder.Append($"<error_tool_not_found path=\"{dataPath}\">Tool 'ReadOpenFile' not found
 154            }
 155
 0156            lastIndex = match.Index + match.Length;
 0157        }
 158
 159        // Append any remaining text after the last chip
 0160        processedContentBuilder.Append(htmlContent.Substring(lastIndex));
 161
 0162        return processedContentBuilder.ToString();
 0163    }
 164
 165    private async Task GetAiResponseAsync()
 166    {
 0167        await _cts.CancelAsync();
 0168        _cts = new CancellationTokenSource();
 0169        await GetAiResponseInternalAsync(0);
 0170    }
 171
 172    private async Task GetAiResponseInternalAsync(int retryCount)
 173    {
 0174        IsLoading = true;
 175
 176        // Add assistant message placeholder
 0177        var assistantMessage = AddVisualMessage(new VisualChatMessage
 0178        {
 0179            Role = ChatMessageRole.Assistant,
 0180            IsStreaming = true,
 0181            IsExpanded = true
 0182        });
 183
 184        try
 185        {
 0186            var reasoning = new StringBuilder();
 0187            var response = new StringBuilder();
 188
 0189            await foreach (var delta in ChatService.GetCompletionsAsync(_cts.Token))
 190            {
 0191                if (delta.ReasoningContent != null)
 192                {
 0193                    reasoning.Append(delta.ReasoningContent);
 0194                    assistantMessage.ReasoningContent = reasoning.ToString();
 195                }
 0196                if (delta.Content != null)
 197                {
 0198                    response.Append(delta.Content);
 199                    // обновляем сегменты в сообщении
 0200                    MessageParser.UpdateSegments(delta.Content, assistantMessage);
 201                }
 202
 0203                assistantMessage.Model ??= ChatService.LastCompletionsModel;
 204
 0205                await InvokeAsync(StateHasChanged);
 206            }
 207
 0208            assistantMessage.Content = response.ToString();
 0209            assistantMessage.IsStreaming = false;
 210
 0211            ParsePlan(assistantMessage);
 212
 0213            await ChatService.AddMessageAsync(assistantMessage);
 214
 215            // TODO: надо подумать что делать, если прервался на незакрытом тулзе...
 216            // Думаю нужно выдавать ошибку модели
 0217            await HandleToolCallAsync(assistantMessage, [.. assistantMessage.Segments.Where(s => s.Type == SegmentType.T
 0218        }
 0219        catch (OperationCanceledException)
 220        {
 0221            assistantMessage.IsStreaming = false;
 0222        }
 0223        catch (Exception ex)
 224        {
 0225            var maxRetries = CommonSettingsProvider.Current.MaxRetries;
 0226            assistantMessage.Content = $"Error: {ex.Message} [{retryCount}/{maxRetries}]";
 227            // обновляем сегменты в сообщении
 0228            MessageParser.UpdateSegments(assistantMessage.Content, assistantMessage);
 0229            assistantMessage.IsStreaming = false;
 0230            Logger.LogError(ex, "Getting response error");
 231
 0232            if (retryCount < maxRetries)
 233            {
 0234                retryCount++;
 0235                var delay = GetRetryDelay(retryCount);
 236
 0237                assistantMessage.MaxRetryAttempts = maxRetries;
 0238                assistantMessage.RetryAttempt = retryCount;
 239
 240                try
 241                {
 0242                    for (var i = delay; i > 0; i--)
 243                    {
 0244                        assistantMessage.RetryCountdown = i;
 0245                        await InvokeAsync(StateHasChanged);
 0246                        await Task.Delay(1000, _cts.Token);
 247                    }
 0248                }
 0249                catch (OperationCanceledException)
 250                {
 0251                    assistantMessage.RetryCountdown = 0;
 0252                    return;
 253                }
 254
 0255                assistantMessage.RetryCountdown = 0;
 0256                Messages.Remove(assistantMessage);
 0257                ChatService.Session.RemoveMessage(assistantMessage.Id);
 0258                IsLoading = false;
 0259                await GetAiResponseInternalAsync(retryCount);
 0260                return;
 261            }
 262        }
 263        finally
 264        {
 0265            IsLoading = false;
 0266            await InvokeAsync(StateHasChanged);
 267        }
 0268    }
 269
 270    private static int GetRetryDelay(int attempt)
 271    {
 0272        return attempt switch
 0273        {
 0274            1 => 2,
 0275            2 => 5,
 0276            3 => 10,
 0277            _ => 20
 0278        };
 279    }
 280
 281    private static void ParsePlan(VisualChatMessage message)
 282    {
 0283        if (string.IsNullOrEmpty(message.Content)) return;
 284
 0285        var planRegex = new Regex(@"<plan>(?<plan>.*?)</plan>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
 0286        var match = planRegex.Match(message.Content);
 0287        if (match.Success)
 288        {
 0289            message.PlanContent = match.Groups["plan"].Value.Trim();
 290            // Remove the plan block from display content to avoid double showing
 0291            message.DisplayContent = planRegex.Replace(message.DisplayContent ?? message.Content, string.Empty).Trim();
 0292            if (string.IsNullOrEmpty(message.DisplayContent))
 293            {
 0294                message.DisplayContent = "Proposed Plan:";
 295            }
 296        }
 0297    }
 298
 299    private async Task ExecutePlanAsync(VisualChatMessage message)
 300    {
 0301        if (!message.HasPlan) return;
 302
 303        // Switch mode to Agent
 0304        ChatService.Session.Mode = AppMode.Agent;
 305
 306        // Notify VS Host if needed (though it's mostly for system prompt generation in UI)
 0307        await VsBridge.ExecuteToolAsync(BasicEnum.SwitchMode, new Dictionary<string, object> { { "param1", "Agent" } });
 308
 309        // Send confirmation message to start implementation
 0310        await SendMessageAsync("Implement the plan.");
 0311    }
 312
 313    private async Task HandleToolApprovalAsync((string MessageId, string SegmentId, bool Approved) args)
 314    {
 315        // оно всегда должно быть, потому что мы блокируем UI,
 316        // пока не придет ответ от модели, и юзер не может кликнуть раньше времени. Но на всякий случай проверим.
 0317        var message = Messages.FirstOrDefault(m => m.Id == args.MessageId);
 0318        if (message != null)
 319        {
 0320            var status = args.Approved ? ToolApprovalStatus.Approved : ToolApprovalStatus.Rejected;
 0321            message.Segments.FirstOrDefault(s => s.Id == args.SegmentId)?.ApprovalStatus = status;
 0322            await InvokeAsync(StateHasChanged);
 323
 0324            if (_approvalWaiters.TryRemove($"{args.MessageId}_{args.SegmentId}", out var tcs))
 325            {
 0326                tcs.SetResult(status);
 327            }
 328        }
 0329    }
 330
 331    private async Task HandleToolCallAsync(VisualChatMessage assistantMessage, List<ContentSegment> toolsSegments)
 332    {
 0333        if (toolsSegments.Count == 0)
 334        {
 0335            return;
 336        }
 337
 0338        _approvalWaiters.Clear();
 339
 0340        foreach (var segment in toolsSegments)
 341        {
 0342            if (_cts.Token.IsCancellationRequested)
 343            {
 0344                return;
 345            }
 346
 0347            var tool = ToolManager.GetTool(segment.ToolName);
 348
 349            VsToolResult vsToolResult;
 0350            if (tool == null)
 351            {
 0352                vsToolResult = new VsToolResult
 0353                {
 0354                    Name = segment.ToolName,
 0355                    Success = false,
 0356                    ErrorMessage = "Tool not found."
 0357                };
 358            }
 359            else
 360            {
 361                // Спрашиваем разрешение если нужно
 0362                if (segment.ApprovalStatus == ToolApprovalStatus.Pending)
 363                {
 0364                    var tcs = new TaskCompletionSource<ToolApprovalStatus>();
 0365                    var waiterKey = $"{assistantMessage.Id}_{segment.Id}";
 0366                    _approvalWaiters[waiterKey] = tcs;
 367
 368                    try
 369                    {
 370                        // Ждем аппрува от пользователя или отмены всего стрима
 0371                        segment.ApprovalStatus = await tcs.Task.WaitAsync(_cts.Token);
 0372                    }
 0373                    catch (OperationCanceledException)
 374                    {
 0375                        _approvalWaiters.Clear();
 0376                        return;
 377                    }
 378                    finally
 379                    {
 0380                        _approvalWaiters.TryRemove(waiterKey, out _);
 381                    }
 0382                }
 383
 0384                if (_cts.Token.IsCancellationRequested)
 385                {
 0386                    return;
 387                }
 388
 389                // Уже должен быть известен статус тулза - или разрешен, или запрещен.
 0390                vsToolResult = segment.ApprovalStatus switch
 0391                {
 0392                    ToolApprovalStatus.Approved => await tool.ExecuteAsync(MessageParser.Parse(segment.ToolName, segment
 0393                    _ => new VsToolResult
 0394                    {
 0395                        Name = segment.ToolName,
 0396                        Success = false,
 0397                        ErrorMessage = "Execution was denied by user."
 0398                    }
 0399                };
 400            }
 401#if DEBUG
 402            // Безголовые (без Visual Studio) тесты
 403            vsToolResult = HeadlessMocker.GetVsToolResult(vsToolResult);
 404            Logger.LogTrace("{request} >>>>>> {result}", JsonUtils.Serialize(tool), JsonUtils.Serialize(vsToolResult));
 405#endif
 406
 407            // для модели обогащаем результат и отправляем в чат
 0408            var result = $"""
 0409                             <tool_result name="{tool.Name}" success={vsToolResult.Success}>
 0410                             {(vsToolResult.Success ? vsToolResult.Result : vsToolResult.ErrorMessage)}
 0411                             </tool_result>
 0412                             """;
 413
 0414            var toolSessionMessage = new VisualChatMessage
 0415            {
 0416                Role = vsToolResult.Role,
 0417                Content = result
 0418            };
 419
 420            // идет в сессию
 0421            await ChatService.AddMessageAsync(toolSessionMessage);
 422
 0423            var toolResultMessage = new VisualChatMessage
 0424            {
 0425                Id = toolSessionMessage.Id, // синхронизируем Id. Для показа в UI и удаления
 0426                Role = ChatMessageRole.Tool,
 0427                Content = vsToolResult.Result,
 0428                ToolDisplayName = (vsToolResult.Success ? "✅ " : "❌ ") + tool.DisplayName ?? tool.Name,
 0429            };
 430
 0431            assistantMessage.ToolMessages.Add(toolResultMessage);
 432
 0433            await InvokeAsync(StateHasChanged);
 0434        }
 435
 0436        if (_cts.Token.IsCancellationRequested)
 437        {
 0438            return;
 439        }
 440
 0441        await GetAiResponseAsync();
 0442    }
 443
 444    private void SyncSessionMessageWithUi()
 445    {
 0446        if (ChatService.Session == null) return;
 447
 0448        Messages.Clear();
 0449        VisualChatMessage? lastAssistantMessage = null;
 0450        foreach (var chatMessage in ChatService.Session.Messages)
 451        {
 452            // тулзы показываем по особому (смотри HandleToolCallAsync)
 0453            var regex = Regex.Match(chatMessage.Content, "^<tool_result name=\"(?<name>.{2,40})\" success=(?<success>[T|
 0454            if (regex.Success)
 455            {
 0456                var isSuccess = string.Equals(regex.Groups["success"].Value, "True", StringComparison.OrdinalIgnoreCase)
 0457                var toolDisplayName = (isSuccess ? "✅ " : "❌ ") + ToolManager.GetTool(regex.Groups["name"].Value)?.Displ
 458
 0459                var toolResultMessage = new VisualChatMessage
 0460                {
 0461                    Id = chatMessage.Id,
 0462                    Role = ChatMessageRole.Tool,
 0463                    Content = regex.Groups["result"].Value,
 0464                    ToolDisplayName = toolDisplayName,
 0465                };
 466
 0467                if (lastAssistantMessage != null)
 468                {
 0469                    lastAssistantMessage.ToolMessages.Add(toolResultMessage);
 470                }
 471                else
 472                {
 473                    // Fallback if somehow there's a tool result without an assistant message before it
 0474                    AddVisualMessage(toolResultMessage, updateState: false);
 475                }
 476            }
 477            else
 478            {
 0479                if (chatMessage.Role == ChatMessageRole.Assistant)
 480                {
 0481                    ParsePlan(chatMessage);
 0482                    lastAssistantMessage = chatMessage;
 483                }
 0484                else if (chatMessage.Role == ChatMessageRole.User)
 485                {
 0486                    lastAssistantMessage = null;
 487                }
 488
 0489                chatMessage.IsExpanded = IsShortMessage(chatMessage.DisplayContent ?? chatMessage.Content);
 0490                AddVisualMessage(chatMessage, updateState: false);
 491            }
 492        }
 493
 494
 0495        InvokeAsync(StateHasChanged);
 0496    }
 497
 498    private static bool IsShortMessage(string content)
 0499        => string.IsNullOrEmpty(content) || (content.Length < 1000 && content.Count(c => c == '\n') < 15);
 500
 0501    private async Task CancelResponceAsync() => await _cts.CancelAsync();
 502
 503    protected override async Task OnInitializedAsync()
 504    {
 0505        await base.OnInitializedAsync();
 0506        _dotNetRef = DotNetObjectReference.Create(this);
 507
 0508        ChatService.OnSessionChanged += HandleSessionChanged;
 509
 0510        ToolManager.RegisterAllTools();
 511
 0512        _recentSessions = await ChatService.GetRecentSessionsAsync(3);
 513
 0514        await VsBridge.InitializeAsync();
 0515        await InvokeAsync(StateHasChanged);
 0516    }
 517
 518    private async void HandleSessionChanged()
 519    {
 0520        SyncSessionMessageWithUi();
 0521        _recentSessions = await ChatService.GetRecentSessionsAsync(3);
 0522        await InvokeAsync(StateHasChanged);
 0523    }
 524
 525    protected override async Task OnAfterRenderAsync(bool firstRender)
 526    {
 0527        if (firstRender)
 528        {
 0529            await JsRuntime.InvokeVoidAsync("setChatHandler", _dotNetRef);
 0530            await JsRuntime.InvokeVoidAsync("initChatAutoScroll", $"#chat-messages", 70);
 531        }
 0532    }
 533
 534    private async Task OnProfileChangeAsync(object value)
 535    {
 0536        var profileId = value as string;
 0537        if (!string.IsNullOrEmpty(profileId))
 538        {
 0539            await ProfileManager.ActivateProfileAsync(profileId);
 0540            NotificationService.Notify(new NotificationMessage
 0541            {
 0542                Severity = NotificationSeverity.Info,
 0543                Summary = "Profile Changed",
 0544                Detail = $"Active profile updated.",
 0545                Duration = 1000
 0546            });
 547        }
 0548    }
 549
 550    private static void OnEditMessage(VisualChatMessage message)
 551    {
 0552        message.TempContent = message.Content;
 0553        message.IsEditing = true;
 0554    }
 555
 556    private static void OnCancelEdit(VisualChatMessage message)
 557    {
 0558        message.IsEditing = false;
 0559        message.TempContent = string.Empty;
 0560    }
 561
 562    private async Task OnSaveEditAsync(VisualChatMessage message)
 563    {
 0564        message.Content = message.TempContent;
 0565        message.IsEditing = false;
 566
 567        // Update display content if it's an assistant message with tools
 0568        ParsePlan(message);
 569
 0570        ChatService.Session.UpdateMessage(message.Id, message.Content);
 0571        await ChatService.SaveSessionAsync();
 0572        await InvokeAsync(StateHasChanged);
 0573    }
 574
 575    private async Task OnDeleteMessageAsync(VisualChatMessage message)
 576    {
 0577        Messages.Remove(message);
 0578        ChatService.Session.RemoveMessage(message.Id);
 579
 0580        foreach (var toolMsg in message.ToolMessages)
 581        {
 0582            ChatService.Session.RemoveMessage(toolMsg.Id);
 583        }
 584
 0585        await ChatService.SaveSessionAsync();
 0586        await InvokeAsync(StateHasChanged);
 0587    }
 588
 589    private async Task OnRegenerateLastAsync()
 590    {
 0591        var lastAssistantMessage = Messages.LastOrDefault(m => m.Role == ChatMessageRole.Assistant);
 0592        if (lastAssistantMessage != null)
 593        {
 0594            Messages.Remove(lastAssistantMessage);
 0595            ChatService.Session.RemoveMessage(lastAssistantMessage.Id);
 596
 0597            foreach (var toolMsg in lastAssistantMessage.ToolMessages)
 598            {
 0599                ChatService.Session.RemoveMessage(toolMsg.Id);
 600            }
 601
 0602            await GetAiResponseAsync();
 603        }
 0604    }
 605
 606    private async Task OnShowSettingsAsync()
 607    {
 0608        await DialogService.OpenSideAsync<SettingsDialog>(@SharedResource.Settings,
 0609            options: new SideDialogOptions
 0610            {
 0611                CloseDialogOnOverlayClick = true,
 0612                Resizable = true,
 0613                Position = DialogPosition.Right,
 0614                MinHeight = 250.0,
 0615                MinWidth = 400.0
 0616            });
 0617    }
 618
 619    /// <inheritdoc />
 0620    protected override string GetComponentCssClass() => ClassList.Create("rz-chat").ToString();
 621
 622    /// <inheritdoc />
 623    public override void Dispose()
 624    {
 0625        base.Dispose();
 626
 0627        _dotNetRef?.Dispose();
 0628        ChatService.OnSessionChanged -= HandleSessionChanged;
 629
 0630        _cts?.Cancel();
 0631        _cts?.Dispose();
 632
 0633        GC.SuppressFinalize(this);
 0634    }
 635}