zc
This commit is contained in:
114
.agent/workflows/refactor-ai-conversation.md
Normal file
114
.agent/workflows/refactor-ai-conversation.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# AI 对话系统重构任务
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
合并 `Dialog_AIConversation` 和 `AIIntelligenceCore`,简化 AI 对话系统架构。
|
||||||
|
|
||||||
|
## 当前架构问题
|
||||||
|
|
||||||
|
### 1. 两个类存在重复逻辑
|
||||||
|
- `Dialog_AIConversation.cs` - 大对话框 UI,包含自己的:
|
||||||
|
- `_history` 历史记录
|
||||||
|
- `RunPhasedRequestAsync()` 三阶段对话逻辑
|
||||||
|
- `ExecuteXmlToolsForPhase()` 工具执行
|
||||||
|
- `BuildSystemInstruction()` 系统指令构建
|
||||||
|
- `BuildUserMessageWithContext()` 上下文附加
|
||||||
|
|
||||||
|
- `AIIntelligenceCore.cs` - WorldComponent,包含:
|
||||||
|
- `_history` 历史记录 (重复!)
|
||||||
|
- `RunPhasedRequestAsync()` 三阶段对话逻辑 (重复!)
|
||||||
|
- `ExecuteXmlToolsForPhase()` 工具执行 (重复!)
|
||||||
|
- `SendUserMessage()` 用户消息发送
|
||||||
|
- 但 **没有** `BuildUserMessageWithContext()` 导致小窗口看不到选中对象上下文
|
||||||
|
|
||||||
|
### 2. 小窗口 `Overlay_WulaLink` 使用 `AIIntelligenceCore.SendUserMessage()`
|
||||||
|
- 这个方法没有附加选中对象的上下文
|
||||||
|
- 导致通过小窗口对话时,AI 看不到玩家选中了什么
|
||||||
|
|
||||||
|
### 3. 历史记录不同步
|
||||||
|
- `Dialog_AIConversation` 和 `AIIntelligenceCore` 各自维护历史记录
|
||||||
|
- 可能导致状态不一致
|
||||||
|
|
||||||
|
## 重构方案
|
||||||
|
|
||||||
|
### 方案 A: 让 AIIntelligenceCore 成为唯一的对话逻辑中心
|
||||||
|
|
||||||
|
1. **在 `AIIntelligenceCore.SendUserMessage()` 中添加上下文附加逻辑**:
|
||||||
|
```csharp
|
||||||
|
public void SendUserMessage(string text)
|
||||||
|
{
|
||||||
|
string messageWithContext = BuildUserMessageWithContext(text);
|
||||||
|
_history.Add(("user", messageWithContext));
|
||||||
|
PersistHistory();
|
||||||
|
_ = RunPhasedRequestAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildUserMessageWithContext(string userText)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.Append(userText);
|
||||||
|
|
||||||
|
if (Find.Selector != null)
|
||||||
|
{
|
||||||
|
if (Find.Selector.SingleSelectedThing != null)
|
||||||
|
{
|
||||||
|
var selected = Find.Selector.SingleSelectedThing;
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append($"[Context: Player has selected '{selected.LabelCap}'");
|
||||||
|
if (selected is Pawn pawn)
|
||||||
|
{
|
||||||
|
sb.Append($" ({pawn.def.label}) at ({pawn.Position.x}, {pawn.Position.z})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append($" at ({selected.Position.x}, {selected.Position.z})");
|
||||||
|
}
|
||||||
|
sb.Append("]");
|
||||||
|
}
|
||||||
|
else if (Find.Selector.SelectedObjects.Count > 1)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append($"[Context: Player has selected {Find.Selector.SelectedObjects.Count} objects: ");
|
||||||
|
var selectedThings = Find.Selector.SelectedObjects.OfType<Thing>().Take(5).ToList();
|
||||||
|
sb.Append(string.Join(", ", selectedThings.Select(t => t.LabelCap)));
|
||||||
|
if (Find.Selector.SelectedObjects.Count > 5) sb.Append("...");
|
||||||
|
sb.Append("]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **让 `Dialog_AIConversation` 使用 `AIIntelligenceCore` 而不是自己的逻辑**:
|
||||||
|
- 移除 `Dialog_AIConversation` 中的 `RunPhasedRequestAsync()`
|
||||||
|
- 移除 `Dialog_AIConversation` 中的 `ExecuteXmlToolsForPhase()`
|
||||||
|
- 让 `SelectOption()` 调用 `_core.SendUserMessage()` 而不是自己处理
|
||||||
|
- 订阅 `_core.OnMessageReceived` 事件来更新 UI
|
||||||
|
|
||||||
|
3. **统一历史记录**:
|
||||||
|
- 只使用 `AIIntelligenceCore._history`
|
||||||
|
- `Dialog_AIConversation` 通过 `_core.GetHistorySnapshot()` 获取历史
|
||||||
|
|
||||||
|
### 方案 B: 完全删除 AIIntelligenceCore,合并到 Dialog_AIConversation
|
||||||
|
|
||||||
|
不推荐,因为 `AIIntelligenceCore` 作为 WorldComponent 可以在游戏暂停/存档时保持状态。
|
||||||
|
|
||||||
|
## 文件位置
|
||||||
|
|
||||||
|
- `c:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\AIIntelligenceCore.cs`
|
||||||
|
- `c:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Dialog_AIConversation.cs`
|
||||||
|
- `c:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\EventSystem\AI\UI\Overlay_WulaLink.cs`
|
||||||
|
|
||||||
|
## 验证标准
|
||||||
|
|
||||||
|
1. 编译通过 (`dotnet build Source\WulaFallenEmpire\WulaFallenEmpire.csproj`)
|
||||||
|
2. 通过大窗口和小窗口对话时,AI 都能看到选中对象的上下文
|
||||||
|
3. 历史记录在两个窗口之间保持同步
|
||||||
|
4. 没有空行/空消息问题
|
||||||
|
5. 工具调用正常工作
|
||||||
|
|
||||||
|
## 最小修复 (如果不想大重构)
|
||||||
|
|
||||||
|
只在 `AIIntelligenceCore.SendUserMessage()` 中添加 `BuildUserMessageWithContext()` 逻辑即可解决小窗口看不到上下文的问题。
|
||||||
Binary file not shown.
@@ -186,11 +186,67 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_history.Add(("user", text));
|
// 附加选中对象的上下文信息
|
||||||
|
string messageWithContext = BuildUserMessageWithContext(text);
|
||||||
|
_history.Add(("user", messageWithContext));
|
||||||
PersistHistory();
|
PersistHistory();
|
||||||
_ = RunPhasedRequestAsync();
|
_ = RunPhasedRequestAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string BuildUserMessageWithContext(string userText)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.Append(userText);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Find.Selector != null)
|
||||||
|
{
|
||||||
|
if (Find.Selector.SingleSelectedThing != null)
|
||||||
|
{
|
||||||
|
var selected = Find.Selector.SingleSelectedThing;
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append($"[Context: Player has selected '{selected.LabelCap}'");
|
||||||
|
|
||||||
|
if (selected is Pawn pawn)
|
||||||
|
{
|
||||||
|
sb.Append($" ({pawn.def.label}) at ({pawn.Position.x}, {pawn.Position.z})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append($" at ({selected.Position.x}, {selected.Position.z})");
|
||||||
|
}
|
||||||
|
sb.Append("]");
|
||||||
|
}
|
||||||
|
else if (Find.Selector.SelectedObjects.Count > 1)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append($"[Context: Player has selected {Find.Selector.SelectedObjects.Count} objects");
|
||||||
|
|
||||||
|
var selectedThings = Find.Selector.SelectedObjects.OfType<Thing>().Take(5).ToList();
|
||||||
|
if (selectedThings.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append(": ");
|
||||||
|
sb.Append(string.Join(", ", selectedThings.Select(t => t.LabelCap)));
|
||||||
|
if (Find.Selector.SelectedObjects.Count > 5)
|
||||||
|
{
|
||||||
|
sb.Append("...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.Append("]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WulaLog.Debug($"[WulaAI] Error building context: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeTools()
|
private void InitializeTools()
|
||||||
{
|
{
|
||||||
_tools.Clear();
|
_tools.Clear();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
using RimWorld;
|
using RimWorld;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Verse;
|
using Verse;
|
||||||
|
using WulaFallenEmpire.EventSystem.AI;
|
||||||
using WulaFallenEmpire.EventSystem.AI.Tools;
|
using WulaFallenEmpire.EventSystem.AI.Tools;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
private Vector2 _scrollPosition = Vector2.zero;
|
private Vector2 _scrollPosition = Vector2.zero;
|
||||||
private bool _scrollToBottom = false;
|
private bool _scrollToBottom = false;
|
||||||
private List<AITool> _tools = new List<AITool>();
|
private List<AITool> _tools = new List<AITool>();
|
||||||
|
private AIIntelligenceCore _core;
|
||||||
private Dictionary<int, Texture2D> _portraits = new Dictionary<int, Texture2D>();
|
private Dictionary<int, Texture2D> _portraits = new Dictionary<int, Texture2D>();
|
||||||
private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase);
|
private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase);
|
||||||
private bool _lastActionExecuted = false;
|
private bool _lastActionExecuted = false;
|
||||||
@@ -111,12 +113,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
this.absorbInputAroundWindow = false;
|
this.absorbInputAroundWindow = false;
|
||||||
this.doCloseX = true;
|
this.doCloseX = true;
|
||||||
this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow;
|
this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow;
|
||||||
this.drawShadow = false; // 禁用阴影
|
this.drawShadow = Dialog_CustomDisplay.Config.showMainWindow;
|
||||||
this.closeOnClickedOutside = false;
|
this.closeOnClickedOutside = false;
|
||||||
this.draggable = true;
|
this.draggable = true;
|
||||||
this.resizeable = true;
|
this.resizeable = true;
|
||||||
|
|
||||||
// 关键修改:禁止Enter键自动关闭窗口
|
// 关键修改:ç¦<EFBFBD>æ¢Enter键自动关é—窗å<EFBFBD>?
|
||||||
this.closeOnAccept = false;
|
this.closeOnAccept = false;
|
||||||
|
|
||||||
_tools.Add(new Tool_SpawnResources());
|
_tools.Add(new Tool_SpawnResources());
|
||||||
@@ -138,11 +140,63 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
Instance = this;
|
Instance = this;
|
||||||
base.PostOpen();
|
base.PostOpen();
|
||||||
LoadPortraits();
|
LoadPortraits();
|
||||||
StartConversation();
|
|
||||||
|
_core = Find.World?.GetComponent<AIIntelligenceCore>();
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
_core.InitializeConversation(def.defName);
|
||||||
|
_core.OnMessageReceived += OnCoreMessageReceived;
|
||||||
|
_core.OnThinkingStateChanged += OnCoreThinkingStateChanged;
|
||||||
|
_core.OnExpressionChanged += OnCoreExpressionChanged;
|
||||||
|
|
||||||
|
_history = _core.GetHistorySnapshot();
|
||||||
|
_isThinking = _core.IsThinking;
|
||||||
|
SyncPortraitFromCore();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StartConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCoreMessageReceived(string message)
|
||||||
|
{
|
||||||
|
if (_core == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_history = _core.GetHistorySnapshot();
|
||||||
|
_scrollToBottom = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCoreThinkingStateChanged(bool isThinking)
|
||||||
|
{
|
||||||
|
_isThinking = isThinking;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCoreExpressionChanged(int id)
|
||||||
|
{
|
||||||
|
SetPortrait(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncPortraitFromCore()
|
||||||
|
{
|
||||||
|
if (_core == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetPortrait(_core.ExpressionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(string role, string message)> GetHistorySnapshot()
|
public List<(string role, string message)> GetHistorySnapshot()
|
||||||
{
|
{
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
return _core.GetHistorySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
return _history?.ToList() ?? new List<(string role, string message)>();
|
return _history?.ToList() ?? new List<(string role, string message)>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,7 +849,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
string cleaned = StripXmlTags(reply)?.Trim() ?? "";
|
string cleaned = StripXmlTags(reply)?.Trim() ?? "";
|
||||||
if (string.IsNullOrWhiteSpace(cleaned))
|
if (string.IsNullOrWhiteSpace(cleaned))
|
||||||
{
|
{
|
||||||
cleaned = "(系统)AI 返回了工具调用(XML),已被拦截。请重试或输入 /clear 清空上下文。";
|
cleaned = "AI returned a tool-only response (XML), which was blocked. Retry or use /clear to reset context.";
|
||||||
}
|
}
|
||||||
reply = cleaned;
|
reply = cleaned;
|
||||||
}
|
}
|
||||||
@@ -827,7 +881,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
if (matches.Count == 0)
|
if (matches.Count == 0)
|
||||||
{
|
{
|
||||||
UpdatePhaseToolLedger(phase, false, new List<string>());
|
UpdatePhaseToolLedger(phase, false, new List<string>());
|
||||||
_history.Add(("toolcall", "<no_action/>"));
|
_history.Add(("assistant", "<no_action/>"));
|
||||||
_history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}"));
|
_history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}"));
|
||||||
PersistHistory();
|
PersistHistory();
|
||||||
UpdateActionLedgerNote();
|
UpdateActionLedgerNote();
|
||||||
@@ -836,7 +890,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
if (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase))
|
if (matches.Count == 1 && matches[0].Groups[1].Value.Equals("no_action", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
UpdatePhaseToolLedger(phase, false, new List<string>());
|
UpdatePhaseToolLedger(phase, false, new List<string>());
|
||||||
_history.Add(("toolcall", "<no_action/>"));
|
_history.Add(("assistant", "<no_action/>"));
|
||||||
_history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}"));
|
_history.Add(("tool", $"[Tool Results]\nTool 'no_action' Result: No action taken.\n{guidance}"));
|
||||||
PersistHistory();
|
PersistHistory();
|
||||||
UpdateActionLedgerNote();
|
UpdateActionLedgerNote();
|
||||||
@@ -1178,6 +1232,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
{
|
{
|
||||||
if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop);
|
if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop);
|
||||||
|
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
_history = _core.GetHistorySnapshot();
|
||||||
|
_isThinking = _core.IsThinking;
|
||||||
|
}
|
||||||
|
|
||||||
// 定义边è·<C3A8>
|
// 定义边è·<C3A8>
|
||||||
float margin = 15f;
|
float margin = 15f;
|
||||||
Rect paddedRect = inRect.ContractedBy(margin);
|
Rect paddedRect = inRect.ContractedBy(margin);
|
||||||
@@ -1185,7 +1245,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
float curY = paddedRect.y;
|
float curY = paddedRect.y;
|
||||||
float width = paddedRect.width;
|
float width = paddedRect.width;
|
||||||
|
|
||||||
// 立绘不需要边距,所以使用原始inRect的位置
|
// 立绘ä¸<EFBFBD>需è¦<EFBFBD>è¾¹è·<EFBFBD>,所以使用原始inRectçš„ä½<EFBFBD>ç½?
|
||||||
if (portrait != null)
|
if (portrait != null)
|
||||||
{
|
{
|
||||||
Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true);
|
Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true);
|
||||||
@@ -1210,15 +1270,15 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
string name = def.characterName ?? "The Legion";
|
string name = def.characterName ?? "The Legion";
|
||||||
float nameHeight = Text.CalcHeight(name, width);
|
float nameHeight = Text.CalcHeight(name, width);
|
||||||
|
|
||||||
// 创建名字的矩形,使其在窗口水平居中
|
// 创建å<EFBFBD><EFBFBD>å—的矩形,使其在窗å<EFBFBD>£æ°´å¹³å±…ä¸?
|
||||||
Rect nameRect = new Rect(paddedRect.x, curY, width, nameHeight);
|
Rect nameRect = new Rect(paddedRect.x, curY, width, nameHeight);
|
||||||
Text.Anchor = TextAnchor.UpperCenter; // 改为上ä¸å¯¹é½<C3A9>
|
Text.Anchor = TextAnchor.UpperCenter; // 改为上ä¸å¯¹é½<C3A9>
|
||||||
Widgets.Label(nameRect, name);
|
Widgets.Label(nameRect, name);
|
||||||
Text.Anchor = TextAnchor.UpperLeft; // 恢复左对齐
|
Text.Anchor = TextAnchor.UpperLeft; // æ<EFBFBD>¢å¤<EFBFBD>左对é½?
|
||||||
|
|
||||||
curY += nameHeight + 10f;
|
curY += nameHeight + 10f;
|
||||||
|
|
||||||
// 计算输入框高度、选项高度和聊天历史高度
|
// 计算输入框高度ã€<EFBFBD>选项高度和è<EFBFBD>Šå¤©åކå<EFBFBD>²é«˜åº?
|
||||||
float inputHeight = 30f;
|
float inputHeight = 30f;
|
||||||
float optionsHeight = _options.Any() ? 100f : 0f;
|
float optionsHeight = _options.Any() ? 100f : 0f;
|
||||||
float spacing = 10f;
|
float spacing = 10f;
|
||||||
@@ -1247,13 +1307,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
|
|
||||||
curY += optionsHeight + spacing;
|
curY += optionsHeight + spacing;
|
||||||
|
|
||||||
// 输入框区域 - 使用带边距的矩形
|
// 输入框区åŸ?- 使用带边è·<C3A8>的矩形
|
||||||
Rect inputRect = new Rect(paddedRect.x, curY, width, inputHeight);
|
Rect inputRect = new Rect(paddedRect.x, curY, width, inputHeight);
|
||||||
|
|
||||||
// ä¿<C3A4>å˜å½“å‰<C3A5>å—体
|
// ä¿<C3A4>å˜å½“å‰<C3A5>å—体
|
||||||
var originalFont = Text.Font;
|
var originalFont = Text.Font;
|
||||||
|
|
||||||
// 设置更小的字体
|
// 设置更å°<EFBFBD>çš„å—ä½?
|
||||||
if (Text.Font == GameFont.Small)
|
if (Text.Font == GameFont.Small)
|
||||||
{
|
{
|
||||||
// 使用 Tiny å—体
|
// 使用 Tiny å—体
|
||||||
@@ -1261,18 +1321,18 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 如果当前不是 Small,降一级
|
// 如果当å‰<EFBFBD>ä¸<EFBFBD>是 Small,é™<C3A9>一çº?
|
||||||
Text.Font = GameFont.Small;
|
Text.Font = GameFont.Small;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算输入框文本高度
|
// 计算输入框文本高�
|
||||||
float textFieldHeight = Text.CalcHeight("Test", inputRect.width - 85);
|
float textFieldHeight = Text.CalcHeight("Test", inputRect.width - 85);
|
||||||
Rect textFieldRect = new Rect(inputRect.x, inputRect.y + (inputHeight - textFieldHeight) / 2, inputRect.width - 85, textFieldHeight);
|
Rect textFieldRect = new Rect(inputRect.x, inputRect.y + (inputHeight - textFieldHeight) / 2, inputRect.width - 85, textFieldHeight);
|
||||||
|
|
||||||
_inputText = Widgets.TextField(textFieldRect, _inputText);
|
_inputText = Widgets.TextField(textFieldRect, _inputText);
|
||||||
|
|
||||||
// 发送按钮 - 使用与Dialog_CustomDisplay相同的自定义按钮样式
|
// å<EFBFBD>‘é€<EFBFBD>按é’?- 使用与Dialog_CustomDisplay相å<C2B8>Œçš„è‡ªå®šä¹‰æŒ‰é’®æ ·å¼<C3A5>
|
||||||
// 保存当前状态
|
// ä¿<EFBFBD>å˜å½“å‰<EFBFBD>状æ€?
|
||||||
var originalAnchor = Text.Anchor;
|
var originalAnchor = Text.Anchor;
|
||||||
var originalColor = GUI.color;
|
var originalColor = GUI.color;
|
||||||
|
|
||||||
@@ -1286,7 +1346,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
// 使用基类的DrawCustomButton方法绘制按钮(与Dialog_CustomDisplay一致)
|
// 使用基类的DrawCustomButton方法绘制按钮(与Dialog_CustomDisplay一致)
|
||||||
base.DrawCustomButton(sendButtonRect, "Wula_AI_Send".Translate(), isEnabled: true);
|
base.DrawCustomButton(sendButtonRect, "Wula_AI_Send".Translate(), isEnabled: true);
|
||||||
|
|
||||||
// 恢复状态
|
// æ<EFBFBD>¢å¤<EFBFBD>状æ€?
|
||||||
GUI.color = originalColor;
|
GUI.color = originalColor;
|
||||||
Text.Anchor = originalAnchor;
|
Text.Anchor = originalAnchor;
|
||||||
Text.Font = originalFont;
|
Text.Font = originalFont;
|
||||||
@@ -1295,13 +1355,13 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
bool sendButtonPressed = Widgets.ButtonInvisible(sendButtonRect);
|
bool sendButtonPressed = Widgets.ButtonInvisible(sendButtonRect);
|
||||||
|
|
||||||
// 直接在DoWindowContentsä¸å¤„ç<E2809E>†Enter键,而ä¸<C3A4>是调用å<C2A8>•独的方法
|
// 直接在DoWindowContentsä¸å¤„ç<E2809E>†Enter键,而ä¸<C3A4>是调用å<C2A8>•独的方法
|
||||||
// 这是为了确保事件在正确的时机被处理
|
// 这是为了确ä¿<EFBFBD>事件在æ£ç¡®çš„æ—¶æœºè¢«å¤„ç<EFBFBD>?
|
||||||
if (Event.current.type == EventType.KeyDown)
|
if (Event.current.type == EventType.KeyDown)
|
||||||
{
|
{
|
||||||
// 检查是否按下了Enter键(主键盘或小键盘的Enter)
|
// 检查是å<EFBFBD>¦æŒ‰ä¸‹äº†Enter键(主键盘或å°<EFBFBD>键盘的Enterï¼?
|
||||||
if ((Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && !string.IsNullOrEmpty(_inputText))
|
if ((Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && !string.IsNullOrEmpty(_inputText))
|
||||||
{
|
{
|
||||||
// 如果AI正在思考,不处理Enter键
|
// 如果AIæ£åœ¨æ€<EFBFBD>考,ä¸<EFBFBD>处ç<EFBFBD>†Enteré”?
|
||||||
if (!_isThinking)
|
if (!_isThinking)
|
||||||
{
|
{
|
||||||
SelectOption(_inputText);
|
SelectOption(_inputText);
|
||||||
@@ -1318,52 +1378,50 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理鼠标点击发送按钮
|
// 处ç<EFBFBD>†é¼ æ ‡ç‚¹å‡»å<EFBFBD>‘é€<EFBFBD>按é’?
|
||||||
if (sendButtonPressed && !string.IsNullOrEmpty(_inputText))
|
if (sendButtonPressed && !string.IsNullOrEmpty(_inputText))
|
||||||
{
|
{
|
||||||
SelectOption(_inputText);
|
SelectOption(_inputText);
|
||||||
_inputText = "";
|
_inputText = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 气泡样式常量
|
|
||||||
private const float BubblePadding = 8f;
|
|
||||||
private const float AvatarSize = 32f;
|
|
||||||
private const float MessageSpacing = 8f;
|
|
||||||
private const float MaxBubbleWidthRatio = 0.75f;
|
|
||||||
private const float BubbleCornerRadius = 8f;
|
|
||||||
|
|
||||||
private void DrawChatHistory(Rect rect)
|
private void DrawChatHistory(Rect rect)
|
||||||
{
|
{
|
||||||
var originalFont = Text.Font;
|
var originalFont = Text.Font;
|
||||||
var originalAnchor = Text.Anchor;
|
var originalAnchor = Text.Anchor;
|
||||||
var originalColor = GUI.color;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
float viewHeight = 0f;
|
float viewHeight = 0f;
|
||||||
var filteredHistory = _history.Where(e => e.role != "tool" && e.role != "system" && e.role != "toolcall").ToList();
|
var filteredHistory = _history.Where(e => e.role != "tool" && e.role != "system" && e.role != "toolcall").ToList();
|
||||||
|
|
||||||
float containerWidth = rect.width - 16f;
|
// æ·»åŠ å†…è¾¹è·?
|
||||||
float maxBubbleWidth = containerWidth * MaxBubbleWidthRatio;
|
float innerPadding = 5f;
|
||||||
|
float contentWidth = rect.width - 16f - innerPadding * 2;
|
||||||
|
|
||||||
// 预计算高度
|
// 预计算高åº?- 使用å°<C3A5>å—ä½?
|
||||||
Text.Font = GameFont.Small;
|
|
||||||
for (int i = 0; i < filteredHistory.Count; i++)
|
for (int i = 0; i < filteredHistory.Count; i++)
|
||||||
{
|
{
|
||||||
var entry = filteredHistory[i];
|
var entry = filteredHistory[i];
|
||||||
string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
|
string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
|
||||||
if (string.IsNullOrWhiteSpace(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
if (string.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
||||||
|
bool isLastMessage = i == filteredHistory.Count - 1;
|
||||||
|
|
||||||
float textWidth = maxBubbleWidth - (BubblePadding * 2);
|
// 设置更å°<C3A5>çš„å—ä½?
|
||||||
if (entry.role == "assistant") textWidth -= (AvatarSize + BubblePadding);
|
if (isLastMessage && entry.role == "assistant")
|
||||||
|
{
|
||||||
float textHeight = Text.CalcHeight(text, textWidth);
|
Text.Font = GameFont.Small; // 原æ<C5B8>¥æ˜?Medium,改ä¸?Small
|
||||||
float bubbleHeight = textHeight + (BubblePadding * 2);
|
}
|
||||||
float rowHeight = Mathf.Max(bubbleHeight, entry.role == "assistant" ? AvatarSize : 0f);
|
else
|
||||||
viewHeight += rowHeight + MessageSpacing;
|
{
|
||||||
|
Text.Font = GameFont.Tiny; // 原æ<C5B8>¥æ˜?Small,改ä¸?Tiny
|
||||||
|
}
|
||||||
|
// å¢žåŠ padding
|
||||||
|
float padding = (isLastMessage && entry.role == "assistant") ? 30f : 15f;
|
||||||
|
viewHeight += Text.CalcHeight(text, contentWidth) + padding + 10f;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect viewRect = new Rect(0f, 0f, containerWidth, viewHeight);
|
Rect viewRect = new Rect(0f, 0f, rect.width - 16f, viewHeight);
|
||||||
if (_scrollToBottom)
|
if (_scrollToBottom)
|
||||||
{
|
{
|
||||||
_scrollPosition.y = float.MaxValue;
|
_scrollPosition.y = float.MaxValue;
|
||||||
@@ -1378,22 +1436,36 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
var entry = filteredHistory[i];
|
var entry = filteredHistory[i];
|
||||||
string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
|
string text = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : entry.message;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
if (string.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
||||||
|
bool isLastMessage = i == filteredHistory.Count - 1;
|
||||||
|
|
||||||
Text.Font = GameFont.Small;
|
// 设置更å°<C3A5>çš„å—ä½?
|
||||||
|
if (isLastMessage && entry.role == "assistant")
|
||||||
if (entry.role == "user")
|
|
||||||
{
|
{
|
||||||
// 用户消息 - 右对齐,深蓝色气泡
|
Text.Font = GameFont.Small; // 原æ<C5B8>¥æ˜?Medium,改ä¸?Small
|
||||||
DrawUserMessage(new Rect(0, curY, containerWidth, 0), text, out float userMsgHeight);
|
|
||||||
curY += userMsgHeight + MessageSpacing;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// AI消息 - 左对齐,带头像,深红色气泡
|
Text.Font = GameFont.Tiny; // 原æ<C5B8>¥æ˜?Small,改ä¸?Tiny
|
||||||
DrawAIMessage(new Rect(0, curY, containerWidth, 0), text, out float aiMsgHeight);
|
|
||||||
curY += aiMsgHeight + MessageSpacing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float padding = (isLastMessage && entry.role == "assistant") ? 30f : 15f;
|
||||||
|
float height = Text.CalcHeight(text, contentWidth) + padding;
|
||||||
|
|
||||||
|
// æ·»åŠ å†…è¾¹è·?
|
||||||
|
Rect labelRect = new Rect(innerPadding, curY, contentWidth, height);
|
||||||
|
|
||||||
|
if (entry.role == "user")
|
||||||
|
{
|
||||||
|
Text.Anchor = TextAnchor.MiddleRight;
|
||||||
|
Widgets.Label(labelRect, $"<color=#add8e6>{text}</color>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
|
Widgets.Label(labelRect, $"P.I.A: {text}");
|
||||||
|
}
|
||||||
|
curY += height + 10f;
|
||||||
}
|
}
|
||||||
Widgets.EndScrollView();
|
Widgets.EndScrollView();
|
||||||
}
|
}
|
||||||
@@ -1401,141 +1473,9 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
{
|
{
|
||||||
Text.Font = originalFont;
|
Text.Font = originalFont;
|
||||||
Text.Anchor = originalAnchor;
|
Text.Anchor = originalAnchor;
|
||||||
GUI.color = originalColor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawUserMessage(Rect containerRect, string text, out float totalHeight)
|
|
||||||
{
|
|
||||||
float maxBubbleWidth = containerRect.width * MaxBubbleWidthRatio;
|
|
||||||
float textWidth = maxBubbleWidth - (BubblePadding * 2);
|
|
||||||
|
|
||||||
Text.Font = GameFont.Small;
|
|
||||||
float textHeight = Text.CalcHeight(text, textWidth);
|
|
||||||
float bubbleHeight = textHeight + (BubblePadding * 2);
|
|
||||||
float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2), maxBubbleWidth);
|
|
||||||
|
|
||||||
// 右对齐
|
|
||||||
float bubbleX = containerRect.xMax - bubbleWidth;
|
|
||||||
Rect bubbleRect = new Rect(bubbleX, containerRect.y, bubbleWidth, bubbleHeight);
|
|
||||||
|
|
||||||
// 绘制圆角气泡背景 - 用户用深蓝色 (StudentBubble)
|
|
||||||
DrawRoundedRect(bubbleRect, WulaLinkStyles.StudentBubbleColor, BubbleCornerRadius);
|
|
||||||
|
|
||||||
// 绘制文字
|
|
||||||
GUI.color = WulaLinkStyles.StudentTextColor;
|
|
||||||
Text.Anchor = TextAnchor.MiddleLeft;
|
|
||||||
Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
|
|
||||||
totalHeight = bubbleHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawAIMessage(Rect containerRect, string text, out float totalHeight)
|
|
||||||
{
|
|
||||||
float maxBubbleWidth = containerRect.width * MaxBubbleWidthRatio;
|
|
||||||
float textWidth = maxBubbleWidth - (BubblePadding * 2) - AvatarSize - BubblePadding;
|
|
||||||
|
|
||||||
Text.Font = GameFont.Small;
|
|
||||||
float textHeight = Text.CalcHeight(text, textWidth);
|
|
||||||
float bubbleHeight = textHeight + (BubblePadding * 2);
|
|
||||||
float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2), maxBubbleWidth - AvatarSize - BubblePadding);
|
|
||||||
|
|
||||||
totalHeight = Mathf.Max(bubbleHeight, AvatarSize);
|
|
||||||
|
|
||||||
// 头像区域 - 左侧
|
|
||||||
Rect avatarRect = new Rect(containerRect.x, containerRect.y + (totalHeight - AvatarSize) / 2f, AvatarSize, AvatarSize);
|
|
||||||
|
|
||||||
// 绘制圆形头像
|
|
||||||
DrawCircularAvatar(avatarRect);
|
|
||||||
|
|
||||||
// 气泡区域 - 头像右侧
|
|
||||||
float bubbleX = avatarRect.xMax + BubblePadding;
|
|
||||||
Rect bubbleRect = new Rect(bubbleX, containerRect.y + (totalHeight - bubbleHeight) / 2f, bubbleWidth, bubbleHeight);
|
|
||||||
|
|
||||||
// 绘制圆角气泡背景 - AI用深红色 (SenseiBubble)
|
|
||||||
DrawRoundedRect(bubbleRect, WulaLinkStyles.SenseiBubbleColor, BubbleCornerRadius);
|
|
||||||
|
|
||||||
// 绘制文字
|
|
||||||
GUI.color = WulaLinkStyles.SenseiTextColor;
|
|
||||||
Text.Anchor = TextAnchor.MiddleLeft;
|
|
||||||
Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text);
|
|
||||||
GUI.color = Color.white;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawCircularAvatar(Rect rect)
|
|
||||||
{
|
|
||||||
// 获取当前头像
|
|
||||||
Texture2D avatarTex = portrait;
|
|
||||||
if (avatarTex == null && _portraits.Count > 0)
|
|
||||||
{
|
|
||||||
avatarTex = _portraits.Values.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制圆形背景
|
|
||||||
DrawRoundedRect(rect, new Color(0.3f, 0.15f, 0.15f, 1f), rect.width / 2f);
|
|
||||||
|
|
||||||
if (avatarTex != null)
|
|
||||||
{
|
|
||||||
// 使用圆形遮罩绘制头像
|
|
||||||
if (WulaLinkStyles.TexCircleMask != null)
|
|
||||||
{
|
|
||||||
// 先绘制头像
|
|
||||||
GUI.DrawTexture(rect, avatarTex, ScaleMode.ScaleToFit);
|
|
||||||
// 再用遮罩(如果可用)
|
|
||||||
GUI.color = new Color(1f, 1f, 1f, 1f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 无遮罩时直接绘制
|
|
||||||
GUI.DrawTexture(rect, avatarTex, ScaleMode.ScaleToFit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 无头像时显示占位符
|
|
||||||
GUI.color = WulaLinkStyles.SenseiTextColor;
|
|
||||||
Text.Font = GameFont.Tiny;
|
|
||||||
Text.Anchor = TextAnchor.MiddleCenter;
|
|
||||||
Widgets.Label(rect, "P.I.A");
|
|
||||||
}
|
|
||||||
GUI.color = Color.white;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRoundedRect(Rect rect, Color color, float radius)
|
|
||||||
{
|
|
||||||
var originalColor = GUI.color;
|
|
||||||
GUI.color = color;
|
|
||||||
|
|
||||||
// RimWorld没有内置圆角矩形,使用实心矩形模拟
|
|
||||||
// 主体矩形
|
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.x + radius, rect.y, rect.width - radius * 2, rect.height), color);
|
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.x, rect.y + radius, rect.width, rect.height - radius * 2), color);
|
|
||||||
|
|
||||||
// 四个角的圆(用小方块近似)
|
|
||||||
float step = radius / 3f;
|
|
||||||
for (float dx = 0; dx < radius; dx += step)
|
|
||||||
{
|
|
||||||
for (float dy = 0; dy < radius; dy += step)
|
|
||||||
{
|
|
||||||
float dist = Mathf.Sqrt(dx * dx + dy * dy);
|
|
||||||
if (dist <= radius)
|
|
||||||
{
|
|
||||||
// 左上角
|
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.x + radius - dx - step, rect.y + radius - dy - step, step, step), color);
|
|
||||||
// 右上角
|
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.xMax - radius + dx, rect.y + radius - dy - step, step, step), color);
|
|
||||||
// 左下角
|
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.x + radius - dx - step, rect.yMax - radius + dy, step, step), color);
|
|
||||||
// 右下角
|
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.xMax - radius + dx, rect.yMax - radius + dy, step, step), color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GUI.color = originalColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ParseResponseForDisplay(string rawResponse)
|
private string ParseResponseForDisplay(string rawResponse)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(rawResponse)) return "";
|
if (string.IsNullOrEmpty(rawResponse)) return "";
|
||||||
@@ -1630,7 +1570,27 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
|
|
||||||
private async void SelectOption(string text)
|
private async void SelectOption(string text)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(text) && string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
if (string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_isThinking = false;
|
||||||
|
_options.Clear();
|
||||||
|
_inputText = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollToBottom = true;
|
||||||
|
_core.SendUserMessage(text);
|
||||||
|
_history = _core.GetHistorySnapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(text.Trim(), "/clear", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_isThinking = false;
|
_isThinking = false;
|
||||||
_options.Clear();
|
_options.Clear();
|
||||||
@@ -1647,75 +1607,35 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
WulaLog.Debug($"[WulaAI] Failed to clear AI history: {ex}");
|
WulaLog.Debug($"[WulaAI] Failed to clear AI history: {ex}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Messages.Message("已清除 AI 对话上下文历史。", MessageTypeDefOf.NeutralEvent);
|
Messages.Message("AI conversation history cleared.", MessageTypeDefOf.NeutralEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建带选中对象上下文的用户消息
|
_history.Add(("user", text));
|
||||||
string messageWithContext = BuildUserMessageWithContext(text);
|
|
||||||
_history.Add(("user", messageWithContext));
|
|
||||||
PersistHistory();
|
PersistHistory();
|
||||||
_scrollToBottom = true;
|
_scrollToBottom = true;
|
||||||
await RunPhasedRequestAsync();
|
await RunPhasedRequestAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildUserMessageWithContext(string userText)
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.Append(userText);
|
|
||||||
|
|
||||||
// 获取当前选中的对象信息
|
|
||||||
if (Find.Selector != null)
|
|
||||||
{
|
|
||||||
if (Find.Selector.SingleSelectedThing != null)
|
|
||||||
{
|
|
||||||
var selected = Find.Selector.SingleSelectedThing;
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.Append($"[Context: Player has selected '{selected.LabelCap}'");
|
|
||||||
|
|
||||||
// 如果是Pawn,提供更多信息
|
|
||||||
if (selected is Pawn pawn)
|
|
||||||
{
|
|
||||||
sb.Append($" ({pawn.def.label})");
|
|
||||||
sb.Append($" at ({pawn.Position.x}, {pawn.Position.z})");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append($" at ({selected.Position.x}, {selected.Position.z})");
|
|
||||||
}
|
|
||||||
sb.Append("]");
|
|
||||||
}
|
|
||||||
else if (Find.Selector.SelectedObjects.Count > 1)
|
|
||||||
{
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.Append($"[Context: Player has selected {Find.Selector.SelectedObjects.Count} objects");
|
|
||||||
|
|
||||||
// 列出前几个选中的对象
|
|
||||||
var selectedThings = Find.Selector.SelectedObjects.OfType<Thing>().Take(5).ToList();
|
|
||||||
if (selectedThings.Count > 0)
|
|
||||||
{
|
|
||||||
sb.Append(": ");
|
|
||||||
sb.Append(string.Join(", ", selectedThings.Select(t => t.LabelCap)));
|
|
||||||
if (Find.Selector.SelectedObjects.Count > 5)
|
|
||||||
{
|
|
||||||
sb.Append("...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.Append("]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void PostClose()
|
public override void PostClose()
|
||||||
{
|
{
|
||||||
|
if (_core != null)
|
||||||
|
{
|
||||||
|
_core.OnMessageReceived -= OnCoreMessageReceived;
|
||||||
|
_core.OnThinkingStateChanged -= OnCoreThinkingStateChanged;
|
||||||
|
_core.OnExpressionChanged -= OnCoreExpressionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
if (Instance == this) Instance = null;
|
if (Instance == this) Instance = null;
|
||||||
PersistHistory();
|
if (_core == null)
|
||||||
|
{
|
||||||
|
PersistHistory();
|
||||||
|
}
|
||||||
base.PostClose();
|
base.PostClose();
|
||||||
HandleAction(def.dismissEffects);
|
HandleAction(def.dismissEffects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -45,9 +45,9 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
this.forcePause = false;
|
this.forcePause = false;
|
||||||
this.absorbInputAroundWindow = false;
|
this.absorbInputAroundWindow = false;
|
||||||
this.closeOnClickedOutside = false;
|
this.closeOnClickedOutside = false;
|
||||||
this.closeOnAccept = false; // 防止 Enter 键误关闭
|
this.closeOnAccept = false; // 防止 Enter 键误关闭
|
||||||
this.doWindowBackground = false; // We draw our own
|
this.doWindowBackground = false; // We draw our own
|
||||||
this.drawShadow = false; // 禁用阴影
|
this.drawShadow = false; // 禁用阴影
|
||||||
this.draggable = true;
|
this.draggable = true;
|
||||||
this.resizeable = true;
|
this.resizeable = true;
|
||||||
this.preventCameraMotion = false;
|
this.preventCameraMotion = false;
|
||||||
@@ -65,16 +65,16 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
|
|
||||||
if (_isMinimized)
|
if (_isMinimized)
|
||||||
{
|
{
|
||||||
// 最小化时保持当前位置,只调整大小
|
// 最小化时保持当前位置,只调整大小
|
||||||
windowRect.width = _minimizedSize.x;
|
windowRect.width = _minimizedSize.x;
|
||||||
windowRect.height = _minimizedSize.y;
|
windowRect.height = _minimizedSize.y;
|
||||||
// 确保不超出屏幕边界
|
// 确保不超出屏幕边界
|
||||||
windowRect.x = Mathf.Clamp(windowRect.x, 0, Verse.UI.screenWidth - _minimizedSize.x);
|
windowRect.x = Mathf.Clamp(windowRect.x, 0, Verse.UI.screenWidth - _minimizedSize.x);
|
||||||
windowRect.y = Mathf.Clamp(windowRect.y, 0, Verse.UI.screenHeight - _minimizedSize.y);
|
windowRect.y = Mathf.Clamp(windowRect.y, 0, Verse.UI.screenHeight - _minimizedSize.y);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 展开时居中到屏幕中心
|
// 展开时居中到屏幕中心
|
||||||
windowRect.width = _expandedSize.x;
|
windowRect.width = _expandedSize.x;
|
||||||
windowRect.height = _expandedSize.y;
|
windowRect.height = _expandedSize.y;
|
||||||
windowRect.x = (Verse.UI.screenWidth - _expandedSize.x) / 2f;
|
windowRect.x = (Verse.UI.screenWidth - _expandedSize.x) / 2f;
|
||||||
@@ -183,7 +183,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
Color statusColor = _core.IsThinking ? Color.yellow : Color.green;
|
Color statusColor = _core.IsThinking ? Color.yellow : Color.green;
|
||||||
|
|
||||||
GUI.color = statusColor;
|
GUI.color = statusColor;
|
||||||
Widgets.Label(statusRect, $"鈼?{status}");
|
Widgets.Label(statusRect, $"鈼?{status}");
|
||||||
GUI.color = Color.white;
|
GUI.color = Color.white;
|
||||||
|
|
||||||
// Unread Badge
|
// Unread Badge
|
||||||
@@ -245,17 +245,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
titleRect.x += 10f;
|
titleRect.x += 10f;
|
||||||
Widgets.Label(titleRect, _def.characterName ?? "MomoTalk");
|
Widgets.Label(titleRect, _def.characterName ?? "MomoTalk");
|
||||||
|
|
||||||
// Header Icons (Minimize/Close) - 自定义样式
|
// Header Icons (Minimize/Close) - 自定义样式
|
||||||
Rect closeRect = new Rect(rect.width - 35f, 10f, 25f, 25f);
|
Rect closeRect = new Rect(rect.width - 35f, 10f, 25f, 25f);
|
||||||
Rect minRect = new Rect(rect.width - 65f, 10f, 25f, 25f);
|
Rect minRect = new Rect(rect.width - 65f, 10f, 25f, 25f);
|
||||||
|
|
||||||
// 最小化按钮
|
// 最小化按钮
|
||||||
if (DrawHeaderButton(minRect, "-"))
|
if (DrawHeaderButton(minRect, "-"))
|
||||||
{
|
{
|
||||||
ToggleMinimize();
|
ToggleMinimize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭按钮
|
// 关闭按钮
|
||||||
if (DrawHeaderButton(closeRect, "X"))
|
if (DrawHeaderButton(closeRect, "X"))
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
@@ -269,8 +269,8 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
{
|
{
|
||||||
bool isMouseOver = Mouse.IsOver(rect);
|
bool isMouseOver = Mouse.IsOver(rect);
|
||||||
Color buttonColor = isMouseOver
|
Color buttonColor = isMouseOver
|
||||||
? new Color(0.6f, 0.3f, 0.3f, 1f) // Hover: 深红色
|
? new Color(0.6f, 0.3f, 0.3f, 1f) // Hover: 深红色
|
||||||
: new Color(0.4f, 0.2f, 0.2f, 0.8f); // Normal: 暗红色
|
: new Color(0.4f, 0.2f, 0.2f, 0.8f); // Normal: 暗红色
|
||||||
Color textColor = isMouseOver ? Color.white : new Color(0.9f, 0.9f, 0.9f);
|
Color textColor = isMouseOver ? Color.white : new Color(0.9f, 0.9f, 0.9f);
|
||||||
|
|
||||||
var originalColor = GUI.color;
|
var originalColor = GUI.color;
|
||||||
@@ -301,8 +301,9 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
var displayHistory = new List<(string role, string message, string displayText)>();
|
var displayHistory = new List<(string role, string message, string displayText)>();
|
||||||
foreach (var msg in history)
|
foreach (var msg in history)
|
||||||
{
|
{
|
||||||
// Skip tool/toolcall/system messages in normal mode
|
// Skip tool/toolcall messages to avoid empty spacing
|
||||||
if ((msg.role == "tool" || msg.role == "toolcall" || msg.role == "system") && !Prefs.DevMode) continue;
|
if (msg.role == "tool" || msg.role == "toolcall") continue;
|
||||||
|
if (msg.role == "system" && !Prefs.DevMode) continue;
|
||||||
|
|
||||||
// For assistant messages, strip XML and check if empty
|
// For assistant messages, strip XML and check if empty
|
||||||
string displayText = msg.message;
|
string displayText = msg.message;
|
||||||
@@ -456,18 +457,18 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
float bubbleHeight = textHeight + (BubblePadding * 2);
|
float bubbleHeight = textHeight + (BubblePadding * 2);
|
||||||
float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2) + 10f, maxBubbleWidth);
|
float bubbleWidth = Mathf.Min(Text.CalcSize(text).x + (BubblePadding * 2) + 10f, maxBubbleWidth);
|
||||||
|
|
||||||
// 气泡位置 - 右对齐,留出箭头空间
|
// 气泡位置 - 右对齐,留出箭头空间
|
||||||
float arrowSize = 8f;
|
float arrowSize = 8f;
|
||||||
Rect bubbleRect = new Rect(rect.xMax - bubbleWidth - arrowSize - 5f, rect.y, bubbleWidth, bubbleHeight);
|
Rect bubbleRect = new Rect(rect.xMax - bubbleWidth - arrowSize - 5f, rect.y, bubbleWidth, bubbleHeight);
|
||||||
|
|
||||||
// 绘制圆角气泡背景 - 蓝色 (Sensei color)
|
// 绘制圆角气泡背景 - 蓝色 (Sensei color)
|
||||||
Color bubbleColor = new Color(0.29f, 0.54f, 0.78f, 1f); // #4a8ac6 MomoTalk Sensei blue
|
Color bubbleColor = new Color(0.29f, 0.54f, 0.78f, 1f); // #4a8ac6 MomoTalk Sensei blue
|
||||||
DrawRoundedBubble(bubbleRect, bubbleColor, 8f);
|
DrawRoundedBubble(bubbleRect, bubbleColor, 8f);
|
||||||
|
|
||||||
// 绘制右侧箭头
|
// 绘制右侧箭头
|
||||||
DrawBubbleArrow(bubbleRect.xMax, bubbleRect.y + 10f, arrowSize, bubbleColor, false);
|
DrawBubbleArrow(bubbleRect.xMax, bubbleRect.y + 10f, arrowSize, bubbleColor, false);
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
GUI.color = Color.white;
|
GUI.color = Color.white;
|
||||||
Text.Anchor = TextAnchor.MiddleLeft;
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text);
|
Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text);
|
||||||
@@ -480,11 +481,11 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
float avatarX = 10f;
|
float avatarX = 10f;
|
||||||
Rect avatarRect = new Rect(avatarX, rect.y, AvatarSize, AvatarSize);
|
Rect avatarRect = new Rect(avatarX, rect.y, AvatarSize, AvatarSize);
|
||||||
|
|
||||||
// 绘制圆形头像背景
|
// 绘制圆形头像背景
|
||||||
Color avatarBgColor = new Color(0.2f, 0.2f, 0.25f, 1f);
|
Color avatarBgColor = new Color(0.2f, 0.2f, 0.25f, 1f);
|
||||||
DrawRoundedBubble(avatarRect, avatarBgColor, AvatarSize / 2f);
|
DrawRoundedBubble(avatarRect, avatarBgColor, AvatarSize / 2f);
|
||||||
|
|
||||||
// 绘制头像
|
// 绘制头像
|
||||||
int expId = _core?.ExpressionId ?? 1;
|
int expId = _core?.ExpressionId ?? 1;
|
||||||
string portraitPath = _def.portraitPath ?? $"Wula/Events/Portraits/WULA_Legion_{expId}";
|
string portraitPath = _def.portraitPath ?? $"Wula/Events/Portraits/WULA_Legion_{expId}";
|
||||||
if (expId > 1 && _def.portraitPath == null)
|
if (expId > 1 && _def.portraitPath == null)
|
||||||
@@ -499,7 +500,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 显示占位符
|
// 显示占位符
|
||||||
GUI.color = Color.white;
|
GUI.color = Color.white;
|
||||||
Text.Font = GameFont.Tiny;
|
Text.Font = GameFont.Tiny;
|
||||||
Text.Anchor = TextAnchor.MiddleCenter;
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
@@ -507,7 +508,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
}
|
}
|
||||||
GUI.color = Color.white;
|
GUI.color = Color.white;
|
||||||
|
|
||||||
// 气泡
|
// 气泡
|
||||||
float maxBubbleWidth = rect.width * MaxBubbleWidthRatio;
|
float maxBubbleWidth = rect.width * MaxBubbleWidthRatio;
|
||||||
float arrowSize = 8f;
|
float arrowSize = 8f;
|
||||||
float bubbleX = avatarRect.xMax + arrowSize + 5f;
|
float bubbleX = avatarRect.xMax + arrowSize + 5f;
|
||||||
@@ -520,14 +521,14 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
|
|
||||||
Rect bubbleRect = new Rect(bubbleX, rect.y, bubbleWidth, bubbleHeight);
|
Rect bubbleRect = new Rect(bubbleX, rect.y, bubbleWidth, bubbleHeight);
|
||||||
|
|
||||||
// 绘制圆角气泡背景 - 灰色 (Student color)
|
// 绘制圆角气泡背景 - 灰色 (Student color)
|
||||||
Color bubbleColor = new Color(0.85f, 0.85f, 0.87f, 1f); // Light gray like MomoTalk
|
Color bubbleColor = new Color(0.85f, 0.85f, 0.87f, 1f); // Light gray like MomoTalk
|
||||||
DrawRoundedBubble(bubbleRect, bubbleColor, 8f);
|
DrawRoundedBubble(bubbleRect, bubbleColor, 8f);
|
||||||
|
|
||||||
// 绘制左侧箭头
|
// 绘制左侧箭头
|
||||||
DrawBubbleArrow(bubbleRect.x, bubbleRect.y + 10f, arrowSize, bubbleColor, true);
|
DrawBubbleArrow(bubbleRect.x, bubbleRect.y + 10f, arrowSize, bubbleColor, true);
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
GUI.color = new Color(0.1f, 0.1f, 0.1f, 1f); // Dark text
|
GUI.color = new Color(0.1f, 0.1f, 0.1f, 1f); // Dark text
|
||||||
Text.Anchor = TextAnchor.MiddleLeft;
|
Text.Anchor = TextAnchor.MiddleLeft;
|
||||||
Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text);
|
Widgets.Label(bubbleRect.ContractedBy(BubblePadding), text);
|
||||||
@@ -609,17 +610,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
return Widgets.ButtonInvisible(rect);
|
return Widgets.ButtonInvisible(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MomoTalk 风格的圆角气泡
|
// MomoTalk 风格的圆角气泡
|
||||||
private void DrawRoundedBubble(Rect rect, Color color, float radius)
|
private void DrawRoundedBubble(Rect rect, Color color, float radius)
|
||||||
{
|
{
|
||||||
var originalColor = GUI.color;
|
var originalColor = GUI.color;
|
||||||
GUI.color = color;
|
GUI.color = color;
|
||||||
|
|
||||||
// 主体矩形
|
// 主体矩形
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.x + radius, rect.y, rect.width - radius * 2, rect.height), color);
|
Widgets.DrawBoxSolid(new Rect(rect.x + radius, rect.y, rect.width - radius * 2, rect.height), color);
|
||||||
Widgets.DrawBoxSolid(new Rect(rect.x, rect.y + radius, rect.width, rect.height - radius * 2), color);
|
Widgets.DrawBoxSolid(new Rect(rect.x, rect.y + radius, rect.width, rect.height - radius * 2), color);
|
||||||
|
|
||||||
// 四个角的近似圆角
|
// 四个角的近似圆角
|
||||||
float step = radius / 4f;
|
float step = radius / 4f;
|
||||||
for (float dx = 0; dx < radius; dx += step)
|
for (float dx = 0; dx < radius; dx += step)
|
||||||
{
|
{
|
||||||
@@ -639,13 +640,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
GUI.color = originalColor;
|
GUI.color = originalColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MomoTalk 风格的气泡箭头
|
// MomoTalk 风格的气泡箭头
|
||||||
private void DrawBubbleArrow(float x, float y, float size, Color color, bool pointLeft)
|
private void DrawBubbleArrow(float x, float y, float size, Color color, bool pointLeft)
|
||||||
{
|
{
|
||||||
var originalColor = GUI.color;
|
var originalColor = GUI.color;
|
||||||
GUI.color = color;
|
GUI.color = color;
|
||||||
|
|
||||||
// 用小方块模拟三角形箭头
|
// 用小方块模拟三角形箭头
|
||||||
float step = size / 4f;
|
float step = size / 4f;
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
@@ -660,3 +661,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user