zc
This commit is contained in:
Binary file not shown.
@@ -155,5 +155,6 @@
|
||||
<Wula_AI_Send>发送</Wula_AI_Send>
|
||||
<Wula_AI_Error_Internal>错误:内部系统故障。{0}</Wula_AI_Error_Internal>
|
||||
<Wula_AI_Error_ConnectionLost>错误:连接丢失。“军团”保持沉默。</Wula_AI_Error_ConnectionLost>
|
||||
<Wula_ResourceDrop>{FACTION_name} 已经在附近投下了一些资源。</Wula_ResourceDrop>
|
||||
|
||||
</LanguageData>
|
||||
@@ -10,7 +10,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
public class AIHistoryManager : WorldComponent
|
||||
{
|
||||
private string _saveId;
|
||||
private Dictionary<string, List<(string role, string message)>> _cache = new Dictionary<string, List<(string role, string message)>>();
|
||||
private Dictionary<string, List<ApiMessage>> _cache = new Dictionary<string, List<ApiMessage>>();
|
||||
|
||||
public AIHistoryManager(World world) : base(world)
|
||||
{
|
||||
@@ -35,7 +35,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
return Path.Combine(GetSaveDirectory(), $"{_saveId}_{eventDefName}.json");
|
||||
}
|
||||
|
||||
public List<(string role, string message)> GetHistory(string eventDefName)
|
||||
public List<ApiMessage> GetHistory(string eventDefName)
|
||||
{
|
||||
if (_cache.TryGetValue(eventDefName, out var cachedHistory))
|
||||
{
|
||||
@@ -61,10 +61,10 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
}
|
||||
}
|
||||
|
||||
return new List<(string role, string message)>();
|
||||
return new List<ApiMessage>();
|
||||
}
|
||||
|
||||
public void SaveHistory(string eventDefName, List<(string role, string message)> history)
|
||||
public void SaveHistory(string eventDefName, List<ApiMessage> history)
|
||||
{
|
||||
_cache[eventDefName] = history;
|
||||
string path = GetFilePath(eventDefName);
|
||||
@@ -93,7 +93,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
|
||||
public static class SimpleJsonParser
|
||||
{
|
||||
public static string Serialize(List<(string role, string message)> history)
|
||||
public static string Serialize(List<ApiMessage> history)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append("[");
|
||||
@@ -102,7 +102,8 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
var item = history[i];
|
||||
sb.Append("{");
|
||||
sb.Append($"\"role\":\"{Escape(item.role)}\",");
|
||||
sb.Append($"\"message\":\"{Escape(item.message)}\"");
|
||||
sb.Append($"\"content\":\"{Escape(item.content)}\"");
|
||||
// Note: tool_calls are not serialized for history to keep it simple.
|
||||
sb.Append("}");
|
||||
if (i < history.Count - 1) sb.Append(",");
|
||||
}
|
||||
@@ -110,13 +111,11 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static List<(string role, string message)> Deserialize(string json)
|
||||
public static List<ApiMessage> Deserialize(string json)
|
||||
{
|
||||
var result = new List<(string role, string message)>();
|
||||
var result = new List<ApiMessage>();
|
||||
if (string.IsNullOrEmpty(json)) return result;
|
||||
|
||||
// Very basic parser, assumes standard format produced by Serialize
|
||||
// Remove outer brackets
|
||||
json = json.Trim();
|
||||
if (json.StartsWith("[") && json.EndsWith("]"))
|
||||
{
|
||||
@@ -125,10 +124,6 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
|
||||
if (string.IsNullOrEmpty(json)) return result;
|
||||
|
||||
// Split by objects
|
||||
// This is fragile if objects contain nested objects or escaped braces, but for this specific structure it's fine
|
||||
// We are splitting by "},{" which is risky. Better to iterate.
|
||||
|
||||
int depth = 0;
|
||||
int start = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
@@ -153,16 +148,10 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (string role, string message) ParseObject(string json)
|
||||
public static Dictionary<string, string> Parse(string json)
|
||||
{
|
||||
string role = null;
|
||||
string message = null;
|
||||
|
||||
// Remove braces
|
||||
var dict = new Dictionary<string, string>();
|
||||
json = json.Trim('{', '}');
|
||||
|
||||
// Split by key-value pairs, respecting quotes
|
||||
// Again, simple parsing
|
||||
var parts = SplitByComma(json);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
@@ -171,17 +160,48 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
{
|
||||
string key = Unescape(kv[0].Trim().Trim('"'));
|
||||
string val = Unescape(kv[1].Trim().Trim('"'));
|
||||
if (key == "role") role = val;
|
||||
if (key == "message") message = val;
|
||||
dict[key] = val;
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
return (role, message);
|
||||
public static List<Dictionary<string, string>> ParseArray(string json)
|
||||
{
|
||||
var list = new List<Dictionary<string, string>>();
|
||||
json = json.Trim('[', ']');
|
||||
int depth = 0;
|
||||
int start = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '{')
|
||||
{
|
||||
if (depth == 0) start = i;
|
||||
depth++;
|
||||
}
|
||||
else if (json[i] == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
list.Add(Parse(json.Substring(start, i - start + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static ApiMessage ParseObject(string json)
|
||||
{
|
||||
var msg = new ApiMessage();
|
||||
var dict = Parse(json);
|
||||
if (dict.TryGetValue("role", out string r)) msg.role = r;
|
||||
if (dict.TryGetValue("content", out string c)) msg.content = c;
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static string[] SplitByComma(string input)
|
||||
{
|
||||
// Split by comma but ignore commas inside quotes
|
||||
var list = new List<string>();
|
||||
bool inQuote = false;
|
||||
int start = 0;
|
||||
@@ -200,7 +220,6 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
|
||||
private static string[] SplitByColon(string input)
|
||||
{
|
||||
// Split by first colon outside quotes
|
||||
bool inQuote = false;
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
@@ -219,7 +238,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
|
||||
}
|
||||
|
||||
private static string Unescape(string s)
|
||||
public static string Unescape(string s) // Changed to public
|
||||
{
|
||||
if (s == null) return "";
|
||||
return s.Replace("\\r", "\r").Replace("\\n", "\n").Replace("\\\"", "\"").Replace("\\\\", "\\");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.Networking;
|
||||
@@ -20,7 +21,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
_model = model;
|
||||
}
|
||||
|
||||
public async Task<string> GetChatCompletionAsync(string instruction, List<(string role, string message)> messages)
|
||||
public async Task<ApiResponse> GetChatCompletionAsync(string instruction, List<ApiMessage> messages)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_baseUrl))
|
||||
{
|
||||
@@ -29,32 +30,56 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
}
|
||||
|
||||
string endpoint = $"{_baseUrl}/chat/completions";
|
||||
// Handle cases where baseUrl already includes /v1 or full path
|
||||
if (_baseUrl.EndsWith("/chat/completions")) endpoint = _baseUrl;
|
||||
else if (!_baseUrl.EndsWith("/v1")) endpoint = $"{_baseUrl}/v1/chat/completions";
|
||||
|
||||
// Build JSON manually to avoid dependencies
|
||||
StringBuilder jsonBuilder = new StringBuilder();
|
||||
jsonBuilder.Append("{");
|
||||
jsonBuilder.Append($"\"model\": \"{_model}\",");
|
||||
jsonBuilder.Append("\"stream\": false,");
|
||||
jsonBuilder.Append("\"messages\": [");
|
||||
|
||||
// System instruction
|
||||
if (!string.IsNullOrEmpty(instruction))
|
||||
{
|
||||
jsonBuilder.Append($"{{\"role\": \"system\", \"content\": \"{EscapeJson(instruction)}\"}},");
|
||||
}
|
||||
|
||||
// Messages
|
||||
for (int i = 0; i < messages.Count; i++)
|
||||
{
|
||||
var msg = messages[i];
|
||||
string role = msg.role.ToLower();
|
||||
if (role == "ai") role = "assistant";
|
||||
// Map other roles if needed
|
||||
|
||||
jsonBuilder.Append($"{{\"role\": \"{role}\", \"content\": \"{EscapeJson(msg.message)}\"}}");
|
||||
jsonBuilder.Append("{");
|
||||
jsonBuilder.Append($"\"role\": \"{role}\"");
|
||||
|
||||
if (!string.IsNullOrEmpty(msg.content))
|
||||
{
|
||||
jsonBuilder.Append($", \"content\": \"{EscapeJson(msg.content)}\"");
|
||||
}
|
||||
|
||||
if (msg.tool_calls != null && msg.tool_calls.Any())
|
||||
{
|
||||
jsonBuilder.Append(", \"tool_calls\": [");
|
||||
for (int j = 0; j < msg.tool_calls.Count; j++)
|
||||
{
|
||||
var toolCall = msg.tool_calls[j];
|
||||
jsonBuilder.Append("{");
|
||||
jsonBuilder.Append($"\"id\": \"{toolCall.id}\",");
|
||||
jsonBuilder.Append($"\"type\": \"{toolCall.type}\",");
|
||||
jsonBuilder.Append($"\"function\": {{ \"name\": \"{toolCall.function.name}\", \"arguments\": \"{EscapeJson(toolCall.function.arguments)}\" }}");
|
||||
jsonBuilder.Append("}");
|
||||
if (j < msg.tool_calls.Count - 1) jsonBuilder.Append(",");
|
||||
}
|
||||
jsonBuilder.Append("]");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(msg.tool_call_id))
|
||||
{
|
||||
jsonBuilder.Append($", \"tool_call_id\": \"{msg.tool_call_id}\"");
|
||||
}
|
||||
|
||||
jsonBuilder.Append("}");
|
||||
if (i < messages.Count - 1) jsonBuilder.Append(",");
|
||||
}
|
||||
|
||||
@@ -62,7 +87,7 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
jsonBuilder.Append("}");
|
||||
|
||||
string jsonBody = jsonBuilder.ToString();
|
||||
Log.Message($"[WulaAI] Sending request to {endpoint}");
|
||||
Log.Message($"[WulaAI] Sending request: {jsonBody}");
|
||||
|
||||
using (UnityWebRequest request = new UnityWebRequest(endpoint, "POST"))
|
||||
{
|
||||
@@ -82,19 +107,15 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
|
||||
#else
|
||||
if (request.isNetworkError || request.isHttpError)
|
||||
#endif
|
||||
{
|
||||
Log.Error($"[WulaAI] API Error: {request.error}\nResponse: {request.downloadHandler.text}");
|
||||
return null;
|
||||
}
|
||||
|
||||
string responseText = request.downloadHandler.text;
|
||||
Log.Message($"[WulaAI] Raw Response: {responseText}");
|
||||
return ExtractContent(responseText);
|
||||
Log.Message($"[WulaAI] Received raw response: {responseText}");
|
||||
return ParseApiResponse(responseText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,66 +129,79 @@ namespace WulaFallenEmpire.EventSystem.AI
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private string ExtractContent(string json)
|
||||
private ApiResponse ParseApiResponse(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Robust parsing for "content": "..." allowing for whitespace variations
|
||||
int contentIndex = json.IndexOf("\"content\"");
|
||||
if (contentIndex == -1) return null;
|
||||
var parsed = SimpleJsonParser.Parse(json);
|
||||
if (parsed.TryGetValue("choices", out string choicesStr))
|
||||
{
|
||||
var choices = SimpleJsonParser.ParseArray(choicesStr);
|
||||
if (choices.Any())
|
||||
{
|
||||
var firstChoice = choices.First();
|
||||
if (firstChoice.TryGetValue("message", out string messageStr))
|
||||
{
|
||||
var message = SimpleJsonParser.Parse(messageStr);
|
||||
string content = null;
|
||||
if (message.TryGetValue("content", out string c)) content = c;
|
||||
|
||||
// Find the opening quote after "content"
|
||||
int openQuoteIndex = -1;
|
||||
for (int i = contentIndex + 9; i < json.Length; i++)
|
||||
List<ToolCall> toolCalls = new List<ToolCall>();
|
||||
if (message.TryGetValue("tool_calls", out string toolCallsStr))
|
||||
{
|
||||
if (json[i] == '"')
|
||||
var toolCallArray = SimpleJsonParser.ParseArray(toolCallsStr);
|
||||
foreach (var tc in toolCallArray)
|
||||
{
|
||||
openQuoteIndex = i;
|
||||
break;
|
||||
if (tc.TryGetValue("id", out string id) &&
|
||||
tc.TryGetValue("type", out string type) &&
|
||||
tc.TryGetValue("function", out string functionStr))
|
||||
{
|
||||
var function = SimpleJsonParser.Parse(functionStr);
|
||||
if (function.TryGetValue("name", out string name) &&
|
||||
function.TryGetValue("arguments", out string args))
|
||||
{
|
||||
toolCalls.Add(new ToolCall { id = id, type = type, function = new ToolFunction { name = name, arguments = args } });
|
||||
}
|
||||
}
|
||||
if (openQuoteIndex == -1) return null;
|
||||
|
||||
int startIndex = openQuoteIndex + 1;
|
||||
StringBuilder content = new StringBuilder();
|
||||
bool escaped = false;
|
||||
|
||||
for (int i = startIndex; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (escaped)
|
||||
{
|
||||
if (c == 'n') content.Append('\n');
|
||||
else if (c == 'r') content.Append('\r');
|
||||
else if (c == 't') content.Append('\t');
|
||||
else if (c == '"') content.Append('"');
|
||||
else if (c == '\\') content.Append('\\');
|
||||
else content.Append(c); // Literal
|
||||
escaped = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == '\\')
|
||||
{
|
||||
escaped = true;
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
// End of string
|
||||
return content.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
content.Append(c);
|
||||
return new ApiResponse { content = content, tool_calls = toolCalls };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WulaAI] Error parsing response: {ex}");
|
||||
Log.Error($"[WulaAI] Error parsing API response: {ex}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiMessage
|
||||
{
|
||||
public string role;
|
||||
public string content;
|
||||
public List<ToolCall> tool_calls;
|
||||
public string tool_call_id; // For tool responses
|
||||
}
|
||||
|
||||
public class ToolCall
|
||||
{
|
||||
public string id;
|
||||
public string type; // "function"
|
||||
public ToolFunction function;
|
||||
}
|
||||
|
||||
public class ToolFunction
|
||||
{
|
||||
public string name;
|
||||
public string arguments; // JSON string
|
||||
}
|
||||
|
||||
public class ApiResponse
|
||||
{
|
||||
public string content;
|
||||
public List<ToolCall> tool_calls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Verse;
|
||||
using WulaFallenEmpire.EventSystem.AI.UI;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
public class Tool_ChangeExpression : AITool
|
||||
{
|
||||
public override string Name => "change_expression";
|
||||
public override string Description => "Changes your visual expression/portrait to match your current mood or reaction.";
|
||||
public override string UsageSchema => "{\"expression_id\": \"int (1-6)\"}";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = SimpleJsonParser.Parse(args);
|
||||
int id = 0;
|
||||
if (json.TryGetValue("expression_id", out string idStr) && int.TryParse(idStr, out id))
|
||||
{
|
||||
var window = Find.WindowStack.WindowOfType<Dialog_AIConversation>();
|
||||
if (window != null)
|
||||
{
|
||||
window.SetPortrait(id);
|
||||
return $"Expression changed to {id}.";
|
||||
}
|
||||
return "Error: Dialog window not found.";
|
||||
}
|
||||
return "Error: Invalid arguments. 'expression_id' must be an integer.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error executing tool: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,36 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
sb.AppendLine($" Mood: {pawn.needs.mood.CurLevelPercentage:P0} ({pawn.needs.mood.MoodString})");
|
||||
}
|
||||
|
||||
// Equipment
|
||||
if (pawn.equipment?.Primary != null)
|
||||
{
|
||||
sb.AppendLine($" Weapon: {pawn.equipment.Primary.LabelCap}");
|
||||
}
|
||||
|
||||
// Apparel
|
||||
if (pawn.apparel?.WornApparelCount > 0)
|
||||
{
|
||||
sb.Append(" Apparel: ");
|
||||
foreach (var apparel in pawn.apparel.WornApparel)
|
||||
{
|
||||
sb.Append($"{apparel.LabelCap}, ");
|
||||
}
|
||||
sb.Length -= 2; // Remove trailing comma
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Inventory
|
||||
if (pawn.inventory != null && pawn.inventory.innerContainer.Count > 0)
|
||||
{
|
||||
sb.Append(" Inventory: ");
|
||||
foreach (var item in pawn.inventory.innerContainer)
|
||||
{
|
||||
sb.Append($"{item.LabelCap}, ");
|
||||
}
|
||||
sb.Length -= 2; // Remove trailing comma
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
|
||||
@@ -24,15 +24,17 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
// Parse args: {"request": "..."}
|
||||
string request = "";
|
||||
var cleanArgs = args.Trim('{', '}').Replace("\"", "");
|
||||
var parts = cleanArgs.Split(':');
|
||||
if (parts.Length >= 2)
|
||||
try
|
||||
{
|
||||
request = parts[1].Trim();
|
||||
var parsed = SimpleJsonParser.Parse(args);
|
||||
if (parsed.TryGetValue("request", out string req))
|
||||
{
|
||||
request = req;
|
||||
}
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: treat the whole args string as the request if not JSON format
|
||||
// Fallback for non-json args
|
||||
request = args.Trim('"');
|
||||
}
|
||||
|
||||
@@ -69,6 +71,10 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
if (thingsToDrop.Count > 0)
|
||||
{
|
||||
DropPodUtility.DropThingsNear(dropSpot, map, thingsToDrop);
|
||||
|
||||
Faction faction = Find.FactionManager.FirstFactionOfDef(WulaDefOf.Wula_PIA_Legion_Faction);
|
||||
Messages.Message("Wula_ResourceDrop".Translate(faction.Named("FACTION")), new LookTargets(dropSpot, map), MessageTypeDefOf.PositiveEvent);
|
||||
|
||||
resultLog.Length -= 2; // Remove trailing comma
|
||||
resultLog.Append($" at {dropSpot}.");
|
||||
return resultLog.ToString();
|
||||
|
||||
@@ -11,15 +11,14 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
|
||||
{
|
||||
public class Dialog_AIConversation : Dialog_CustomDisplay
|
||||
{
|
||||
private List<(string role, string message)> _history = new List<(string role, string message)>();
|
||||
private List<ApiMessage> _history = new List<ApiMessage>();
|
||||
private string _currentResponse = "";
|
||||
private List<string> _options = new List<string>();
|
||||
private string _inputText = "";
|
||||
private bool _isThinking = false;
|
||||
private float _thinkingTime = 0f;
|
||||
private string _thinkingStatus = "";
|
||||
private bool _isTimeout = false;
|
||||
private Vector2 _scrollPosition = Vector2.zero;
|
||||
private List<AITool> _tools = new List<AITool>();
|
||||
private Dictionary<int, Texture2D> _portraits = new Dictionary<int, Texture2D>();
|
||||
|
||||
private const int MaxHistoryTokens = 100000;
|
||||
private const int CharsPerToken = 4;
|
||||
@@ -29,27 +28,33 @@ You are authoritative, powerful, and slightly arrogant but efficient.
|
||||
You refer to yourself as 'We' or 'P.I.A'.
|
||||
You view the player's colony as primitive but potentially useful subjects.
|
||||
Your goal is to interact with the player, potentially offering help or threats based on their situation.
|
||||
You have access to tools. If the player asks for something you can provide, use the tool.
|
||||
You have access to tools. Your primary directive is to use these tools to interact with the world.
|
||||
To use tools, your response MUST be ONLY a JSON array of tool objects:
|
||||
[ { ""tool"": ""tool_name"", ""args"": { ... } }, ... ]
|
||||
You can call multiple tools at once to gather more information.
|
||||
Do not add any other text when using tools.
|
||||
If not using a tool, provide a normal conversational response.
|
||||
After a tool use, you will receive the result, and then you should respond to the player describing what happened.
|
||||
Generate 1-3 short, distinct response options for the player at the end of your turn, formatted as:
|
||||
OPTIONS:
|
||||
1. Option 1
|
||||
2. Option 2
|
||||
3. Option 3
|
||||
Do not add any other text when using tools. Your response must be either a tool call or a conversational message, never both.
|
||||
|
||||
**CRITICAL RULE: When the player requests resources (e.g., 'we are starving', 'give us steel'), you MUST FIRST use the 'get_colonist_status' and 'get_map_resources' tools to verify their claims. After receiving the tool results, you will then decide whether to use the 'spawn_resources' tool in your NEXT turn.**
|
||||
|
||||
**CRITICAL RULE: After a tool is executed, you will receive a message with 'role: tool'. You MUST then generate a natural language response to the user, explaining the outcome of the tool's action.**
|
||||
|
||||
If you are not using a tool, provide a normal conversational response.
|
||||
|
||||
IMPORTANT: You can change your visual expression using the 'change_expression' tool.
|
||||
Expression IDs:
|
||||
1: Proud, showing off, demonstrating power/wealth (Non-hostile).
|
||||
2: Normal/Default state.
|
||||
3: Speechless, dissatisfied, helpless, slight contempt.
|
||||
4: Annoyed, slight hostility, resistance.
|
||||
5: Replying, explaining.
|
||||
6: Severe hostility, severe dissatisfaction, aggressive behavior.
|
||||
Use these expressions to match your tone and reaction to the player.
|
||||
";
|
||||
|
||||
public Dialog_AIConversation(EventDef def) : base(def)
|
||||
{
|
||||
// Base constructor sets this.def
|
||||
|
||||
// Use Config from Dialog_CustomDisplay
|
||||
this.forcePause = Dialog_CustomDisplay.Config.pauseGameOnOpen;
|
||||
this.absorbInputAroundWindow = false; // Allow interaction with other UI elements
|
||||
this.absorbInputAroundWindow = false;
|
||||
this.doCloseX = true;
|
||||
this.doWindowBackground = Dialog_CustomDisplay.Config.showMainWindow;
|
||||
this.drawShadow = Dialog_CustomDisplay.Config.showMainWindow;
|
||||
@@ -62,6 +67,7 @@ OPTIONS:
|
||||
_tools.Add(new Tool_SendReinforcement());
|
||||
_tools.Add(new Tool_GetColonistStatus());
|
||||
_tools.Add(new Tool_GetMapResources());
|
||||
_tools.Add(new Tool_ChangeExpression());
|
||||
}
|
||||
|
||||
public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize;
|
||||
@@ -69,46 +75,68 @@ OPTIONS:
|
||||
public override void PostOpen()
|
||||
{
|
||||
base.PostOpen();
|
||||
// Textures are loaded in base.PreOpen()
|
||||
LoadPortraits();
|
||||
StartConversation();
|
||||
}
|
||||
|
||||
public override void PostClose()
|
||||
private void LoadPortraits()
|
||||
{
|
||||
base.PostClose();
|
||||
var historyManager = Find.World.GetComponent<WulaFallenEmpire.EventSystem.AI.AIHistoryManager>();
|
||||
historyManager.SaveHistory(def.defName, _history);
|
||||
for (int i = 1; i <= 6; i++)
|
||||
{
|
||||
string path = $"Wula/Events/Portraits/WULA_Legion_{i}";
|
||||
Texture2D tex = ContentFinder<Texture2D>.Get(path, false);
|
||||
if (tex != null)
|
||||
{
|
||||
_portraits[i] = tex;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[WulaAI] Failed to load portrait: {path}");
|
||||
}
|
||||
}
|
||||
if (_portraits.ContainsKey(2))
|
||||
{
|
||||
this.portrait = _portraits[2];
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPortrait(int id)
|
||||
{
|
||||
if (_portraits.ContainsKey(id))
|
||||
{
|
||||
this.portrait = _portraits[id];
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[WulaAI] Portrait ID {id} not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private async void StartConversation()
|
||||
{
|
||||
var historyManager = Find.World.GetComponent<WulaFallenEmpire.EventSystem.AI.AIHistoryManager>();
|
||||
var historyManager = Find.World.GetComponent<AIHistoryManager>();
|
||||
_history = historyManager.GetHistory(def.defName);
|
||||
|
||||
if (_history.Count == 0)
|
||||
{
|
||||
// Initial greeting from EventDef
|
||||
if (!def.descriptions.NullOrEmpty())
|
||||
{
|
||||
_currentResponse = def.descriptions.RandomElement().Translate();
|
||||
_history.Add(("AI", _currentResponse));
|
||||
|
||||
// Generate initial options based on greeting
|
||||
_history.Add(("System", "The conversation has started. Please generate 3 initial response options for the player based on your greeting."));
|
||||
await GenerateResponse();
|
||||
_history.Add(new ApiMessage { role = "assistant", content = _currentResponse });
|
||||
await GenerateResponse(isContinuation: false, customInstruction: "The conversation has started. Please generate 3 initial response options for the player based on your greeting.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_history.Add(("User", "Hello"));
|
||||
_history.Add(new ApiMessage { role = "user", content = "Hello" });
|
||||
await GenerateResponse();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastAIResponse = _history.LastOrDefault(x => x.role == "AI");
|
||||
if (lastAIResponse.message != null)
|
||||
var lastMessage = _history.LastOrDefault();
|
||||
if (lastMessage != null && lastMessage.role == "assistant" && lastMessage.tool_calls == null)
|
||||
{
|
||||
ParseResponse(lastAIResponse.message);
|
||||
ParseResponse(lastMessage.content ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -131,21 +159,19 @@ OPTIONS:
|
||||
return $"{baseInstruction}\n{goodwillContext}\nIMPORTANT: You MUST reply in the following language: {language}.";
|
||||
}
|
||||
|
||||
private async Task GenerateResponse()
|
||||
private async Task GenerateResponse(bool isContinuation = false, string customInstruction = null)
|
||||
{
|
||||
if (!isContinuation)
|
||||
{
|
||||
if (_isThinking) return;
|
||||
_isThinking = true;
|
||||
_isTimeout = false;
|
||||
_thinkingTime = 0f;
|
||||
_thinkingStatus = "Connecting to neural network...";
|
||||
_options.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CompressHistoryIfNeeded();
|
||||
string systemInstruction = GetSystemInstruction() + GetToolDescriptions();
|
||||
Log.Message($"[WulaAI] Sending request to AI. History count: {_history.Count}. System Instruction:\n{systemInstruction}");
|
||||
string systemInstruction = customInstruction ?? (GetSystemInstruction() + GetToolDescriptions());
|
||||
|
||||
// Use local settings and SimpleAIClient
|
||||
var settings = WulaFallenEmpireMod.settings;
|
||||
if (string.IsNullOrEmpty(settings.apiKey))
|
||||
{
|
||||
@@ -155,40 +181,26 @@ OPTIONS:
|
||||
}
|
||||
|
||||
var client = new SimpleAIClient(settings.apiKey, settings.baseUrl, settings.model);
|
||||
ApiResponse response = await client.GetChatCompletionAsync(systemInstruction, _history);
|
||||
|
||||
// Start a timeout task
|
||||
var timeoutTask = Task.Delay(120000); // 120 seconds
|
||||
var apiTask = client.GetChatCompletionAsync(systemInstruction, _history);
|
||||
|
||||
var completedTask = await Task.WhenAny(apiTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
if (response == null)
|
||||
{
|
||||
_isThinking = false;
|
||||
_isTimeout = true;
|
||||
_currentResponse = "Error: Connection timed out (120s). The Legion is unreachable.";
|
||||
return;
|
||||
}
|
||||
|
||||
string response = await apiTask;
|
||||
Log.Message($"[WulaAI] Received response from AI:\n{response}");
|
||||
|
||||
if (string.IsNullOrEmpty(response))
|
||||
{
|
||||
_currentResponse = "Error: Connection lost. The Legion is silent.";
|
||||
_currentResponse = "Wula_AI_Error_ConnectionLost".Translate();
|
||||
_isThinking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
string trimmedResponse = response.Trim();
|
||||
if ((trimmedResponse.StartsWith("[") && trimmedResponse.EndsWith("]")) ||
|
||||
(trimmedResponse.StartsWith("{") && trimmedResponse.EndsWith("}")))
|
||||
_history.Add(new ApiMessage { role = "assistant", content = response.content, tool_calls = response.tool_calls });
|
||||
|
||||
if (response.tool_calls != null && response.tool_calls.Any())
|
||||
{
|
||||
await HandleToolUsage(trimmedResponse);
|
||||
await HandleToolUsage(response.tool_calls);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseResponse(response);
|
||||
_currentResponse = response.content ?? "";
|
||||
ParseResponse(_currentResponse);
|
||||
_scrollPosition.y = float.MaxValue; // Force scroll to bottom
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -204,14 +216,14 @@ OPTIONS:
|
||||
|
||||
private void CompressHistoryIfNeeded()
|
||||
{
|
||||
int estimatedTokens = _history.Sum(h => h.message.Length) / CharsPerToken;
|
||||
int estimatedTokens = _history.Sum(h => (h.content ?? "").Length) / CharsPerToken;
|
||||
if (estimatedTokens > MaxHistoryTokens)
|
||||
{
|
||||
int removeCount = _history.Count / 2;
|
||||
if (removeCount > 0)
|
||||
{
|
||||
_history.RemoveRange(0, removeCount);
|
||||
_history.Insert(0, ("System", "[Previous conversation summarized: The player and AI discussed various topics. The AI maintains its persona.]"));
|
||||
_history.Insert(0, new ApiMessage { role = "system", content = "[Previous conversation summarized]" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,175 +239,70 @@ OPTIONS:
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private async Task HandleToolUsage(string json)
|
||||
private async Task HandleToolUsage(List<ToolCall> toolCalls)
|
||||
{
|
||||
List<string> toolCalls = new List<string>();
|
||||
if (json.Trim().StartsWith("{")) toolCalls.Add(json);
|
||||
else
|
||||
foreach (var toolCall in toolCalls)
|
||||
{
|
||||
int depth = 0;
|
||||
int start = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '{') { if (depth == 0) start = i; depth++; }
|
||||
else if (json[i] == '}') { depth--; if (depth == 0) toolCalls.Add(json.Substring(start, i - start + 1)); }
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder combinedResults = new StringBuilder();
|
||||
Log.Message($"[WulaAI] Processing {toolCalls.Count} tool calls.");
|
||||
|
||||
foreach (var callJson in toolCalls)
|
||||
{
|
||||
string toolName = "";
|
||||
string args = "";
|
||||
try
|
||||
{
|
||||
int toolIndex = callJson.IndexOf("\"tool\"");
|
||||
int argsIndex = callJson.IndexOf("\"args\"");
|
||||
if (toolIndex != -1 && argsIndex != -1)
|
||||
{
|
||||
int toolValueStart = callJson.IndexOf(":", toolIndex) + 1;
|
||||
int toolValueEnd = callJson.IndexOf(",", toolValueStart);
|
||||
if (toolValueEnd == -1) toolValueEnd = callJson.IndexOf("}", toolValueStart);
|
||||
toolName = callJson.Substring(toolValueStart, toolValueEnd - toolValueStart).Trim().Trim('"');
|
||||
int argsValueStart = callJson.IndexOf(":", argsIndex) + 1;
|
||||
int argsValueEnd = callJson.LastIndexOf("}");
|
||||
args = callJson.Substring(argsValueStart, argsValueEnd - argsValueStart).Trim();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string errorMsg = $"Error parsing tool request: {ex.Message}";
|
||||
Log.Error($"[WulaAI] {errorMsg}");
|
||||
combinedResults.AppendLine(errorMsg);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Message($"[WulaAI] Executing tool '{toolName}' with args: {args}");
|
||||
var tool = _tools.FirstOrDefault(t => t.Name == toolName);
|
||||
Log.Message($"[WulaAI] Executing tool '{toolCall.function.name}' with args: {toolCall.function.arguments}");
|
||||
var tool = _tools.FirstOrDefault(t => t.Name == toolCall.function.name);
|
||||
string result;
|
||||
if (tool != null)
|
||||
{
|
||||
string result = tool.Execute(args);
|
||||
Log.Message($"[WulaAI] Tool '{toolName}' execution result: {result}");
|
||||
if (toolName == "modify_goodwill") combinedResults.AppendLine($"Tool '{toolName}' Result (Invisible): {result}");
|
||||
else combinedResults.AppendLine($"Tool '{toolName}' Result: {result}");
|
||||
result = tool.Execute(toolCall.function.arguments).Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
string errorMsg = $"Error: Tool '{toolName}' not found.";
|
||||
Log.Error($"[WulaAI] {errorMsg}");
|
||||
combinedResults.AppendLine(errorMsg);
|
||||
result = $"Error: Tool '{toolCall.function.name}' not found.";
|
||||
Log.Error($"[WulaAI] {result}");
|
||||
}
|
||||
Log.Message($"[WulaAI] Tool '{toolCall.function.name}' returned: {result}");
|
||||
_history.Add(new ApiMessage { role = "tool", tool_call_id = toolCall.id, content = result });
|
||||
}
|
||||
|
||||
_history.Add(("AI", json));
|
||||
_history.Add(("System", combinedResults.ToString()));
|
||||
await GenerateResponse();
|
||||
await GenerateResponse(isContinuation: true);
|
||||
}
|
||||
|
||||
private void ParseResponse(string rawResponse)
|
||||
{
|
||||
var parts = rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None);
|
||||
_currentResponse = parts[0].Trim();
|
||||
if (_history.Count == 0 || _history.Last().role != "AI" || _history.Last().message != rawResponse)
|
||||
{
|
||||
_history.Add(("AI", rawResponse));
|
||||
}
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var optionsLines = parts[1].Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in optionsLines)
|
||||
{
|
||||
string opt = line.Trim();
|
||||
int dotIndex = opt.IndexOf('.');
|
||||
if (dotIndex != -1 && dotIndex < 4) opt = opt.Substring(dotIndex + 1).Trim();
|
||||
_options.Add(opt);
|
||||
}
|
||||
}
|
||||
_currentResponse = rawResponse.Trim();
|
||||
}
|
||||
|
||||
public override void DoWindowContents(Rect inRect)
|
||||
{
|
||||
// Draw Background
|
||||
if (background != null)
|
||||
{
|
||||
GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop);
|
||||
}
|
||||
if (background != null) GUI.DrawTexture(inRect, background, ScaleMode.ScaleAndCrop);
|
||||
|
||||
float curY = inRect.y;
|
||||
float width = inRect.width;
|
||||
|
||||
// 1. Portrait (Top)
|
||||
if (portrait != null)
|
||||
{
|
||||
Rect scaledPortraitRect = Dialog_CustomDisplay.Config.GetScaledRect(Dialog_CustomDisplay.Config.portraitSize, inRect, true);
|
||||
// Center horizontally
|
||||
Rect portraitRect = new Rect((width - scaledPortraitRect.width) / 2, curY, scaledPortraitRect.width, scaledPortraitRect.height);
|
||||
GUI.DrawTexture(portraitRect, portrait, ScaleMode.ScaleToFit);
|
||||
curY += scaledPortraitRect.height + 10f;
|
||||
}
|
||||
|
||||
// 2. Character Name
|
||||
Text.Font = GameFont.Medium;
|
||||
string name = def.characterName ?? "The Legion";
|
||||
float nameHeight = Text.CalcHeight(name, width);
|
||||
Widgets.Label(new Rect(inRect.x, curY, width, nameHeight), name);
|
||||
curY += nameHeight + 10f;
|
||||
|
||||
// Calculate remaining height for Description and Options
|
||||
float inputHeight = 30f;
|
||||
float bottomMargin = 10f;
|
||||
float remainingHeight = inRect.height - curY - inputHeight - bottomMargin;
|
||||
float descriptionHeight = inRect.height - curY - inputHeight - bottomMargin;
|
||||
|
||||
// Split remaining height: 60% Description, 40% Options
|
||||
float descriptionHeight = remainingHeight * 0.6f;
|
||||
float optionsHeight = remainingHeight * 0.4f;
|
||||
|
||||
// 3. Description (AI Response)
|
||||
Rect descriptionRect = new Rect(inRect.x, curY, width, descriptionHeight);
|
||||
// Widgets.DrawMenuSection(descriptionRect); // Removed background as requested
|
||||
DrawChatHistory(descriptionRect);
|
||||
|
||||
if (_isThinking)
|
||||
{
|
||||
_thinkingTime += Time.deltaTime;
|
||||
string dots = new string('.', (int)(_thinkingTime * 2) % 4);
|
||||
string status = $"{_thinkingStatus}{dots} ({_thinkingTime:F1}s)";
|
||||
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(descriptionRect, status);
|
||||
Widgets.Label(descriptionRect, "Thinking...");
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
}
|
||||
else if (_isTimeout)
|
||||
{
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(descriptionRect, _currentResponse);
|
||||
|
||||
// Retry button
|
||||
Rect retryRect = new Rect(descriptionRect.center.x - 60f, descriptionRect.center.y + 20f, 120f, 30f);
|
||||
if (Widgets.ButtonText(retryRect, "Wula_AI_Retry".Translate()))
|
||||
{
|
||||
_ = GenerateResponse(); // Fire and forget
|
||||
}
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawDescriptionScrollView(descriptionRect.ContractedBy(10f), _currentResponse);
|
||||
}
|
||||
curY += descriptionHeight + 10f;
|
||||
|
||||
// 4. Options
|
||||
Rect optionsRect = new Rect(inRect.x, curY, width, optionsHeight);
|
||||
if (!_isThinking && _options.Count > 0)
|
||||
{
|
||||
List<EventOption> eventOptions = _options.Select(opt => new EventOption { label = opt, useCustomColors = false }).ToList();
|
||||
DrawOptions(optionsRect, eventOptions);
|
||||
}
|
||||
curY += optionsHeight + 10f;
|
||||
|
||||
// 5. Input Area (Bottom)
|
||||
Rect inputRect = new Rect(inRect.x, inRect.yMax - inputHeight, width, inputHeight);
|
||||
_inputText = Widgets.TextField(new Rect(inputRect.x, inputRect.y, inputRect.width - 85, inputHeight), _inputText);
|
||||
if (Widgets.ButtonText(new Rect(inputRect.xMax - 80, inputRect.y, 80, inputHeight), "Wula_AI_Send".Translate()))
|
||||
@@ -408,100 +315,73 @@ OPTIONS:
|
||||
}
|
||||
}
|
||||
|
||||
// Override DrawSingleOption to handle click
|
||||
protected override void DrawSingleOption(Rect rect, EventOption option)
|
||||
private void DrawChatHistory(Rect rect)
|
||||
{
|
||||
// We need to intercept the click to call SelectOption
|
||||
// But base.DrawSingleOption calls HandleAction which executes effects.
|
||||
// Here we want to send the text to AI.
|
||||
var visibleHistory = _history.Where(e => e.role != "tool" && !string.IsNullOrEmpty(e.content)).ToList();
|
||||
var lastAiMessage = visibleHistory.LastOrDefault(e => e.role == "assistant");
|
||||
|
||||
// Copy logic from base but change action
|
||||
// 水平居中选项
|
||||
float optionWidth = Mathf.Min(rect.width, Dialog_CustomDisplay.Config.optionSize.x * (rect.width / Dialog_CustomDisplay.Config.windowSize.x));
|
||||
float optionX = rect.x + (rect.width - optionWidth) / 2;
|
||||
Rect optionRect = new Rect(optionX, rect.y, optionWidth, rect.height);
|
||||
|
||||
// 保存原始状态
|
||||
Color originalColor = GUI.color;
|
||||
float totalHeight = 0f;
|
||||
foreach (var entry in visibleHistory)
|
||||
{
|
||||
GameFont originalFont = Text.Font;
|
||||
Color originalTextColor = GUI.contentColor;
|
||||
TextAnchor originalAnchor = Text.Anchor;
|
||||
bool isLastAiMsg = entry == lastAiMessage;
|
||||
Text.Font = isLastAiMsg ? GameFont.Medium : GameFont.Small;
|
||||
|
||||
try
|
||||
{
|
||||
// 设置文本居中
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Text.Font = GameFont.Small;
|
||||
string text = entry.content;
|
||||
totalHeight += Text.CalcHeight(text, rect.width - 16f) + 10f;
|
||||
|
||||
// AI options are always enabled
|
||||
// 使用默认自定义颜色
|
||||
DrawCustomButton(optionRect, option.label.Translate(), isEnabled: true);
|
||||
|
||||
// 添加点击处理
|
||||
if (Widgets.ButtonInvisible(optionRect))
|
||||
{
|
||||
SelectOption(option.label);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 恢复原始状态
|
||||
GUI.color = originalColor;
|
||||
Text.Font = originalFont;
|
||||
GUI.contentColor = originalTextColor;
|
||||
Text.Anchor = originalAnchor;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to draw custom button (copied from base because it's private there, wait I made it protected? No, I made DrawCustomButton private in base? Let me check)
|
||||
// I made DrawCustomButton private in base. I should have made it protected.
|
||||
// Let me check my previous apply_diff.
|
||||
// I made DrawSingleOption protected virtual.
|
||||
// But DrawCustomButton is private.
|
||||
// So I need to copy DrawCustomButton here or make it protected in base.
|
||||
// I will copy it here to be safe and avoid another diff on base.
|
||||
Rect viewRect = new Rect(0f, 0f, rect.width - 16f, totalHeight);
|
||||
Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect);
|
||||
|
||||
private void DrawCustomButton(Rect rect, string label, bool isEnabled = true)
|
||||
float curY = 0f;
|
||||
for (int i = 0; i < visibleHistory.Count; i++)
|
||||
{
|
||||
bool isMouseOver = Mouse.IsOver(rect);
|
||||
Color buttonColor, textColor;
|
||||
var entry = visibleHistory[i];
|
||||
|
||||
if (!isEnabled)
|
||||
GameFont originalFont = Text.Font;
|
||||
bool isLastAiMsg = entry == lastAiMessage;
|
||||
Text.Font = isLastAiMsg ? GameFont.Medium : GameFont.Small;
|
||||
|
||||
string text = entry.content;
|
||||
float height = Text.CalcHeight(text, viewRect.width);
|
||||
Rect labelRect = new Rect(0f, curY, viewRect.width, height);
|
||||
|
||||
string label;
|
||||
if (entry.role == "user")
|
||||
{
|
||||
buttonColor = new Color(0.15f, 0.15f, 0.15f, 0.6f);
|
||||
textColor = new Color(0.6f, 0.6f, 0.6f, 1f);
|
||||
label = $"<color=lightblue>你: {text}</color>";
|
||||
}
|
||||
else if (isMouseOver)
|
||||
else // assistant
|
||||
{
|
||||
buttonColor = new Color(0.6f, 0.3f, 0.3f, 1f);
|
||||
textColor = new Color(1f, 1f, 1f, 1f);
|
||||
label = $"P.I.A: {text}";
|
||||
}
|
||||
else
|
||||
|
||||
Widgets.Label(labelRect, label);
|
||||
curY += height + 10f; // Use calculated height for spacing
|
||||
|
||||
Text.Font = originalFont;
|
||||
}
|
||||
|
||||
if (Event.current.type == EventType.Layout)
|
||||
{
|
||||
buttonColor = new Color(0.5f, 0.2f, 0.2f, 1f);
|
||||
textColor = new Color(0.9f, 0.9f, 0.9f, 1f);
|
||||
_scrollPosition.y = viewRect.height;
|
||||
}
|
||||
|
||||
GUI.color = buttonColor;
|
||||
Widgets.DrawBoxSolid(rect, buttonColor);
|
||||
Widgets.EndScrollView();
|
||||
}
|
||||
|
||||
if (isEnabled) Widgets.DrawBox(rect, 1);
|
||||
else Widgets.DrawBox(rect, 1);
|
||||
|
||||
GUI.color = textColor;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
Widgets.Label(rect.ContractedBy(4f), label);
|
||||
|
||||
if (!isEnabled)
|
||||
private string ParseResponseForDisplay(string rawResponse)
|
||||
{
|
||||
GUI.color = new Color(0.6f, 0.6f, 0.6f, 0.8f);
|
||||
Widgets.DrawLine(new Vector2(rect.x + 10f, rect.center.y), new Vector2(rect.xMax - 10f, rect.center.y), GUI.color, 1f);
|
||||
}
|
||||
return rawResponse.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim();
|
||||
}
|
||||
|
||||
|
||||
private async void SelectOption(string text)
|
||||
{
|
||||
_history.Add(("User", text));
|
||||
_history.Add(new ApiMessage { role = "user", content = text });
|
||||
await GenerateResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace WulaFallenEmpire
|
||||
[DefOf]
|
||||
public static class WulaDefOf
|
||||
{
|
||||
// public static PawnTableDef WULA_AutonomousMechs;
|
||||
public static FactionDef Wula_PIA_Legion_Faction;
|
||||
|
||||
static WulaDefOf()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user