diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 1ec002f9..642dccc7 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml index f2fa736d..c1ebdd6c 100644 --- a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml +++ b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml @@ -144,132 +144,130 @@ - - - WULA_StorageBase - (13,14) - - - (5, 0, 6) - - - -
  • (6,0,6,0)
  • -
  • (6,12,6,12)
  • -
    -
    - - -
  • (0,6,0,6)
  • -
  • (12,6,12,6)
  • -
    - Clockwise -
    - - -
  • (6,1,6,11)
  • -
  • (1,6,5,6)
  • -
  • (7,6,11,6)
  • -
    -
    - - -
  • (4,1,4,1)
  • -
  • (8,1,8,1)
  • -
    - Opposite -
    - - -
  • (1,4,1,4)
  • -
  • (1,8,1,8)
  • -
    - Counterclockwise -
    - - -
  • (11,4,11,4)
  • -
  • (11,8,11,8)
  • -
    - Clockwise -
    - - -
  • (4,11,4,11)
  • -
  • (8,11,8,11)
  • -
    -
    - - (7, 0, 6) - - - -
  • (0,0,5,0)
  • -
  • (7,0,12,0)
  • -
  • (0,1,0,5)
  • -
  • (12,1,12,5)
  • -
  • (0,7,0,12)
  • -
  • (12,7,12,12)
  • -
  • (1,12,5,12)
  • -
  • (7,12,11,12)
  • -
    -
    - - -
  • (3, 0, 2)
  • -
  • (5, 0, 2)
  • -
  • (8, 0, 2)
  • -
  • (10, 0, 2)
  • -
  • (3, 0, 4)
  • -
  • (5, 0, 4)
  • -
  • (8, 0, 4)
  • -
  • (10, 0, 4)
  • -
  • (3, 0, 7)
  • -
  • (5, 0, 7)
  • -
  • (8, 0, 7)
  • -
  • (10, 0, 7)
  • -
  • (3, 0, 9)
  • -
  • (5, 0, 9)
  • -
  • (8, 0, 9)
  • -
  • (10, 0, 9)
  • -
    - Opposite - WULA_Alloy -
    - - -
  • (2, 0, 3)
  • -
  • (4, 0, 3)
  • -
  • (7, 0, 3)
  • -
  • (9, 0, 3)
  • -
  • (2, 0, 5)
  • -
  • (4, 0, 5)
  • -
  • (7, 0, 5)
  • -
  • (9, 0, 5)
  • -
  • (2, 0, 8)
  • -
  • (4, 0, 8)
  • -
  • (7, 0, 8)
  • -
  • (9, 0, 8)
  • -
  • (2, 0, 10)
  • -
  • (4, 0, 10)
  • -
  • (7, 0, 10)
  • -
  • (9, 0, 10)
  • -
    - WULA_Alloy -
    - - (6, 0, 6) - -
    - + + WULA_StorageBase + (13,14) + + + (5, 0, 6) + + + +
  • (6,0,6,0)
  • +
  • (6,12,6,12)
  • +
    +
    + + +
  • (0,6,0,6)
  • +
  • (12,6,12,6)
  • +
    + Clockwise +
    + + +
  • (6,1,6,11)
  • +
  • (1,6,5,6)
  • +
  • (7,6,11,6)
  • +
    +
    + + +
  • (4,1,4,1)
  • +
  • (8,1,8,1)
  • +
    + Opposite +
    + + +
  • (1,4,1,4)
  • +
  • (1,8,1,8)
  • +
    + Counterclockwise +
    + + +
  • (11,4,11,4)
  • +
  • (11,8,11,8)
  • +
    + Clockwise +
    + + +
  • (4,11,4,11)
  • +
  • (8,11,8,11)
  • +
    +
    + + (7, 0, 6) + + + +
  • (0,0,5,0)
  • +
  • (7,0,12,0)
  • +
  • (0,1,0,5)
  • +
  • (12,1,12,5)
  • +
  • (0,7,0,12)
  • +
  • (12,7,12,12)
  • +
  • (1,12,5,12)
  • +
  • (7,12,11,12)
  • +
    +
    + + +
  • (3, 0, 2)
  • +
  • (5, 0, 2)
  • +
  • (8, 0, 2)
  • +
  • (10, 0, 2)
  • +
  • (3, 0, 4)
  • +
  • (5, 0, 4)
  • +
  • (8, 0, 4)
  • +
  • (10, 0, 4)
  • +
  • (3, 0, 7)
  • +
  • (5, 0, 7)
  • +
  • (8, 0, 7)
  • +
  • (10, 0, 7)
  • +
  • (3, 0, 9)
  • +
  • (5, 0, 9)
  • +
  • (8, 0, 9)
  • +
  • (10, 0, 9)
  • +
    + Opposite + WULA_Alloy +
    + + +
  • (2, 0, 3)
  • +
  • (4, 0, 3)
  • +
  • (7, 0, 3)
  • +
  • (9, 0, 3)
  • +
  • (2, 0, 5)
  • +
  • (4, 0, 5)
  • +
  • (7, 0, 5)
  • +
  • (9, 0, 5)
  • +
  • (2, 0, 8)
  • +
  • (4, 0, 8)
  • +
  • (7, 0, 8)
  • +
  • (9, 0, 8)
  • +
  • (2, 0, 10)
  • +
  • (4, 0, 10)
  • +
  • (7, 0, 10)
  • +
  • (9, 0, 10)
  • +
    + WULA_Alloy +
    + + (6, 0, 6) + +
    +
  • (0,1,12,12)
  • -
    - +
    WULA_KitchenBase @@ -417,160 +415,358 @@ - + + WULA_HospitalBase + (13,14) + + + +
  • (1,2,1,2)
  • +
  • (1,10,1,10)
  • +
    + Clockwise +
    + + (11, 0, 2) + Counterclockwise + 93 + + + (11, 0, 10) + Counterclockwise + + + (6, 0, 0) + 982 + + + (6, 0, 12) + 977 + + + (10, 0, 6) + Clockwise + + + (8, 0, 11) + 92 + + + (2, 0, 6) + + + (8, 0, 6) + Clockwise + WULA_Alloy + Normal + + + +
  • (4,1,4,1)
  • +
  • (8,1,8,1)
  • +
    + Opposite +
    + + +
  • (1,4,1,4)
  • +
  • (1,8,1,8)
  • +
    + Counterclockwise +
    + + +
  • (11,4,11,4)
  • +
  • (11,8,11,8)
  • +
    + Clockwise +
    + + (4, 0, 11) + + + (8, 0, 11) + 17 + + + +
  • (1, 0, 1)
  • +
  • (1, 0, 3)
  • +
  • (1, 0, 9)
  • +
  • (1, 0, 11)
  • +
    + Clockwise + WULA_Alloy + Normal +
    + + (11, 0, 1) + Counterclockwise + WULA_Alloy + Normal + 149 + + + +
  • (11, 0, 3)
  • +
  • (11, 0, 9)
  • +
  • (11, 0, 11)
  • +
    + Counterclockwise + WULA_Alloy + Normal +
    + + +
  • (4,5,4,7)
  • +
    +
    + + +
  • (1, 0, 4)
  • +
  • (3, 0, 4)
  • +
  • (8, 0, 4)
  • +
  • (10, 0, 4)
  • +
  • (1, 0, 8)
  • +
  • (3, 0, 8)
  • +
  • (8, 0, 8)
  • +
  • (10, 0, 8)
  • +
    + Synthread +
    + + +
  • (0,0,5,0)
  • +
  • (7,0,12,0)
  • +
  • (0,1,0,12)
  • +
  • (12,2,12,2)
  • +
  • (12,4,12,12)
  • +
  • (1,12,5,12)
  • +
  • (8,12,11,12)
  • +
    +
    + + +
  • (12,1,12,1)
  • +
  • (12,3,12,3)
  • +
  • (7,12,7,12)
  • +
    + 1199 +
    + + +
  • (4, 0, 1)
  • +
  • (7, 0, 1)
  • +
    + WULA_Alloy +
    + + (5, 0, 11) + Opposite + WULA_Alloy + + + (6, 0, 6) + +
    + + + +
  • (0,1,12,12)
  • +
    +
    +
    +
    + + + + WULA_PowerPlantBase + (13,14) + + + +
  • (5, 0, 3)
  • +
  • (6, 0, 3)
  • +
  • (7, 0, 3)
  • +
    +
    + + +
  • (3, 0, 5)
  • +
  • (3, 0, 6)
  • +
  • (3, 0, 7)
  • +
    + Clockwise +
    + + +
  • (9, 0, 5)
  • +
  • (9, 0, 6)
  • +
  • (9, 0, 7)
  • +
    + Counterclockwise +
    + + +
  • (5, 0, 9)
  • +
  • (6, 0, 9)
  • +
  • (7, 0, 9)
  • +
    + Opposite +
    + + +
  • (6,0,6,0)
  • +
  • (6,12,6,12)
  • +
    +
    + + +
  • (0,6,0,6)
  • +
  • (12,6,12,6)
  • +
    + Clockwise +
    + + +
  • (3, 0, 3)
  • +
  • (9, 0, 3)
  • +
  • (3, 0, 9)
  • +
  • (9, 0, 9)
  • +
    +
    + + +
  • (3,1,3,1)
  • +
  • (9,1,9,1)
  • +
  • (1,3,1,3)
  • +
  • (11,3,11,3)
  • +
  • (3,11,3,11)
  • +
  • (9,11,9,11)
  • +
    +
    + + +
  • (4,1,4,1)
  • +
  • (8,1,8,1)
  • +
    + Opposite +
    + + +
  • (1,4,1,4)
  • +
  • (1,8,1,8)
  • +
    + Counterclockwise +
    + + +
  • (11,4,11,4)
  • +
  • (11,8,11,8)
  • +
    + Clockwise +
    + + +
  • (4,11,4,11)
  • +
  • (8,11,8,11)
  • +
    +
    + + +
  • (5,5,5,5)
  • +
  • (7,7,7,7)
  • +
    +
    + + (6, 0, 2) + Opposite + WULA_Alloy + + + (2, 0, 6) + Counterclockwise + WULA_Alloy + + + (10, 0, 6) + Clockwise + WULA_Alloy + + + (6, 0, 10) + WULA_Alloy + + + +
  • (0,0,5,0)
  • +
  • (7,0,12,0)
  • +
  • (0,1,0,5)
  • +
  • (12,1,12,5)
  • +
  • (0,7,0,12)
  • +
  • (12,7,12,12)
  • +
  • (1,12,5,12)
  • +
  • (7,12,11,12)
  • +
    +
    + + (6, 0, 6) + +
    + + + +
  • (0,1,12,12)
  • +
    +
    +
    +
    + + - WULA_HospitalBase - (13,14) + WULA_RavenShuttleBase + (13,13) - + + +
  • (1,6,1,6)
  • +
  • (11,6,11,6)
  • +
    + Clockwise +
    +
  • (1,2,1,2)
  • (1,10,1,10)
  • - Clockwise -
    - - (11, 0, 2) - Counterclockwise - 93 - - - (11, 0, 10) - Counterclockwise - - - (6, 0, 0) - 982 - - - (6, 0, 12) - 977 - - - (10, 0, 6) - Clockwise - - - (8, 0, 11) - 92 - - - (2, 0, 6) - - - (8, 0, 6) - Clockwise - WULA_Alloy - Normal - - - -
  • (4,1,4,1)
  • -
  • (8,1,8,1)
  • -
    - Opposite -
    - - -
  • (1,4,1,4)
  • -
  • (1,8,1,8)
  • -
    Counterclockwise
    -
  • (11,4,11,4)
  • -
  • (11,8,11,8)
  • +
  • (11,2,11,2)
  • +
  • (11,10,11,10)
  • Clockwise
    - - (4, 0, 11) - - - (8, 0, 11) - 17 - - - -
  • (1, 0, 1)
  • -
  • (1, 0, 3)
  • -
  • (1, 0, 9)
  • -
  • (1, 0, 11)
  • -
    - Clockwise - WULA_Alloy - Normal -
    - - (11, 0, 1) - Counterclockwise - WULA_Alloy - Normal - 149 - - - -
  • (11, 0, 3)
  • -
  • (11, 0, 9)
  • -
  • (11, 0, 11)
  • -
    - Counterclockwise - WULA_Alloy - Normal -
    - - -
  • (4,5,4,7)
  • -
    -
    - - -
  • (1, 0, 4)
  • -
  • (3, 0, 4)
  • -
  • (8, 0, 4)
  • -
  • (10, 0, 4)
  • -
  • (1, 0, 8)
  • -
  • (3, 0, 8)
  • -
  • (8, 0, 8)
  • -
  • (10, 0, 8)
  • -
    - Synthread -
    -
  • (0,0,5,0)
  • -
  • (7,0,12,0)
  • -
  • (0,1,0,12)
  • -
  • (12,2,12,2)
  • -
  • (12,4,12,12)
  • -
  • (1,12,5,12)
  • -
  • (8,12,11,12)
  • +
  • (0,0,1,0)
  • +
  • (11,0,12,0)
  • +
  • (0,1,0,5)
  • +
  • (12,1,12,5)
  • +
  • (1,5,1,5)
  • +
  • (11,5,11,5)
  • +
  • (0,7,1,7)
  • +
  • (11,7,12,7)
  • +
  • (0,8,0,12)
  • +
  • (12,8,12,12)
  • +
  • (1,12,1,12)
  • +
  • (11,12,11,12)
  • - - -
  • (12,1,12,1)
  • -
  • (12,3,12,3)
  • -
  • (7,12,7,12)
  • -
    - 1199 -
    - - -
  • (4, 0, 1)
  • -
  • (7, 0, 1)
  • -
    - WULA_Alloy -
    - - (5, 0, 11) - Opposite - WULA_Alloy - - + (6, 0, 6) - +
    diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 138b1676..535f6b59 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -35,6 +35,7 @@ namespace WulaFallenEmpire.EventSystem.AI private float _thinkingStartTime; private int _thinkingPhaseIndex = 1; private bool _thinkingPhaseRetry; + private float _lastThinkingDuration; private bool _lastActionExecuted; private bool _lastActionHadError; @@ -192,6 +193,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori public int ThinkingPhaseIndex => _thinkingPhaseIndex; public bool ThinkingPhaseRetry => _thinkingPhaseRetry; public int ThinkingPhaseTotal => _thinkingPhaseTotal; + public float LastThinkingDuration => _lastThinkingDuration; public void InitializeConversation(string eventDefName) { if (string.IsNullOrWhiteSpace(eventDefName)) @@ -439,6 +441,11 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori return; } + if (_isThinking && !isThinking) + { + _lastThinkingDuration = Mathf.Max(0f, Time.realtimeSinceStartup - _thinkingStartTime); + } + _isThinking = isThinking; OnThinkingStateChanged?.Invoke(_isThinking); } diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index 83c5f889..40d9b2d4 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using RimWorld; using UnityEngine; using Verse; +using WulaFallenEmpire; using WulaFallenEmpire.EventSystem.AI; using WulaFallenEmpire.EventSystem.AI.Utils; using System.Text.RegularExpressions; @@ -29,6 +30,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private Dictionary _portraits = new Dictionary(); private int _currentPortraitId = 0; private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase); + private bool _reactTraceExpanded = false; + private bool _hasReactTrace = false; + private float _reactTraceHeight = 0f; + private float _reactTraceYOffset = 0f; + private float _reactTraceHeaderHeight = 0f; + private string _reactTraceHeader = ""; + private List _reactTraceLines = new List(); private class CachedMessage { @@ -40,6 +48,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI public GameFont font; } + private class ReactTraceStep + { + public int Step; + public List Calls = new List(); + public List Results = new List(); + } + public static Dialog_AIConversation Instance { get; private set; } public Dialog_AIConversation(EventDef def) : base(def) @@ -353,6 +368,8 @@ namespace WulaFallenEmpire.EventSystem.AI.UI curY += height + 10f; } + + UpdateReactTraceCache(history, contentWidth, ref curY); _cachedTotalHeight = curY; } @@ -409,6 +426,15 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } } + if (_hasReactTrace) + { + Rect traceRect = new Rect(innerPadding, _reactTraceYOffset, contentWidth, _reactTraceHeight); + if (traceRect.yMax >= viewTop && traceRect.y <= viewBottom) + { + DrawReactTracePanel(traceRect); + } + } + if (_isThinking) { float thinkingY = _cachedTotalHeight > 0 ? _cachedTotalHeight : 0f; @@ -445,6 +471,186 @@ namespace WulaFallenEmpire.EventSystem.AI.UI return text.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim(); } + private void UpdateReactTraceCache(List<(string role, string message)> history, float contentWidth, ref float curY) + { + _hasReactTrace = false; + _reactTraceLines.Clear(); + _reactTraceHeader = ""; + _reactTraceHeight = 0f; + _reactTraceYOffset = 0f; + _reactTraceHeaderHeight = 0f; + + if (WulaFallenEmpireMod.settings?.showReactTraceInUI != true) + { + return; + } + + List steps = BuildReactTraceSteps(history); + if (steps.Count == 0) + { + return; + } + + _hasReactTrace = true; + _reactTraceHeader = BuildReactTraceHeader(); + foreach (var step in steps) + { + foreach (string call in step.Calls) + { + _reactTraceLines.Add($"Step {step.Step}: call {call}"); + } + foreach (string result in step.Results) + { + _reactTraceLines.Add($"Step {step.Step}: result {result}"); + } + } + + var originalFont = Text.Font; + Text.Font = GameFont.Tiny; + float panelPadding = 6f; + float headerWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); + string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; + _reactTraceHeaderHeight = Text.CalcHeight(headerLine, headerWidth) + 6f; + + float linesHeight = 0f; + if (_reactTraceExpanded && _reactTraceLines.Count > 0) + { + float lineWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); + foreach (string line in _reactTraceLines) + { + linesHeight += Text.CalcHeight(line, lineWidth) + 2f; + } + linesHeight += 4f; + } + _reactTraceHeight = _reactTraceHeaderHeight + linesHeight; + Text.Font = originalFont; + + curY += 6f; + _reactTraceYOffset = curY; + curY += _reactTraceHeight + 8f; + } + + private List BuildReactTraceSteps(List<(string role, string message)> history) + { + var steps = new List(); + if (history == null || history.Count == 0) return steps; + + int lastUserIndex = history.FindLastIndex(entry => entry.role == "user"); + if (lastUserIndex < 0) return steps; + + int stepIndex = 0; + for (int i = lastUserIndex + 1; i < history.Count; i++) + { + var entry = history[i]; + if (entry.role != "toolcall") continue; + + stepIndex++; + var step = new ReactTraceStep { Step = stepIndex }; + + if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _)) + { + foreach (var call in calls) + { + if (string.IsNullOrWhiteSpace(call?.Name)) continue; + string args = call.ArgumentsJson; + string callText = string.IsNullOrWhiteSpace(args) || args == "{}" + ? call.Name + : $"{call.Name} {TrimForDisplay(args, 160)}"; + step.Calls.Add(callText); + } + } + + if (i + 1 < history.Count && history[i + 1].role == "tool") + { + step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4)); + i++; + } + + if (step.Calls.Count > 0 || step.Results.Count > 0) + { + steps.Add(step); + } + } + + return steps; + } + + private static List ExtractToolResultLines(string toolMessage, int maxLines) + { + var lines = new List(); + if (string.IsNullOrWhiteSpace(toolMessage)) return lines; + + string[] rawLines = toolMessage.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + foreach (string raw in rawLines) + { + string line = raw.Trim(); + if (string.IsNullOrEmpty(line)) continue; + if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue; + lines.Add(TrimForDisplay(line, 200)); + if (lines.Count >= maxLines) break; + } + + return lines; + } + + private static string TrimForDisplay(string text, int maxChars) + { + if (string.IsNullOrEmpty(text) || text.Length <= maxChars) return text ?? ""; + return text.Substring(0, maxChars) + "..."; + } + + private string BuildReactTraceHeader() + { + string state = _isThinking ? "思考中" : "已思考"; + float elapsed = _isThinking + ? Mathf.Max(0f, Time.realtimeSinceStartup - (_core?.ThinkingStartTime ?? 0f)) + : _core?.LastThinkingDuration ?? 0f; + string elapsedText = elapsed > 0f ? elapsed.ToString("0.0", CultureInfo.InvariantCulture) : "0.0"; + return $"{state} ({elapsedText}s · Loop {_core?.ThinkingPhaseIndex ?? 0}/{_core?.ThinkingPhaseTotal ?? 0})"; + } + + private void DrawReactTracePanel(Rect rect) + { + var originalColor = GUI.color; + var originalFont = Text.Font; + var originalAnchor = Text.Anchor; + + float padding = 6f; + Rect headerRect = new Rect(rect.x, rect.y, rect.width, _reactTraceHeaderHeight); + GUI.color = new Color(0.15f, 0.15f, 0.15f, 0.8f); + Widgets.DrawBoxSolid(headerRect, GUI.color); + GUI.color = Color.white; + + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleLeft; + string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; + Widgets.Label(headerRect.ContractedBy(padding), headerLine); + + if (Widgets.ButtonInvisible(headerRect)) + { + _reactTraceExpanded = !_reactTraceExpanded; + _lastHistoryCount = -1; + _lastUsedWidth = -1f; + } + + if (_reactTraceExpanded && _reactTraceLines.Count > 0) + { + float y = headerRect.yMax + 2f; + foreach (string line in _reactTraceLines) + { + float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f; + Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight); + GUI.color = new Color(0.85f, 0.85f, 0.85f, 1f); + Widgets.Label(lineRect, line); + y += lineHeight; + } + } + + GUI.color = originalColor; + Text.Font = originalFont; + Text.Anchor = originalAnchor; + } + private string BuildThinkingStatus() { if (_core == null) return "Thinking..."; diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs index 42be57b2..44d98a32 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs @@ -4,6 +4,7 @@ using System.Linq; using UnityEngine; using Verse; using RimWorld; +using WulaFallenEmpire; using WulaFallenEmpire.EventSystem.AI; using WulaFallenEmpire.EventSystem.AI.Utils; @@ -24,6 +25,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private float _lastUsedWidth = -1f; private List _cachedMessages = new List(); private float _cachedTotalHeight = 0f; + private bool _reactTraceExpanded = false; + private bool _hasReactTrace = false; + private float _reactTraceHeight = 0f; + private float _reactTraceYOffset = 0f; + private float _reactTraceHeaderHeight = 0f; + private string _reactTraceHeader = ""; + private List _reactTraceLines = new List(); private class CachedMessage { @@ -34,6 +42,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI public float yOffset; } + private class ReactTraceStep + { + public int Step; + public List Calls = new List(); + public List Results = new List(); + } + // HUD / Minimized State private bool _isMinimized = false; @@ -410,6 +425,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI curY += h + reducedSpacing; } + UpdateReactTraceCache(history, width, ref curY); _cachedTotalHeight = curY; } @@ -464,6 +480,15 @@ namespace WulaFallenEmpire.EventSystem.AI.UI } } + if (_hasReactTrace) + { + Rect traceRect = new Rect(0, _reactTraceYOffset, width, _reactTraceHeight); + if (traceRect.yMax >= viewTop && traceRect.y <= viewBottom) + { + DrawReactTracePanel(traceRect); + } + } + if (_core != null && _core.IsThinking) { float thinkingY = _cachedTotalHeight > 0 ? _cachedTotalHeight : 10f; @@ -490,6 +515,189 @@ namespace WulaFallenEmpire.EventSystem.AI.UI return text.Remove(index, fragment.Length).Trim(); } + private void UpdateReactTraceCache(List<(string role, string message)> history, float contentWidth, ref float curY) + { + _hasReactTrace = false; + _reactTraceLines.Clear(); + _reactTraceHeader = ""; + _reactTraceHeight = 0f; + _reactTraceYOffset = 0f; + _reactTraceHeaderHeight = 0f; + + if (WulaFallenEmpireMod.settings?.showReactTraceInUI != true) + { + return; + } + + List steps = BuildReactTraceSteps(history); + if (steps.Count == 0) + { + return; + } + + _hasReactTrace = true; + _reactTraceHeader = BuildReactTraceHeader(); + foreach (var step in steps) + { + foreach (string call in step.Calls) + { + _reactTraceLines.Add($"Step {step.Step}: call {call}"); + } + foreach (string result in step.Results) + { + _reactTraceLines.Add($"Step {step.Step}: result {result}"); + } + } + + var originalFont = Text.Font; + Text.Font = GameFont.Tiny; + float panelPadding = 8f; + float headerWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); + string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; + _reactTraceHeaderHeight = Text.CalcHeight(headerLine, headerWidth) + 6f; + + float linesHeight = 0f; + if (_reactTraceExpanded && _reactTraceLines.Count > 0) + { + float lineWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f); + foreach (string line in _reactTraceLines) + { + linesHeight += Text.CalcHeight(line, lineWidth) + 2f; + } + linesHeight += 6f; + } + _reactTraceHeight = _reactTraceHeaderHeight + linesHeight; + Text.Font = originalFont; + + curY += 6f; + _reactTraceYOffset = curY; + curY += _reactTraceHeight + 6f; + } + + private List BuildReactTraceSteps(List<(string role, string message)> history) + { + var steps = new List(); + if (history == null || history.Count == 0) return steps; + + int lastUserIndex = history.FindLastIndex(entry => entry.role == "user"); + if (lastUserIndex < 0) return steps; + + int stepIndex = 0; + for (int i = lastUserIndex + 1; i < history.Count; i++) + { + var entry = history[i]; + if (entry.role != "toolcall") continue; + + stepIndex++; + var step = new ReactTraceStep { Step = stepIndex }; + + if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _)) + { + foreach (var call in calls) + { + if (string.IsNullOrWhiteSpace(call?.Name)) continue; + string args = call.ArgumentsJson; + string callText = string.IsNullOrWhiteSpace(args) || args == "{}" + ? call.Name + : $"{call.Name} {TrimForDisplay(args, 160)}"; + step.Calls.Add(callText); + } + } + + if (i + 1 < history.Count && history[i + 1].role == "tool") + { + step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4)); + i++; + } + + if (step.Calls.Count > 0 || step.Results.Count > 0) + { + steps.Add(step); + } + } + + return steps; + } + + private static List ExtractToolResultLines(string toolMessage, int maxLines) + { + var lines = new List(); + if (string.IsNullOrWhiteSpace(toolMessage)) return lines; + + string[] rawLines = toolMessage.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + foreach (string raw in rawLines) + { + string line = raw.Trim(); + if (string.IsNullOrEmpty(line)) continue; + if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue; + lines.Add(TrimForDisplay(line, 200)); + if (lines.Count >= maxLines) break; + } + + return lines; + } + + private static string TrimForDisplay(string text, int maxChars) + { + if (string.IsNullOrEmpty(text) || text.Length <= maxChars) return text ?? ""; + return text.Substring(0, maxChars) + "..."; + } + + private string BuildReactTraceHeader() + { + string state = _core != null && _core.IsThinking ? "思考中" : "已思考"; + float startTime = _core?.ThinkingStartTime ?? 0f; + float elapsed = _core != null && _core.IsThinking + ? Mathf.Max(0f, Time.realtimeSinceStartup - startTime) + : _core?.LastThinkingDuration ?? 0f; + string elapsedText = elapsed > 0f ? elapsed.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) : "0.0"; + int phaseIndex = _core?.ThinkingPhaseIndex ?? 0; + int phaseTotal = _core?.ThinkingPhaseTotal ?? 0; + return $"{state} ({elapsedText}s · Loop {phaseIndex}/{phaseTotal})"; + } + + private void DrawReactTracePanel(Rect rect) + { + var originalColor = GUI.color; + var originalFont = Text.Font; + var originalAnchor = Text.Anchor; + + float padding = 8f; + Rect headerRect = new Rect(rect.x, rect.y, rect.width, _reactTraceHeaderHeight); + GUI.color = new Color(0.12f, 0.12f, 0.12f, 0.8f); + Widgets.DrawBoxSolid(headerRect, GUI.color); + GUI.color = Color.white; + + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleLeft; + string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}"; + Widgets.Label(headerRect.ContractedBy(padding), headerLine); + + if (Widgets.ButtonInvisible(headerRect)) + { + _reactTraceExpanded = !_reactTraceExpanded; + _lastHistoryCount = -1; + _lastUsedWidth = -1f; + } + + if (_reactTraceExpanded && _reactTraceLines.Count > 0) + { + float y = headerRect.yMax + 2f; + foreach (string line in _reactTraceLines) + { + float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f; + Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight); + GUI.color = new Color(0.85f, 0.85f, 0.85f, 1f); + Widgets.Label(lineRect, line); + y += lineHeight; + } + } + + GUI.color = originalColor; + Text.Font = originalFont; + Text.Anchor = originalAnchor; + } + private void DrawFooter(Rect rect) { Widgets.DrawBoxSolid(rect, WulaLinkStyles.InputBarColor);