< Summary

Line coverage
42%
Covered lines: 147
Uncovered lines: 201
Coverable lines: 348
Total lines: 706
Line coverage: 42.2%
Branch coverage
30%
Covered branches: 41
Total branches: 134
Branch coverage: 30.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: BuildRenderTree(...)100%44100%
File 2: get_Messages()100%11100%
File 2: get_IsLoading()100%11100%
File 2: .ctor()100%11100%
File 2: get_NotificationService()100%11100%
File 2: get_DialogService()100%11100%
File 2: get_ChatService()100%11100%
File 2: get_ToolManager()100%11100%
File 2: get_ProfileManager()100%11100%
File 2: get_CommonSettingsProvider()100%11100%
File 2: get_VsBridge()100%11100%
File 2: get_JsRuntime()100%11100%
File 2: get_MessageParser()100%11100%
File 2: get_Logger()100%11100%
File 2: NewSessionAsync()100%11100%
File 2: SendMessageAsync()100%66100%
File 2: GetAiResponseAsync()100%11100%
File 2: CompressAsync()0%210140%
File 2: GetAiResponseInternalAsync()30.55%2603644.33%
File 2: GetRetryDelay(...)0%2040%
File 2: ParsePlan(...)12.5%53811.11%
File 2: ExecutePlanAsync()0%620%
File 2: HandleToolApprovalAsync()0%7280%
File 2: CallToolAsync()0%2040%
File 2: HandleToolCallAsync()7.14%165148.33%
File 2: LoadMessagesFromSession()50%141277.77%
File 2: IsShortMessage(...)50%44100%
File 2: CancelResponseAsync()100%210%
File 2: OnInitializedAsync()100%11100%
File 2: HandleSessionChanged(...)50%2280%
File 2: OnAfterRenderAsync()100%22100%
File 2: OnProfileChangeAsync()100%22100%
File 2: OnEditMessage(...)100%210%
File 2: OnCancelEdit(...)100%210%
File 2: OnSaveEditAsync()100%210%
File 2: OnDeleteMessageAsync()100%210%
File 2: OnRegenerateLastAsync()0%620%
File 2: OnShowSettingsAsync()100%2285.71%
File 2: Dispose()50%66100%

File(s)

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

#LineLine coverage
 1@inherits RadzenComponent
 2
 173<div @ref="@Element" @attributes="Attributes" class="rz-chat rz-h-100" id="@GetId()">
 174    <!-- Chat Header -->
 175    <RadzenStack class="rz-chat-header" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyConte
 176        <UsageIndicators Messages="@Messages" />
 177        <RadzenStack class="rz-chat-header-actions" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" 
 438            @if (ProfileManager.Current.Profiles.Count > 0)
 9            {
 10                <RadzenDropDown TValue="string" Data="@ProfileManager.Current.Profiles" TextProperty="Name" ValuePropert
 11                                Value="@ProfileManager.Current.ActiveProfileId" Change="@OnProfileChangeAsync"
 12                                Placeholder="Select Profile"
 13                                Style="width: 180px;" />
 14            }
 15            <RadzenButton
 16                Icon="add_comment"
 17                Variant="Variant.Text"
 18                ButtonStyle="ButtonStyle.Base"
 19                Click="@NewSessionAsync"
 20                title="New session"
 21                class="rz-chat-header-new" />
 22            <RadzenButton
 23                Icon="settings"
 24                Variant="Variant.Text"
 25                ButtonStyle="ButtonStyle.Base"
 26                Click="@OnShowSettingsAsync"
 27                IsBusy="@_callSettings"
 28                title="Settings"
 29                class="rz-chat-header-settings" />
 30        </RadzenStack>
 31    </RadzenStack>
 32
 33    <!-- Chat Messages -->
 34    <div class="rz-chat-messages" >
 2635        @if (Messages.Count == 0)
 36        {
 37            <RecentSessionsPicker ShowTitle=true />
 38        }
 39        else
 40        {
 641            var lastMessage = Messages.LastOrDefault();
 3242            foreach (var message in Messages)
 43            {
 44                <ChatMessageView
 45                    @key="message.Id"
 46                    Message="message"
 47                    IsLast="(message == lastMessage)"
 48                    IsLoading="IsLoading"
 49                    OnToolApproval="HandleToolApprovalAsync"
 50                    OnExecutePlan="ExecutePlanAsync"
 051                    OnEdit="((m) => OnEditMessage(m))"
 052                    OnCancelEdit="((m) => OnCancelEdit(m))"
 53                    OnSaveEdit="OnSaveEditAsync"
 54                    OnDelete="OnDeleteMessageAsync"
 55                    OnRegenerate="OnRegenerateLastAsync" />
 56            }
 57            <div class="anchor" />
 58        }
 59    </div>
 60
 61    <!-- Chat Input -->
 62    <AiChatInput
 63        IsLoading="IsLoading"
 64        SendMessage="SendMessageAsync"
 65        Cancel="CancelResponseAsync">
 66    </AiChatInput>
 67</div>

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

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.ComponentModel;
 3using System.Diagnostics;
 4using Microsoft.AspNetCore.Components;
 5using Microsoft.JSInterop;
 6using Radzen;
 7using UIBlazor.Services;
 8using UIBlazor.Services.Settings;
 9using static System.Collections.Specialized.BitVector32;
 10using ConversationSession = UIBlazor.Models.ConversationSession;
 11
 12namespace UIBlazor.Components;
 13
 14public partial class AiChat : RadzenComponent
 15{
 6616    private List<VisualChatMessage> Messages => ChatService.Session.Messages;
 17
 3918    private bool IsLoading { get; set; }
 19
 20    private DotNetObjectReference<AiChat>? _dotNetRef;
 21
 1722    private readonly ConcurrentDictionary<string, TaskCompletionSource<ToolApprovalStatus>> _approvalWaiters = [];
 23
 1724    private CancellationTokenSource _cts = new();
 25
 26    private bool _callSettings;
 27
 1828    [Inject] private NotificationService NotificationService { get; set; } = null!;
 29
 1830    [Inject] private DialogService DialogService { get; set; } = null!;
 31
 12732    [Inject] IChatService ChatService { get; set; } = null!;
 33
 3434    [Inject] private IToolManager ToolManager { get; set; } = null!;
 35
 9536    [Inject] private IProfileManager ProfileManager { get; set; } = null!;
 37
 1738    [Inject] private ICommonSettingsProvider CommonSettingsProvider { get; set; } = null!;
 39
 3440    [Inject] private IVsBridge VsBridge { get; set; } = null!;
 41
 3542    [Inject] private IJSRuntime JsRuntime { get; set; } = null!;
 43
 1844    [Inject] private IMessageParser MessageParser { get; set; } = null!;
 45
 1746    [Inject] private ILogger<AiChat> Logger { get; set; } = null!;
 47
 48    /// <summary>
 49    /// Starts a new session.
 50    /// </summary>
 51    public async Task NewSessionAsync()
 52    {
 153        await ChatService.NewSessionAsync();
 154        await InvokeAsync(StateHasChanged);
 155    }
 56
 57    /// <summary>
 58    /// Sends a message programmatically.
 59    /// </summary>
 60    /// <param name="content">The message content to send.</param>
 61    public async Task SendMessageAsync(string content)
 62    {
 363        if (string.IsNullOrWhiteSpace(content) || IsLoading)
 264            return;
 65
 66        // Add user message
 167        var userMessage = new VisualChatMessage
 168        {
 169            Content = content,
 170            Role = ChatMessageRole.User,
 171            IsExpanded = IsShortMessage(content)
 172        };
 73
 174        ChatService.Session.AddMessage(userMessage);
 75
 76        // скролл вниз
 177        await Task.Yield();
 178        await JsRuntime.InvokeVoidAsync("scrollToAnchor");
 79
 80        // Get AI response
 181        await GetAiResponseAsync();
 382    }
 83
 84    private async Task GetAiResponseAsync()
 85    {
 186        await _cts.CancelAsync();
 187        _cts = new CancellationTokenSource();
 188        await GetAiResponseInternalAsync(0);
 189    }
 90
 91    private async Task CompressAsync()
 92    {
 093        var compessingMessage = new VisualChatMessage
 094        {
 095            Role = ChatMessageRole.Assistant,
 096            IsStreaming = true,
 097            IsExpanded = true,
 098            Content = "### Compressing... ♻ \n\n"
 099        };
 100
 0101        ChatService.Session.AddMessage(compessingMessage);
 0102        MessageParser.UpdateSegments(compessingMessage.Content, compessingMessage);
 103
 0104        await InvokeAsync(StateHasChanged);
 105
 0106        var reasoning = new StringBuilder();
 0107        var response = new StringBuilder();
 108
 109        try
 110        {
 0111            await foreach (var delta in ChatService.CompressSessionAsync(_cts.Token))
 112            {
 0113                if (!string.IsNullOrEmpty(delta.ReasoningContent))
 114                {
 0115                    reasoning.Append(delta.ReasoningContent);
 0116                    compessingMessage.ReasoningContent = reasoning.ToString();
 117                }
 0118                if (!string.IsNullOrEmpty(delta.Content))
 119                {
 0120                    response.Append(delta.Content);
 121                    // обновляем сегменты в сообщении
 0122                    MessageParser.UpdateSegments(delta.Content, compessingMessage);
 123                }
 124
 0125                compessingMessage.Model ??= ChatService.LastCompletionsModel;
 126
 0127                await InvokeAsync(StateHasChanged);
 128            }
 0129        }
 0130        catch (OperationCanceledException) when (_cts.IsCancellationRequested)
 131        {
 0132            compessingMessage.Content = "Cancelled by user...";
 0133            IsLoading = false;
 0134        }
 0135        catch (Exception ex)
 136        {
 0137            compessingMessage.Content += $"\n\nError: {ex.Message}";
 0138        }
 139        finally
 140        {
 0141            compessingMessage = ChatService.Session.Messages.LastOrDefault(m => m.Role == ChatMessageRole.Assistant && m
 0142            if (compessingMessage is not null)
 143            {
 0144                compessingMessage.ReasoningContent = reasoning.ToString();
 0145                compessingMessage.Model ??= ChatService.LastCompletionsModel;
 0146                MessageParser.UpdateSegments(compessingMessage.Content, compessingMessage);
 0147                compessingMessage.IsStreaming = false;
 148            }
 0149            await InvokeAsync(StateHasChanged);
 150        }
 0151    }
 152
 153    private async Task GetAiResponseInternalAsync(int retryCount)
 154    {
 1155        IsLoading = true;
 156
 1157        if (ChatService.NeedCompression)
 158        {
 0159            await CompressAsync();
 0160            if (_cts.Token.IsCancellationRequested)
 161            {
 0162                IsLoading = false;
 0163                return;
 164            }
 165        }
 166
 167        // Add assistant message placeholder
 1168        var assistantMessage = new VisualChatMessage
 1169        {
 1170            Role = ChatMessageRole.Assistant,
 1171            IsStreaming = true,
 1172            IsExpanded = true
 1173        };
 174
 1175        ChatService.Session.AddMessage(assistantMessage);
 1176        await ChatService.SaveSessionAsync();
 1177        await InvokeAsync(StateHasChanged);
 178
 179        try
 180        {
 1181            var sw = Stopwatch.StartNew();
 1182            var reasoning = new StringBuilder();
 1183            var response = new StringBuilder();
 1184            var firstToken = 0L;
 1185            var firstContentToken = 0L;
 1186            var endTokens = 0L;
 187
 2188            await foreach (var delta in ChatService.GetCompletionsAsync(_cts.Token))
 189            {
 0190                if (firstToken == 0)
 191                {
 0192                    firstToken = sw.ElapsedMilliseconds;
 193                }
 0194                if (!string.IsNullOrEmpty(delta.ReasoningContent))
 195                {
 0196                    reasoning.Append(delta.ReasoningContent);
 0197                    assistantMessage.ReasoningContent = reasoning.ToString();
 198                }
 0199                if (!string.IsNullOrEmpty(delta.Content))
 200                {
 0201                    if (firstContentToken == 0)
 202                    {
 0203                        firstContentToken = sw.ElapsedMilliseconds;
 204                    }
 0205                    response.Append(delta.Content);
 206                    // обновляем сегменты в сообщении
 0207                    MessageParser.UpdateSegments(delta.Content, assistantMessage);
 208                }
 209
 0210                assistantMessage.Model ??= ChatService.LastCompletionsModel;
 211
 0212                await InvokeAsync(StateHasChanged);
 213            }
 214
 1215            endTokens = sw.ElapsedMilliseconds;
 1216            var correctedFirstToken = ProfileManager.ActiveProfile.Stream
 1217                ? firstToken
 1218                : Math.Min(500, endTokens - 500);
 1219            var secForTokens = (endTokens - correctedFirstToken) / 1000f;
 1220            sw.Stop();
 221
 1222            assistantMessage.Content = response.ToString();
 1223            assistantMessage.IsStreaming = false;
 1224            assistantMessage.Timings = new MessageTimings
 1225            {
 1226                FirstToken = TimeSpan.FromMilliseconds(firstToken),
 1227                Reasoning = !string.IsNullOrEmpty(assistantMessage.ReasoningContent)
 1228                    ? TimeSpan.FromMilliseconds(firstContentToken - firstToken)
 1229                    : TimeSpan.Zero,
 1230                Content = TimeSpan.FromMilliseconds(endTokens - firstContentToken),
 1231                Total = TimeSpan.FromMilliseconds(endTokens),
 1232                TokensInSec = ChatService.LastUsage != null && secForTokens > 0
 1233                    ? ChatService.LastUsage.CompletionTokens / secForTokens
 1234                    : 0f
 1235            };
 236
 1237            if (ChatService.FinishReason?.Equals("length", StringComparison.OrdinalIgnoreCase) == true)
 238            {
 0239                NotificationService.Notify(new NotificationMessage
 0240                {
 0241                    Severity = NotificationSeverity.Error,
 0242                    Summary = SharedResource.ErrorFinishByLength,
 0243                    Detail = string.Empty,
 0244                    Duration = 30_000,
 0245                    ShowProgress = true,
 0246                });
 247            }
 248
 1249            if (!string.IsNullOrEmpty(ChatService.LastError))
 250            {
 0251                NotificationService.Notify(new NotificationMessage
 0252                {
 0253                    Severity = NotificationSeverity.Error,
 0254                    Summary = ChatService.LastError,
 0255                    Detail = string.Empty,
 0256                    Duration = 30_000,
 0257                    ShowProgress = true,
 0258                });
 259            }
 260
 1261            ParsePlan(assistantMessage);
 262
 1263            await ChatService.SaveSessionAsync();
 264
 265            // TODO: надо подумать что делать, если прервался на незакрытом тулзе...
 266            // Думаю нужно выдавать ошибку модели
 1267            await HandleToolCallAsync(assistantMessage, [.. assistantMessage.Segments.Where(s => s.Type == SegmentType.T
 1268        }
 0269        catch (OperationCanceledException) when (_cts.IsCancellationRequested)
 270        {
 271            // если вручную отменили, тогда не включать повторы
 0272            if (string.IsNullOrEmpty(assistantMessage.Content))
 273            {
 0274                assistantMessage.Content = "Cancelled by user...";
 0275                MessageParser.UpdateSegments(assistantMessage.Content, assistantMessage);
 276            }
 0277        }
 0278        catch (Exception ex)
 279        {
 0280            var maxRetries = CommonSettingsProvider.Current.MaxRetries;
 0281            assistantMessage.Content = $"Error: {ex.Message} [{retryCount}/{maxRetries}]";
 282            // обновляем сегменты в сообщении
 0283            MessageParser.UpdateSegments(assistantMessage.Content, assistantMessage);
 0284            Logger.LogError(ex, "Getting response error");
 285
 0286            if (retryCount < maxRetries)
 287            {
 0288                retryCount++;
 0289                var delay = GetRetryDelay(retryCount);
 290
 0291                assistantMessage.MaxRetryAttempts = maxRetries;
 0292                assistantMessage.RetryAttempt = retryCount;
 293
 294                try
 295                {
 0296                    for (var i = delay; i > 0; i--)
 297                    {
 0298                        assistantMessage.RetryCountdown = i;
 0299                        await InvokeAsync(StateHasChanged);
 0300                        await Task.Delay(1000, _cts.Token);
 301                    }
 0302                }
 0303                catch (OperationCanceledException)
 304                {
 0305                    assistantMessage.RetryCountdown = 0;
 0306                    return;
 307                }
 308
 0309                assistantMessage.RetryCountdown = 0;
 0310                ChatService.Session.RemoveMessage(assistantMessage.Id);
 0311                IsLoading = false;
 0312                await GetAiResponseInternalAsync(retryCount);
 313            }
 314        }
 315        finally
 316        {
 1317            assistantMessage.IsStreaming = false;
 1318            IsLoading = false;
 1319            await InvokeAsync(StateHasChanged);
 320        }
 1321    }
 322
 323    private static int GetRetryDelay(int attempt)
 324    {
 0325        return attempt switch
 0326        {
 0327            1 => 2,
 0328            2 => 5,
 0329            3 => 10,
 0330            _ => 20
 0331        };
 332    }
 333
 334    private static void ParsePlan(VisualChatMessage message)
 335    {
 2336        if (string.IsNullOrEmpty(message.Content)) return;
 337
 0338        var planRegex = new Regex(@"<plan>(?<plan>.*?)</plan>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
 0339        var match = planRegex.Match(message.Content);
 0340        if (match.Success)
 341        {
 0342            message.PlanContent = match.Groups["plan"].Value.Trim();
 343            // Remove the plan block from display content to avoid double showing
 0344            message.DisplayContent = planRegex.Replace(message.DisplayContent ?? message.Content, string.Empty).Trim();
 0345            if (string.IsNullOrEmpty(message.DisplayContent))
 346            {
 0347                message.DisplayContent = "Proposed Plan:";
 348            }
 349        }
 0350    }
 351
 352    private async Task ExecutePlanAsync(VisualChatMessage message)
 353    {
 0354        if (!message.HasPlan) return;
 355
 356        // Switch mode to Agent
 0357        ChatService.Session.Mode = AppMode.Agent;
 358
 359        // Send confirmation message to start implementation
 0360        await SendMessageAsync("Implement the plan.");
 0361    }
 362
 363    private async Task HandleToolApprovalAsync((string MessageId, string SegmentId, bool Approved) args)
 364    {
 365        // оно всегда должно быть, потому что мы блокируем UI,
 366        // пока не придет ответ от модели, и юзер не может кликнуть раньше времени. Но на всякий случай проверим.
 0367        var message = Messages.FirstOrDefault(m => m.Id == args.MessageId);
 0368        if (message != null)
 369        {
 0370            var status = args.Approved ? ToolApprovalStatus.Approved : ToolApprovalStatus.Rejected;
 0371            message.Segments.FirstOrDefault(s => s.Id == args.SegmentId)?.ApprovalStatus = status;
 0372            await InvokeAsync(StateHasChanged);
 373
 0374            if (_approvalWaiters.TryRemove($"{args.MessageId}_{args.SegmentId}", out var tcs))
 375            {
 0376                tcs.SetResult(status);
 377            }
 378        }
 0379    }
 380
 381    private async Task<VsToolResult> CallToolAsync(Tool tool, ContentSegment segment)
 382    {
 0383        if (segment.ApprovalStatus != ToolApprovalStatus.Approved)
 384        {
 0385            return new VsToolResult
 0386            {
 0387                Name = segment.ToolName,
 0388                Success = false,
 0389                ErrorMessage = "Execution was denied by user."
 0390            };
 391        }
 392
 0393        if (segment.ToolName.StartsWith("mcp__"))
 394        {
 395            // для MCP десериализуем параметры
 0396            var args = JsonUtils.DeserializeParameters(string.Join('\n', segment.ToolParams.Values))
 0397                .Where(x => x.Value is not null).ToDictionary(); // удаляем null-ы
 0398            return await tool.ExecuteAsync(args, _cts.Token);
 399        }
 400
 0401        return await tool.ExecuteAsync(segment.ToolParams, _cts.Token);
 0402    }
 403
 404    private async Task HandleToolCallAsync(VisualChatMessage assistantMessage, List<ContentSegment> toolsSegments)
 405    {
 1406        if (toolsSegments.Count == 0)
 407        {
 1408            return;
 409        }
 410
 0411        _approvalWaiters.Clear();
 412
 0413        foreach (var segment in toolsSegments)
 414        {
 0415            if (_cts.Token.IsCancellationRequested)
 416            {
 0417                return;
 418            }
 419
 0420            var tool = ToolManager.GetTool(segment.ToolName);
 421
 422            VsToolResult vsToolResult;
 0423            if (tool == null)
 424            {
 0425                vsToolResult = new VsToolResult
 0426                {
 0427                    Name = segment.ToolName,
 0428                    Success = false,
 0429                    ErrorMessage = "Tool not found."
 0430                };
 431            }
 432            else
 433            {
 434                // Спрашиваем разрешение если нужно
 0435                if (segment.ApprovalStatus == ToolApprovalStatus.Pending)
 436                {
 0437                    var tcs = new TaskCompletionSource<ToolApprovalStatus>();
 0438                    var waiterKey = $"{assistantMessage.Id}_{segment.Id}";
 0439                    _approvalWaiters[waiterKey] = tcs;
 440
 441                    try
 442                    {
 443                        // Ждем аппрува от пользователя или отмены всего стрима
 0444                        segment.ApprovalStatus = await tcs.Task.WaitAsync(_cts.Token);
 0445                    }
 0446                    catch (OperationCanceledException)
 447                    {
 0448                        _approvalWaiters.Clear();
 0449                        return;
 450                    }
 451                    finally
 452                    {
 0453                        _approvalWaiters.TryRemove(waiterKey, out _);
 454                    }
 0455                }
 456
 0457                if (_cts.Token.IsCancellationRequested)
 458                {
 0459                    return;
 460                }
 461
 462                // Уже должен быть известен статус тулза - или разрешен, или запрещен.
 0463                vsToolResult = await CallToolAsync(tool, segment);
 464            }
 465#if DEBUG
 466            // Безголовые (без Visual Studio) тесты
 467            vsToolResult = HeadlessMocker.GetVsToolResult(vsToolResult);
 468            Logger.LogTrace("{request} >>>>>> {result}", JsonUtils.Serialize(tool), JsonUtils.Serialize(vsToolResult));
 469#endif
 470            // вложенные тулзы — часть сообщения ассистента
 0471            assistantMessage.ToolResults.Add(ToolResult.Convert(vsToolResult, tool.DisplayName, tool.Name));
 472
 0473            await InvokeAsync(StateHasChanged);
 0474        }
 475
 0476        await ChatService.SaveSessionAsync();
 477
 0478        if (_cts.Token.IsCancellationRequested)
 479        {
 0480            return;
 481        }
 482
 0483        await GetAiResponseAsync();
 1484    }
 485
 486    private void LoadMessagesFromSession()
 487    {
 6488        foreach (var chatMessage in Messages)
 489        {
 1490            if (chatMessage.Role == ChatMessageRole.Assistant)
 491            {
 0492                ParsePlan(chatMessage);
 493            }
 494
 1495            chatMessage.IsExpanded = IsShortMessage(chatMessage.DisplayContent ?? chatMessage.Content);
 1496            MessageParser.UpdateSegments(chatMessage.Content, chatMessage, isHistory: true);
 497
 498            // восстанавливаем ToolDisplayName для вложенных тулзов
 2499            foreach (var toolMsg in chatMessage.ToolResults)
 500            {
 0501                toolMsg.DisplayName = ToolResult.GetDisplayName(toolMsg.Success, ToolManager.GetTool(toolMsg.Name)?.Disp
 502            }
 503        }
 504
 2505        InvokeAsync(StateHasChanged);
 2506    }
 507
 508    private static bool IsShortMessage(string content)
 18509        => string.IsNullOrEmpty(content) || (content.Length < 1000 && content.Count(c => c == '\n') < 15);
 510
 0511    private async Task CancelResponseAsync() => await _cts.CancelAsync();
 512
 513    protected override async Task OnInitializedAsync()
 514    {
 17515        await base.OnInitializedAsync();
 17516        _dotNetRef = DotNetObjectReference.Create(this);
 517
 17518        ChatService.SessionChanged += HandleSessionChanged;
 519
 17520        ToolManager.RegisterAllTools();
 521
 17522        await VsBridge.InitializeAsync();
 17523        await InvokeAsync(StateHasChanged);
 17524    }
 525
 526    private void HandleSessionChanged(object? sender, PropertyChangedEventArgs e)
 527    {
 2528        if (e.PropertyName != nameof(ConversationSession))
 0529            return;
 530
 2531        LoadMessagesFromSession();
 2532        InvokeAsync(StateHasChanged);
 2533    }
 534
 535    protected override async Task OnAfterRenderAsync(bool firstRender)
 536    {
 26537        if (firstRender)
 538        {
 17539            await JsRuntime.InvokeVoidAsync("setChatHandler", _dotNetRef);
 540        }
 26541    }
 542
 543    private async Task OnProfileChangeAsync(object value)
 544    {
 1545        var profileId = value as string;
 1546        if (!string.IsNullOrEmpty(profileId))
 547        {
 1548            await ProfileManager.ActivateProfileAsync(profileId);
 1549            NotificationService.Notify(new NotificationMessage
 1550            {
 1551                Severity = NotificationSeverity.Info,
 1552                Summary = "Profile Changed",
 1553                Detail = $"Active profile updated.",
 1554                Duration = 1000
 1555            });
 556        }
 1557    }
 558
 559    private static void OnEditMessage(VisualChatMessage message)
 560    {
 0561        message.TempContent = message.Content;
 0562        message.IsEditing = true;
 0563    }
 564
 565    private static void OnCancelEdit(VisualChatMessage message)
 566    {
 0567        message.IsEditing = false;
 0568        message.TempContent = string.Empty;
 0569    }
 570
 571    private async Task OnSaveEditAsync(VisualChatMessage message)
 572    {
 0573        message.Content = message.TempContent;
 574
 575        // обновляем сегменты в сообщении
 0576        message.Segments.Clear();
 0577        MessageParser.UpdateSegments(message.Content, message);
 578
 0579        message.IsEditing = false;
 580
 581        // Update display content if it's an assistant message with tools
 0582        ParsePlan(message);
 583
 0584        ChatService.Session.UpdateMessage(message.Id, message.Content);
 0585        await ChatService.SaveSessionAsync();
 0586        await InvokeAsync(StateHasChanged);
 0587    }
 588
 589    private async Task OnDeleteMessageAsync(VisualChatMessage message)
 590    {
 0591        ChatService.Session.RemoveMessage(message.Id);
 0592        await ChatService.SaveSessionAsync();
 0593        await InvokeAsync(StateHasChanged);
 0594    }
 595
 596    private async Task OnRegenerateLastAsync()
 597    {
 0598        var lastAssistantMessage = Messages.LastOrDefault(m => m.Role == ChatMessageRole.Assistant);
 0599        if (lastAssistantMessage != null)
 600        {
 0601            ChatService.Session.RemoveMessage(lastAssistantMessage.Id);
 0602            await GetAiResponseAsync();
 603        }
 0604    }
 605
 606    private async Task OnShowSettingsAsync()
 607    {
 1608        _callSettings = true;
 609
 1610        StateHasChanged();
 611
 1612        await Task.Yield();
 613
 1614        await DialogService.OpenSideAsync<SettingsDialog>(@SharedResource.Settings,
 1615            options: new SideDialogOptions
 1616            {
 1617                CloseDialogOnOverlayClick = true,
 1618                Resizable = true,
 1619                Position = DialogPosition.Right,
 1620                MinHeight = 250.0,
 1621                MinWidth = 400.0
 1622            });
 0623        _callSettings = false;
 0624    }
 625
 626    /// <inheritdoc />
 627    public override void Dispose()
 628    {
 17629        base.Dispose();
 630
 17631        _dotNetRef?.Dispose();
 17632        ChatService.SessionChanged -= HandleSessionChanged;
 633
 17634        _cts?.Cancel();
 17635        _cts?.Dispose();
 636
 17637        GC.SuppressFinalize(this);
 17638    }
 639}