From d06a93f759b5bd5fb30ad1f28040c0bf6457d69f Mon Sep 17 00:00:00 2001 From: lanyizi Date: Fri, 21 Aug 2026 15:34:24 +0200 Subject: [PATCH] ai plan v2 --- AIChatPanel.xaml.cs | 460 ++++++++++-- AIProviderSettingsControl.xaml | 15 +- AIProviderSettingsControl.xaml.cs | 12 + AiV2.Tests/AiV2.Tests.csproj | 22 + AiV2.Tests/Program.cs | 697 ++++++++++++++++++ AnotherReplayReader.csproj | 10 +- EventDump.xaml.cs | 45 +- PLAN_ai_analysis_v2.md | 299 ++++++++ Properties/AssemblyInfo.cs | 3 + ReplayFile/RA3Commands.cs | 2 +- Utils/AIAnalysisValidation.cs | 386 ++++++++-- Utils/AIAnalyze.cs | 341 +++------ Utils/AiContextBudget.cs | 79 ++ Utils/AiKnowledge.cs | 270 +++++-- Utils/AiSettings.cs | 5 + Utils/AiV2Pipeline.cs | 540 ++++++++++++++ Utils/ReplayFactIndex.cs | 358 +++++++-- knowledge_corona.md | 36 +- knowledge_default.md | 36 +- ...units.json => knowledge_units_default.json | 0 20 files changed, 3093 insertions(+), 523 deletions(-) create mode 100644 AiV2.Tests/AiV2.Tests.csproj create mode 100644 AiV2.Tests/Program.cs create mode 100644 PLAN_ai_analysis_v2.md create mode 100644 Utils/AiContextBudget.cs create mode 100644 Utils/AiV2Pipeline.cs rename knowledge_units.json => knowledge_units_default.json (100%) diff --git a/AIChatPanel.xaml.cs b/AIChatPanel.xaml.cs index 6f8a2d4..3c428ae 100644 --- a/AIChatPanel.xaml.cs +++ b/AIChatPanel.xaml.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.Tracing; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -64,6 +65,20 @@ namespace AnotherReplayReader private TimeIndexedPrefixSums? _eventCounts; private ReplayFactIndex? _factIndex; + // v2 管线状态 + private Replay? _replay; + private ImmutableSortedDictionary _players = ImmutableSortedDictionary.Empty; + private string? _systemPrompt; + private string? _replayData; + private ImmutableArray _eventSpans = ImmutableArray.Empty; + private ImmutableArray _slices = ImmutableArray.Empty; + private string? _digest; + private OverviewResult _overview = new(string.Empty, ImmutableArray.Empty); + private string? _overviewNarrative; + private readonly List _findings = new(); + private readonly List _segmentResponses = new(); + private int _currentSegmentIndex; + // 分析状态 private bool _isRunning; private bool _isFailed; @@ -79,6 +94,8 @@ namespace AnotherReplayReader private bool _thinkBlockWasExpanded = false; // 输出段落 private (Paragraph? Think, Paragraph? Content)? _currentContent; + private Paragraph? _lastContentParagraph; + private bool _suppressDisplay; // 进度计时 private TimeSpan _previousTotalTime; @@ -167,11 +184,25 @@ namespace AnotherReplayReader _analyzer = null; _eventCounts = null; _factIndex = null; + _replay = null; + _players = ImmutableSortedDictionary.Empty; + _systemPrompt = null; + _replayData = null; + _eventSpans = ImmutableArray.Empty; + _slices = ImmutableArray.Empty; + _digest = null; + _overview = new(string.Empty, ImmutableArray.Empty); + _overviewNarrative = null; + _findings.Clear(); + _segmentResponses.Clear(); + _currentSegmentIndex = 0; _document.Blocks.Clear(); _updateCurrentThinkingSection = null; _thinkBlockWasExpanded = false; _currentContent = null; + _lastContentParagraph = null; + _suppressDisplay = false; _previousTotalTime = TimeSpan.Zero; _previousTotalChars = 0; @@ -201,6 +232,7 @@ namespace AnotherReplayReader string replayData, TimeIndexedPrefixSums eventCounts, ReplayFactIndex factIndex, + ImmutableArray eventSpans, CancellationToken externalToken) { if (_isRunning) @@ -249,54 +281,79 @@ namespace AnotherReplayReader _analyzer = new AIAnalyze(); _eventCounts = eventCounts; _factIndex = factIndex; + _replay = replay; + _players = players; + _replayData = replayData; + _eventSpans = eventSpans; FinishCurrentContent(); var requestContext = GetRequestContext(); + _systemPrompt = AIAnalyze.GetSystemPrompt(replay, players, GetPromptSettings?.Invoke()); - var systemPrompt = AIAnalyze.GetSystemPrompt(replay, players, GetPromptSettings?.Invoke()); - var userPrompt = AIAnalyze.BuildUserPrompt(replay.Mod, players, replayData, out var userPromptPrefix); - AppendLog($"让 AI 了解录像...", - userPromptPrefix - + "[输入:操作信息流水账]\r\n" + // ---- 机械分段(M2) ---- + var model = requestContext.Model; + var budget = AiContextBudget.GetContextBudget(model); + var hardCap = model.ContextLength > 0 + ? (int)(model.ContextLength * AiContextBudget.HardUsageRatio) + : 48_000; + var effectiveBudget = budget > 0 ? budget : hardCap; + var headroom = AiContextBudget.GetOutputHeadroom(requestContext.Provider, model); + var fixedOverhead = AiContextBudget.EstimateTokens(_systemPrompt) + + 4_000 // digest + + 2_000 // overview + + 1_500 // instruction + + 10_000 // findings reserve + + headroom; + var sliceBudget = Math.Max(effectiveBudget - fixedOverhead, MechanicalSegmenter.MinSliceTokens); + var (slices, sliceWarnings) = MechanicalSegmenter.Slice( + replayData, eventSpans, sliceBudget, sliceBudget / 12); + _slices = slices; + foreach (var warning in sliceWarnings) + { + AppendLog("分段警告", warning, false); + } + if (_slices.IsEmpty) + { + throw new InvalidOperationException("操作记录为空,无法分析。"); + } + + AppendLog( + "v2 管线准备", + $"上下文预算: {FormatNumber(budget, false)} token(0 = 不支持长录像)\r\n" + + $"分段预算: {FormatNumber(sliceBudget, false)} token,实际切分 {_slices.Length} 段\r\n" + + $"日志总量: {FormatNumber(AiContextBudget.EstimateTokens(replayData), false)} token\r\n" + $"[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]", - true); + false); - #region info - var (systemPromptSize, systemPromptTokenCount) = AIAnalyze.EstimateTokenCount(systemPrompt); - var (userPromptSize, userPromptTokenCount) = AIAnalyze.EstimateTokenCount(userPrompt); - var totalPromptSize = systemPromptSize + userPromptSize; - var totalPromptTokenCount = systemPromptTokenCount + userPromptTokenCount; - var estimateText = $"系统提示词: {FormatNumber(systemPromptSize, true, "B")},约 {FormatNumber(systemPromptTokenCount, false)} Token\r\n" + - $"初始用户提示词: {FormatNumber(userPromptSize, true, "B")},约 {FormatNumber(userPromptTokenCount, false)} Token\r\n" + - $"总大小: {FormatNumber(totalPromptSize, true, "B")},约 {FormatNumber(totalPromptTokenCount, false)} Token"; - AppendLog("初始阶段 Token 用量预测", estimateText, false); - #endregion + // ---- 对局摘要 ---- + _digest = MatchDigestBuilder.Build(factIndex, players, replay.Mod, _slices, replayData); + AppendLog("对局摘要", _digest, true); - var result = await _analyzer.AnalyzeAsync( - systemPrompt, - userPrompt, + // ---- 总览轮 ---- + _phaseText.Text = "正在生成总览..."; + var overviewMessages = BuildSystemMessages(_systemPrompt, _digest, null) + .Add(new AIAnalyze.ChatMessage("user", AIAnalyze.BuildOverviewUserPrompt(_slices))); + CheckAndLogContextUsage(overviewMessages, requestContext); + var overviewResult = await _analyzer.CompleteAsync( + overviewMessages, requestContext, OnChunk, _linkedCts.Token); - - UpdateTokenDisplay(result); - - var eventCountsBuilder = new StringBuilder(); - foreach (var (Start, End, Description) in result.State.Segments) + UpdateTokenDisplay(overviewResult); + _overview = OverviewParser.Parse(overviewResult.Response); + _overviewNarrative = _overview.Narrative; + AppendLog("整局总览", overviewResult.Response, false); + if (_overview.Segments.IsEmpty) { - var count = eventCounts.Query(Start, End); - eventCountsBuilder.AppendLine($"[{Start:mm\\:ss\\.ff} - {End:mm\\:ss\\.ff}],事件数: {count}"); + AppendLog("总览解析警告", "未解析到 [分段概述] 块,将直接使用机械分段。", false); } - AppendLog("各分段事件数量", eventCountsBuilder.ToString(), false); - // 分析完成,分段列表就绪 - _lastSuccessfulSegment = 0; + // ---- 分段分析循环 ---- + _currentSegmentIndex = 0; + await ProcessSegmentsAsync(requestContext); - // 分段分析循环 - await ProcessSegmentsAsync(result.State, eventCounts); - - // 总结 - await ProcessSummaryAsync(eventCounts); + // ---- 总结 ---- + await ProcessSummaryAsync(requestContext); // 成功结束 _phaseText.Text = "分析完成"; @@ -328,42 +385,113 @@ namespace AnotherReplayReader } } - // ---- 分析阶段 ---- - private async Task ProcessSegmentsAsync(AIAnalyze.State lastState, - TimeIndexedPrefixSums eventCounts) + // ---- 分析阶段(v2) ---- + private async Task ProcessSegmentsAsync(AiRequestContext requestContext) { - _analyzer!.SetState(lastState); - while (lastState.CurrentSegment < lastState.Segments.Count) + if (_linkedCts is null || _replayData is null || _systemPrompt is null || _digest is null || _replay is null) + { + return; + } + + var playerNameToIndex = AIAnalyze.PlayerNamesForAI(_replay.Mod, _players) + .ToDictionary(kv => kv.Value, kv => kv.Key); + var structuredKnowledge = StructuredKnowledge.GetForMod( + AIAnalyze.GetKnowledgeModName(_replay)); + + while (_currentSegmentIndex < _slices.Length) { - if (_linkedCts is null) - { - break; - } _linkedCts.Token.ThrowIfCancellationRequested(); - var seg = lastState.Segments[lastState.CurrentSegment]; - var segmentIndex = lastState.CurrentSegment + 1; - var totalSegments = lastState.Segments.Count; + var slice = _slices[_currentSegmentIndex]; + var segmentIndex = _currentSegmentIndex + 1; + var totalSegments = _slices.Length; _phaseText.Text = $"正在分析第 {segmentIndex}/{totalSegments} 段"; FinishCurrentContent(); // 记录回滚点 _blockCountBeforeSegment = _document.Blocks.Count; + _lastContentParagraph = null; - // 计算事件数 - var eventCount = eventCounts.Query(seg.Start, seg.End); - var segmentPrompt = AIAnalyze.BuildSegmentUserPrompt(lastState.Segments, lastState.CurrentSegment, eventCount); - var requestContext = GetRequestContext!(); - AppendLog($"让 AI 分析第{segmentIndex}段...", - segmentPrompt - + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]", + var eventCount = _eventCounts?.Query(slice.Start, slice.End) ?? slice.EventCount; + var title = _overview.Segments.FirstOrDefault(s => s.Index == segmentIndex)?.Title; + var sliceText = slice.GetText(_replayData); + var findingsText = string.Join("\n\n", _findings); + var instruction = AIAnalyze.BuildSegmentUserPromptV2( + _currentSegmentIndex, totalSegments, slice, eventCount, title); + + var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative) + .Add(new AIAnalyze.ChatMessage("user", sliceText)); + if (!string.IsNullOrWhiteSpace(findingsText)) + { + messages = messages.Add(new AIAnalyze.ChatMessage( + "user", "之前各段的已发现事实:\n" + findingsText)); + } + messages = messages.Add(new AIAnalyze.ChatMessage("user", instruction)); + + AppendLog( + $"让 AI 分析第{segmentIndex}段...", + instruction + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]", true); - var segmentResult = await _analyzer.ContinueAnalyzeAsync( - segmentPrompt, - requestContext, - OnChunk, - _linkedCts.Token); - var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims(segmentResult.Response, _factIndex); + CheckAndLogContextUsage(messages, requestContext); + + // 回查循环(M3):同一段会话内,模型可多次请求远处原始区间 + const int maxBackqueriesPerSegment = 3; + var segmentMessages = messages; + var backqueryCount = 0; + string segmentResponse; + while (true) + { + var segmentResult = await _analyzer!.CompleteAsync( + segmentMessages, + requestContext, + OnChunk, + _linkedCts.Token); + UpdateTokenDisplay(segmentResult); + segmentResponse = segmentResult.Response; + + if (backqueryCount >= maxBackqueriesPerSegment) + { + break; + } + + var backqueries = BackqueryParser.Parse(segmentResponse); + var pendingTexts = new List(); + foreach (var (start, end) in backqueries) + { + if (backqueryCount + pendingTexts.Count >= maxBackqueriesPerSegment) + { + AppendLog("回查限制", "本段回查次数已达上限,剩余区间已忽略。", false); + break; + } + var (text, reason) = BackquerySliceExtractor.Extract( + _replayData, _eventSpans, start, end); + if (text is null) + { + AppendLog("回查失败", reason ?? "未知原因", false); + continue; + } + pendingTexts.Add( + $"[回查 {MatchDigestBuilder.FormatTime(start)}~{MatchDigestBuilder.FormatTime(end)}]\n" + text); + } + + if (pendingTexts.Count == 0) + { + break; + } + backqueryCount += pendingTexts.Count; + segmentMessages = segmentMessages + .Add(new AIAnalyze.ChatMessage("assistant", segmentResponse)) + .Add(new AIAnalyze.ChatMessage( + "user", AIAnalyze.BuildBackqueryUserPrompt(string.Join("\n\n", pendingTexts)))); + AppendLog( + $"第{segmentIndex}段回查(第 {backqueryCount} 次)", + $"已提供 {pendingTexts.Count} 个区间,继续分析。", + true); + } + + FinishCurrentContent(); + var validationResult = AIAnalysisValidation.ValidateMachineReadableClaims( + segmentResponse, _factIndex, playerNameToIndex, structuredKnowledge); if (validationResult.HasIssues) { AppendLog( @@ -371,16 +499,91 @@ namespace AnotherReplayReader AIAnalysisValidation.FormatIssues(validationResult.Issues), false); } - _lastSuccessfulSegment = lastState.CurrentSegment; - lastState = segmentResult.State; - UpdateTokenDisplay(segmentResult); + + // 隐藏修订 pass(M6):Contradiction 触发,最多 1 次;修订期间不流式显示 + var finalResponse = segmentResponse; + var finalValidation = validationResult; + if (validationResult.RequiresRevision) + { + var issueCount = validationResult.Issues.Count( + i => i.Severity == AIValidationSeverity.Contradiction); + AppendLog( + $"第{segmentIndex}段修订", + $"验证器发现 {issueCount} 个矛盾问题,正在请求 AI 修正...", + false); + var relevantFacts = RelevantFactsFormatter.Format(validationResult.Claims, _factIndex!); + var revisionPrompt = AIAnalyze.BuildRevisionUserPrompt( + segmentResponse, + AIAnalysisValidation.FormatIssues(validationResult.Issues), + string.IsNullOrWhiteSpace(relevantFacts) ? "(无额外事实)" : relevantFacts); + var revisionMessages = segmentMessages + .Add(new AIAnalyze.ChatMessage("assistant", segmentResponse)) + .Add(new AIAnalyze.ChatMessage("user", revisionPrompt)); + CheckAndLogContextUsage(revisionMessages, requestContext); + + _suppressDisplay = true; + AIAnalyze.Result revisionResult; + try + { + revisionResult = await _analyzer!.CompleteAsync( + revisionMessages, + requestContext, + OnChunk, + _linkedCts.Token); + } + finally + { + _suppressDisplay = false; + } + UpdateTokenDisplay(revisionResult); + + if (string.IsNullOrWhiteSpace(revisionResult.Response)) + { + AppendLog("修订失败", "修订输出为空,保留原分析。", false); + } + else + { + finalResponse = revisionResult.Response; + finalValidation = AIAnalysisValidation.ValidateMachineReadableClaims( + revisionResult.Response, _factIndex, playerNameToIndex, structuredKnowledge); + ReplaceSegmentContent(finalResponse); + AppendLog( + "修订完成", + "已采用修正后的分析。" + (finalValidation.HasIssues + ? "\n仍存在的问题:\n" + AIAnalysisValidation.FormatIssues(finalValidation.Issues) + : string.Empty), + false); + } + } + FinishCurrentContent(); + + // 追加已发现事实 + var entry = new StringBuilder(); + var summary = ClaimFindingsFormatter.ExtractSummary(finalResponse); + if (!string.IsNullOrWhiteSpace(summary)) + { + entry.AppendLine($"[小结] {summary}"); + } + var claimsText = ClaimFindingsFormatter.Format(finalValidation.Claims); + if (!string.IsNullOrWhiteSpace(claimsText)) + { + entry.AppendLine(claimsText); + } + if (entry.Length > 0) + { + _findings.Add(entry.ToString().TrimEnd()); + } + + _segmentResponses.Add(finalResponse); + _lastSuccessfulSegment = _currentSegmentIndex; + _currentSegmentIndex++; AppendLog($"第{segmentIndex}段完成。", null, true); } } - private async Task ProcessSummaryAsync(TimeIndexedPrefixSums eventCounts) + private async Task ProcessSummaryAsync(AiRequestContext requestContext) { - if (_linkedCts is null) + if (_linkedCts is null || _systemPrompt is null || _digest is null) { return; } @@ -390,16 +593,37 @@ namespace AnotherReplayReader FinishCurrentContent(); _blockCountBeforeSegment = _document.Blocks.Count; - var totalEvents = eventCounts.GetTotal(); - var finalPrompt = AIAnalyze.BuildFinalUserPrompt(totalEvents); - var requestContext = GetRequestContext!(); - AppendLog($"让 AI 生成总结...", - finalPrompt - + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]", + var totalEvents = _eventCounts?.GetTotal() ?? 0; + var analyses = string.Join("\n\n", _segmentResponses); + var budget = AiContextBudget.GetContextBudget(requestContext.Model); + var maxAnalysesTokens = budget > 0 ? Math.Max(20_000, budget / 3) : 40_000; + var truncated = TruncateByTokens(analyses, maxAnalysesTokens); + if (!ReferenceEquals(truncated, analyses)) + { + AppendLog( + "总结输入警告", + $"各段分析过长,已截断到约 {FormatNumber(maxAnalysesTokens, false)} token。", + false); + } + + var finalPrompt = AIAnalyze.BuildSummaryUserPromptV2(totalEvents); + var findingsText = string.Join("\n\n", _findings); + var messages = BuildSystemMessages(_systemPrompt, _digest, _overviewNarrative) + .Add(new AIAnalyze.ChatMessage("user", "各分段的推理分析:\n" + truncated)); + if (!string.IsNullOrWhiteSpace(findingsText)) + { + messages = messages.Add(new AIAnalyze.ChatMessage( + "user", "各分段的已发现事实:\n" + findingsText)); + } + messages = messages.Add(new AIAnalyze.ChatMessage("user", finalPrompt)); + + AppendLog( + "让 AI 生成总结...", + finalPrompt + $"\r\n[AI: {requestContext.Provider.Name}/{requestContext.Model.ModelId}]", true); - - var result = await _analyzer!.FinishAnalyzeAsync( - finalPrompt, + CheckAndLogContextUsage(messages, requestContext); + var result = await _analyzer!.CompleteAsync( + messages, requestContext, OnChunk, _linkedCts.Token); @@ -407,6 +631,53 @@ namespace AnotherReplayReader UpdateTokenDisplay(result); } + // ---- v2 管线辅助 ---- + private static ImmutableList BuildSystemMessages( + string systemPrompt, + string digest, + string? overviewNarrative) + { + var content = systemPrompt + "\n\n# 对局摘要\n" + digest; + if (!string.IsNullOrWhiteSpace(overviewNarrative)) + { + content += "\n\n# 整局总览\n" + overviewNarrative; + } + return ImmutableList.Empty + .Add(new AIAnalyze.ChatMessage("system", content)); + } + + private void CheckAndLogContextUsage( + ImmutableList messages, + AiRequestContext requestContext) + { + var total = 0; + foreach (var message in messages) + { + total += AiContextBudget.EstimateTokens(message.Content); + } + var check = AiContextBudget.CheckPromptUsage( + total, requestContext.Provider, requestContext.Model); + if (check.Block) + { + throw new InvalidOperationException(check.Message); + } + if (!check.IsOk) + { + AppendLog("上下文预算警告", check.Message, false); + } + } + + private static string TruncateByTokens(string text, int maxTokens) + { + if (AiContextBudget.EstimateTokens(text) <= maxTokens) + { + return text; + } + // 中文约 1 token/字,取保守系数 1.1 字符/token + var chars = Math.Min(text.Length, (int)(maxTokens * 1.1)); + return text.Substring(0, chars) + "\n…(已截断)"; + } + // ---- 块追加与折叠 ---- private void OnChunk(AIAnalyze.AIChunk chunk) { @@ -444,9 +715,28 @@ namespace AnotherReplayReader private void StartContentParagraph() { _currentContent = (_currentContent?.Think, Content: new Paragraph()); + _lastContentParagraph = _currentContent.Value.Content; _document.Blocks.Add(_currentContent.Value.Content); } + private void ReplaceSegmentContent(string text) + { + FinishCurrentContent(); + // 移除本段已显示的全部块(草稿/回查中间内容),只保留最终修正版 + while (_document.Blocks.Count > _blockCountBeforeSegment) + { + _document.Blocks.Remove(_document.Blocks.LastBlock); + } + _currentContent = null; + _lastContentParagraph = null; + StartContentParagraph(); + _lastContentParagraph!.Inlines.Add(new Run(text) + { + Foreground = ContentStyle.Foreground, + FontSize = ContentStyle.FontSize + }); + } + private void AppendToParagraph(Paragraph? paragraph, string text, ParagraphStyle style) { if (paragraph is null) @@ -610,6 +900,16 @@ namespace AnotherReplayReader { AutoScroll(); + // 修订 pass 期间:不显示中间内容,只统计字符数 + if (_suppressDisplay) + { + while (_chunkQueue.TryDequeue(out var data)) + { + _currentOutputChars += data.Chunk.Text.Length; + } + return; + } + var thinkSb = new StringBuilder(); var contentSb = new StringBuilder(); var errorSb = new StringBuilder(); @@ -738,7 +1038,7 @@ namespace AnotherReplayReader MessageBox.Show("无法重试:外部取消令牌不可用。", "错误", MessageBoxButton.OK, MessageBoxImage.Error); return; } - if (_analyzer.LastSuccessfulState.CurrentSegment < 0) + if (_overviewNarrative is null) { _canRetry = false; UpdateButtons(); @@ -778,12 +1078,16 @@ namespace AnotherReplayReader _internalCts = new CancellationTokenSource(); _linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalCancelToken, _internalCts.Token); - // 注意:此时 _analyzer.CurrentSegment 停留在失败的那一段 - // 需要继续 ProcessSegmentsAsync,但需要将现有的 result.State 作为起点 - await ProcessSegmentsAsync(_analyzer.LastSuccessfulState, _eventCounts); + // 注意:此时 _currentSegmentIndex 停留在失败的那一段 + // _slices/_findings/_overview 保留,从失败段继续 + if (GetRequestContext() is not { } retryContext) + { + throw new InvalidOperationException("未配置 AI 请求上下文,无法重试。"); + } + await ProcessSegmentsAsync(retryContext); // 总结 - await ProcessSummaryAsync(_eventCounts); + await ProcessSummaryAsync(retryContext); } catch (OperationCanceledException) { diff --git a/AIProviderSettingsControl.xaml b/AIProviderSettingsControl.xaml index 9b90ed5..96be9b2 100644 --- a/AIProviderSettingsControl.xaml +++ b/AIProviderSettingsControl.xaml @@ -139,6 +139,7 @@ + @@ -154,25 +155,29 @@