zc
This commit is contained in:
Binary file not shown.
@@ -827,7 +827,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(("assistant", "<no_action/>"));
|
_history.Add(("toolcall", "<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 +836,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(("assistant", "<no_action/>"));
|
_history.Add(("toolcall", "<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();
|
||||||
@@ -1325,43 +1325,45 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
|||||||
_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 innerPadding = 5f;
|
float maxBubbleWidth = containerWidth * MaxBubbleWidthRatio;
|
||||||
float contentWidth = rect.width - 16f - innerPadding * 2;
|
|
||||||
|
|
||||||
// 预计算高度 - 使用小字体
|
// 预计算高度
|
||||||
|
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.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
if (string.IsNullOrWhiteSpace(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
||||||
bool isLastMessage = i == filteredHistory.Count - 1;
|
|
||||||
|
|
||||||
// 设置更小的字体
|
float textWidth = maxBubbleWidth - (BubblePadding * 2);
|
||||||
if (isLastMessage && entry.role == "assistant")
|
if (entry.role == "assistant") textWidth -= (AvatarSize + BubblePadding);
|
||||||
{
|
|
||||||
Text.Font = GameFont.Small; // 原来是 Medium,改为 Small
|
float textHeight = Text.CalcHeight(text, textWidth);
|
||||||
}
|
float bubbleHeight = textHeight + (BubblePadding * 2);
|
||||||
else
|
float rowHeight = Mathf.Max(bubbleHeight, entry.role == "assistant" ? AvatarSize : 0f);
|
||||||
{
|
viewHeight += rowHeight + MessageSpacing;
|
||||||
Text.Font = GameFont.Tiny; // 原来是 Small,改为 Tiny
|
|
||||||
}
|
|
||||||
// 增加padding
|
|
||||||
float padding = (isLastMessage && entry.role == "assistant") ? 30f : 15f;
|
|
||||||
viewHeight += Text.CalcHeight(text, contentWidth) + padding + 10f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect viewRect = new Rect(0f, 0f, rect.width - 16f, viewHeight);
|
Rect viewRect = new Rect(0f, 0f, containerWidth, viewHeight);
|
||||||
if (_scrollToBottom)
|
if (_scrollToBottom)
|
||||||
{
|
{
|
||||||
_scrollPosition.y = float.MaxValue;
|
_scrollPosition.y = float.MaxValue;
|
||||||
@@ -1376,36 +1378,22 @@ 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.IsNullOrEmpty(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
if (string.IsNullOrWhiteSpace(text) || (entry.role == "user" && text.StartsWith("[Tool Results]"))) continue;
|
||||||
bool isLastMessage = i == filteredHistory.Count - 1;
|
|
||||||
|
|
||||||
// 设置更小的字体
|
Text.Font = GameFont.Small;
|
||||||
if (isLastMessage && entry.role == "assistant")
|
|
||||||
{
|
|
||||||
Text.Font = GameFont.Small; // 原来是 Medium,改为 Small
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Text.Font = GameFont.Tiny; // 原来是 Small,改为 Tiny
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
if (entry.role == "user")
|
||||||
{
|
{
|
||||||
Text.Anchor = TextAnchor.MiddleRight;
|
// 用户消息 - 右对齐,深蓝色气泡
|
||||||
Widgets.Label(labelRect, $"<color=#add8e6>{text}</color>");
|
DrawUserMessage(new Rect(0, curY, containerWidth, 0), text, out float userMsgHeight);
|
||||||
|
curY += userMsgHeight + MessageSpacing;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Text.Anchor = TextAnchor.MiddleLeft;
|
// AI消息 - 左对齐,带头像,深红色气泡
|
||||||
Widgets.Label(labelRect, $"P.I.A: {text}");
|
DrawAIMessage(new Rect(0, curY, containerWidth, 0), text, out float aiMsgHeight);
|
||||||
|
curY += aiMsgHeight + MessageSpacing;
|
||||||
}
|
}
|
||||||
curY += height + 10f;
|
|
||||||
}
|
}
|
||||||
Widgets.EndScrollView();
|
Widgets.EndScrollView();
|
||||||
}
|
}
|
||||||
@@ -1413,9 +1401,141 @@ 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 "";
|
||||||
|
|||||||
56
Tools/claude_handoff.md
Normal file
56
Tools/claude_handoff.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
WulaLink / AI Core Handoff (for Claude)
|
||||||
|
|
||||||
|
Context
|
||||||
|
- Mod: RimWorld WulaFallenEmpire.
|
||||||
|
- The AI conversation system was refactored to use a shared WorldComponent core.
|
||||||
|
- The small WulaLink overlay is now optional; the main event entry should open the old large dialog.
|
||||||
|
|
||||||
|
Current behavior and verification
|
||||||
|
- `Effect_OpenAIConversation` now opens the large window `Dialog_AIConversation`.
|
||||||
|
- The small overlay (`Overlay_WulaLink`) remains available via dev/debug entry.
|
||||||
|
- Non-final / streaming AI output should not create empty lines in the small window:
|
||||||
|
- SimpleAIClient is non-stream by default.
|
||||||
|
- AIIntelligenceCore only fires `OnMessageReceived` on final reply and ignores empty or XML-only output.
|
||||||
|
- Overlay filters `tool`/`toolcall` messages unless DevMode is on.
|
||||||
|
- Build verified: `dotnet build C:\Steam\steamapps\common\RimWorld\Mods\3516260226\Source\WulaFallenEmpire\WulaFallenEmpire.csproj`
|
||||||
|
|
||||||
|
Key changes
|
||||||
|
1) New shared AI core
|
||||||
|
- File: `Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs`
|
||||||
|
- WorldComponent with static `Instance` and events:
|
||||||
|
- `OnMessageReceived`, `OnThinkingStateChanged`, `OnExpressionChanged`
|
||||||
|
- Public API:
|
||||||
|
- `InitializeConversation`, `GetHistorySnapshot`, `SetExpression`, `SetPortrait`, `SendMessage`, `SendUserMessage`
|
||||||
|
- Core responsibilities:
|
||||||
|
- History load/save via `AIHistoryManager`
|
||||||
|
- `/clear` support
|
||||||
|
- Expression tag parsing `[EXPR:n]`
|
||||||
|
- 3-phase tool pipeline (query/action/reply) from the old dialog logic
|
||||||
|
- Tool execution and ledger tracking
|
||||||
|
|
||||||
|
2) OpenAI conversation entry now opens the large dialog
|
||||||
|
- File: `Source/WulaFallenEmpire/EventSystem/Effect/Effect_OpenAIConversation.cs`
|
||||||
|
- Uses `Dialog_AIConversation` instead of `Overlay_WulaLink`.
|
||||||
|
- XML entry in `1.6/1.6/Defs/EventDefs/Wula_AI_Events.xml` stays the same.
|
||||||
|
|
||||||
|
3) Overlay and tools point to the shared core
|
||||||
|
- Files:
|
||||||
|
- `Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs`
|
||||||
|
- `Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink_Notification.cs`
|
||||||
|
- `Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_ChangeExpression.cs`
|
||||||
|
- `Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs`
|
||||||
|
- Namespace import updated to `WulaFallenEmpire.EventSystem.AI`.
|
||||||
|
|
||||||
|
4) WulaLink styles restored for overlay build
|
||||||
|
- File: `Source/WulaFallenEmpire/EventSystem/AI/UI/WulaLinkStyles.cs`
|
||||||
|
- Added colors used by overlay:
|
||||||
|
- `InputBarColor`, `SystemAccentColor`, `SenseiTextColor`, `StudentTextColor`
|
||||||
|
|
||||||
|
Notes / gotchas
|
||||||
|
- Some UI files are not UTF-8 (likely ANSI). If you edit them with scripts, prefer `-Encoding Default` in PowerShell to avoid invalid UTF-8 errors.
|
||||||
|
- The old large dialog is still self-contained; the shared core is used by overlay + tools. Future cleanup can rewire `Dialog_AIConversation` to use the core if desired.
|
||||||
|
|
||||||
|
Open questions / TODO (if needed later)
|
||||||
|
- Memory system integration is not done in the core:
|
||||||
|
- `AIMemoryManager.cs`, `MemoryPrompts.cs`, and `Dialog_AIConversation.cs` integration still pending.
|
||||||
|
- If the overlay should become a non-debug entry, wire an explicit effect or UI button to open it.
|
||||||
Reference in New Issue
Block a user