1 line
97 KiB
JSON
1 line
97 KiB
JSON
{"repo": "badcharmvu/imvucompanion", "files": {"MainWindow.AiProviders.cs": {"7b0a50526ae70411190c2742a5c661390f2fcca5": {"t": 1785746216.6563344, "n": 17101, "h": "7038dfae29821791", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Windows;\nusing System.Windows.Controls;\n\nnamespace IMVUCompanion;\n\npublic partial class MainWindow\n{\n private static readonly string AiSettingsFile = UserDataPaths.GetConfigFile(\"ai_settings.json\");\n private const string AiCommandToken = \"!bbot\";\n private const string AiMaintenanceReply = \"Sorry, my conversation module is under maintenance.\";\n\n private static readonly string[] AiProviderNames =\n {\n \"Grok\", \"Claude\", \"Gemini\", \"GPT\", \"DeepSeek\", \"Mistral\", \"Qwen\", \"Llama\", \"Fireworks\"\n };\n\n private readonly Dictionary<string, FrameworkElement> _aiDynamicFields = new(StringComparer.OrdinalIgnoreCase);\n private AiSettings _aiSettings = new();\n private string _botDisplayName = \"\";\n private string? _aiFieldsBoundProvider;\n private bool _aiProviderUiSyncing;\n\n private sealed class ProviderConfig\n {\n public string ApiKey { get; set; } = \"\";\n public string Endpoint { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1024;\n public bool Enabled { get; set; }\n }\n\n private sealed class AiSettings\n {\n public string SelectedProvider { get; set; } = \"Grok\";\n public string BotDisplayName { get; set; } = \"\";\n /// <summary>Built-in paid AI name/trigger (AI Settings section) — not the same as provider keys.</summary>\n public string CompanionAiTrigger { get; set; } = \"\";\n public Dictionary<string, ProviderConfig> Providers { get; set; } =\n new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase);\n }\n\n private static ProviderConfig DefaultProviderConfig(string name) => name switch\n {\n \"Grok\" => new ProviderConfig\n {\n Endpoint = \"https://api.x.ai/v1\",\n Model = \"grok-2\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Claude\" => new ProviderConfig\n {\n Endpoint = \"https://api.anthropic.com/v1\",\n Model = \"claude-sonnet-4-20250514\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Gemini\" => new ProviderConfig\n {\n Endpoint = \"https://generativelanguage.googleapis.com/v1beta\",\n Model = \"gemini-2.0-flash\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"GPT\" => new ProviderConfig\n {\n Endpoint = \"https://api.openai.com/v1\",\n Model = \"gpt-4o\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"DeepSeek\" => new ProviderConfig\n {\n Endpoint = \"https://api.deepseek.com/v1\",\n Model = \"deepseek-chat\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Mistral\" => new ProviderConfig\n {\n Endpoint = \"https://api.mistral.ai/v1\",\n Model = \"mistral-large-latest\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Qwen\" => new ProviderConfig\n {\n Endpoint = \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n Model = \"qwen-plus\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Llama\" => new ProviderConfig\n {\n Endpoint = \"https://api.together.xyz/v1\",\n Model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Fireworks\" => new ProviderConfig\n {\n Endpoint = \"https://api.fireworks.ai/inference/v1\",\n Model = \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n _ => new ProviderConfig()\n };\n\n private void InitAiProvidersUi()\n {\n if (AiProviderCombo == null) return;\n\n AiProviderCombo.Items.Clear();\n foreach (string name in AiProviderNames)\n AiProviderCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });\n\n LoadAiSettings();\n\n _aiProviderUiSyncing = true;\n try\n {\n string selected = _aiSettings.SelectedProvider;\n for (int i = 0; i < AiProviderCombo.Items.Count; i++)\n {\n if (AiProviderCombo.Items[i] is ComboBoxItem cbi &&\n string.Equals(cbi.Tag?.ToString(), selected, StringComparison.OrdinalIgnoreCase))\n {\n AiProviderCombo.SelectedIndex = i;\n break;\n }\n }\n if (AiProviderCombo.SelectedIndex < 0 && AiProviderCombo.Items.Count > 0)\n AiProviderCombo.SelectedIndex = 0;\n }\n finally\n {\n _aiProviderUiSyncing = false;\n }\n\n if (BotDisplayNameBox != null)\n BotDisplayNameBox.Text = _aiSettings.BotDisplayName;\n\n _botDisplayName = _aiSettings.BotDisplayName?.Trim() ?? \"\";\n _aiFieldsBoundProvider = GetSelectedAiProviderName();\n RebuildAiProviderFields();\n }\n\n private void LoadAiSettings()\n {\n _aiSettings = new AiSettings();\n try\n {\n if (File.Exists(AiSettingsFile))\n {\n string json = File.ReadAllText(AiSettingsFile);\n var loaded = JsonSerializer.Deserialize<AiSettings>(json);\n if (loaded != null) _aiSettings = loaded;\n }\n }\n catch (Exception ex) { AppendLog(\"Load AI settings err: \" + ex.Message, LogCategory.Warning); }\n\n // API keys may be DPAPI-wrapped on disk; keep plaintext only in memory\n bool hadLegacyPlain = false;\n foreach (var kv in _aiSettings.Providers)\n {\n if (kv.Value == null) continue;\n string stored = kv.Value.ApiKey ?? \"\";\n if (!string.IsNullOrEmpty(stored) && !SecretProtector.IsProtected(stored))\n hadLegacyPlain = true;\n kv.Value.ApiKey = SecretProtector.Unprotect(stored);\n }\n\n foreach (string name in AiProviderNames)\n {\n if (!_aiSettings.Providers.ContainsKey(name))\n _aiSettings.Providers[name] = DefaultProviderConfig(name);\n }\n\n if (string.IsNullOrWhiteSpace(_aiSettings.SelectedProvider) ||\n !AiProviderNames.Contains(_aiSettings.SelectedProvider, StringComparer.OrdinalIgnoreCase))\n _aiSettings.SelectedProvider = \"Grok\";\n\n // One-time upgrade: rewrite file so keys are DPAPI-protected at rest\n if (hadLegacyPlain)\n {\n try { SaveAiSettingsCore(logSuccess: false); }\n catch { /* next explicit save will protect */ }\n }\n }\n\n private void SaveAiSettings() => SaveAiSettingsCore(logSuccess: true);\n\n private void SaveAiSettingsCore(bool logSuccess)\n {\n try\n {\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n if (BotDisplayNameBox != null)\n _aiSettings.BotDisplayName = BotDisplayNameBox.Text.Trim();\n _botDisplayName = _aiSettings.BotDisplayName;\n\n // Snapshot for disk: encrypt API keys (DPAPI CurrentUser); leave in-memory plaintext\n if (CompanionAiTriggerBox != null)\n _aiSettings.CompanionAiTrigger = CompanionAiTriggerBox.Text.Trim();\n\n var disk = new AiSettings\n {\n SelectedProvider = _aiSettings.SelectedProvider,\n BotDisplayName = _aiSettings.BotDisplayName,\n CompanionAiTrigger = _aiSettings.CompanionAiTrigger,\n Providers = new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase)\n };\n foreach (var kv in _aiSettings.Providers)\n {\n var src = kv.Value ?? DefaultProviderConfig(kv.Key);\n disk.Providers[kv.Key] = new ProviderConfig\n {\n ApiKey = SecretProtector.Protect(src.ApiKey),\n Endpoint = src.Endpoint,\n Model = src.Model,\n Temperature = src.Temperature,\n MaxTokens = src.MaxTokens,\n Enabled = src.Enabled\n };\n }\n\n Directory.CreateDirectory(UserDataPaths.Root);\n string json = JsonSerializer.Serialize(disk, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(AiSettingsFile, json);\n if (logSuccess)\n AppendLog(\"AI settings saved (API keys protected with Windows DPAPI).\", LogCategory.Info);\n }\n catch (Exception ex) { AppendLog(\"Save AI settings err: \" + ex.Message, LogCategory.Error); }\n }\n\n private string? GetSelectedAiProviderName()\n {\n if (AiProviderCombo?.SelectedItem is ComboBoxItem cbi)\n return cbi.Tag?.ToString() ?? cbi.Content?.ToString();\n return _aiSettings.SelectedProvider;\n }\n\n private ProviderConfig GetOrCreateProviderConfig(string name)\n {\n if (!_aiSettings.Providers.TryGetValue(name, out var cfg) || cfg == null)\n {\n cfg = DefaultProviderConfig(name);\n _aiSettings.Providers[name] = cfg;\n }\n return cfg;\n }\n\n private void FlushAiFieldsToSettings(string? providerName = null)\n {\n string? name = providerName ?? _aiFieldsBoundProvider ?? GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n var cfg = GetOrCreateProviderConfig(name);\n if (_aiDynamicFields.TryGetValue(\"ApiKey\", out var apiEl) && apiEl is PasswordBox apiPb)\n cfg.ApiKey = apiPb.Password;\n if (_aiDynamicFields.TryGetValue(\"Endpoint\", out var epEl) && epEl is TextBox epTb)\n cfg.Endpoint = epTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Model\", out var modelEl) && modelEl is TextBox modelTb)\n cfg.Model = modelTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Temperature\", out var tempEl) && tempEl is TextBox tempTb &&\n double.TryParse(tempTb.Text.Trim(), out double temp))\n cfg.Temperature = Math.Clamp(temp, 0, 2);\n if (_aiDynamicFields.TryGetValue(\"MaxTokens\", out var tokEl) && tokEl is TextBox tokTb &&\n int.TryParse(tokTb.Text.Trim(), out int tok))\n cfg.MaxTokens = Math.Max(1, tok);\n if (_aiDynamicFields.TryGetValue(\"Enabled\", out var enEl) && enEl is CheckBox enCb)\n cfg.Enabled = enCb.IsChecked == true;\n }\n\n private void RebuildAiProviderFields()\n {\n if (AiProviderFieldsPanel == null) return;\n\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n _aiSettings.SelectedProvider = name;\n _aiFieldsBoundProvider = name;\n var cfg = GetOrCreateProviderConfig(name);\n\n AiProviderFieldsPanel.Children.Clear();\n _aiDynamicFields.Clear();\n\n AiProviderFieldsPanel.Children.Add(MakeAiHint(\n name switch\n {\n \"Grok\" => \"xAI Grok — OpenAI-compatible chat completions.\",\n \"Claude\" => \"Anthropic Claude — Messages API compatible endpoint.\",\n \"Gemini\" => \"Google Gemini — API key from Google AI Studio.\",\n \"GPT\" => \"OpenAI GPT — standard OpenAI API.\",\n \"DeepSeek\" => \"DeepSeek — OpenAI-compatible API.\",\n \"Mistral\" => \"Mistral AI — La Plateforme API.\",\n \"Qwen\" => \"Alibaba Qwen — DashScope compatible mode.\",\n \"Llama\" => \"Meta Llama — via Together or compatible host.\",\n \"Fireworks\" => \"Fireworks AI — fast inference API.\",\n _ => \"Configure API credentials for this provider.\"\n }));\n\n AddAiPasswordField(\"ApiKey\", \"API Key\", cfg.ApiKey, \"Paste your API key\");\n AddAiTextField(\"Endpoint\", \"API Endpoint\", cfg.Endpoint, \"https://...\");\n AddAiTextField(\"Model\", \"Model\", cfg.Model, \"model-id\");\n AddAiTextField(\"Temperature\", \"Temperature\", cfg.Temperature.ToString(\"0.##\"), \"0.0 – 2.0\");\n AddAiTextField(\"MaxTokens\", \"Max Tokens\", cfg.MaxTokens.ToString(), \"e.g. 1024\");\n AddAiCheckField(\"Enabled\", \"Enable this provider\", cfg.Enabled);\n\n if (AiProviderStatusText != null)\n AiProviderStatusText.Text = cfg.Enabled\n ? $\"{name} enabled — AI wiring comes in a future update.\"\n : $\"{name} configured (disabled until enabled).\";\n }\n\n private static TextBlock MakeAiHint(string text) => new()\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x70, 0x70, 0x90)),\n TextWrapping = TextWrapping.Wrap,\n Margin = new Thickness(0, 0, 0, 6)\n };\n\n private void AddAiLabel(string text)\n {\n AiProviderFieldsPanel.Children.Add(new TextBlock\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xA0, 0xA0, 0xC8)),\n Margin = new Thickness(0, 4, 0, 2)\n });\n }\n\n private void AddAiTextField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var tb = new TextBox\n {\n Text = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(tb);\n _aiDynamicFields[key] = tb;\n }\n\n private void AddAiPasswordField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var pb = new PasswordBox\n {\n Password = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(pb);\n _aiDynamicFields[key] = pb;\n }\n\n private void AddAiCheckField(string key, string label, bool value)\n {\n var cb = new CheckBox\n {\n Content = label,\n IsChecked = value,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n FontSize = 10,\n Margin = new Thickness(0, 6, 0, 0)\n };\n AiProviderFieldsPanel.Children.Add(cb);\n _aiDynamicFields[key] = cb;\n }\n\n private void AiProviderCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n {\n if (_aiProviderUiSyncing || !IsLoaded || AiProviderFieldsPanel == null) return;\n\n // Save fields to the provider we're leaving before loading the new one\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n RebuildAiProviderFields();\n }\n\n private void SaveAiSettings_Click(object sender, RoutedEventArgs e) => SaveAiSettings();\n\n private void BotDisplayNameBox_LostFocus(object sender, RoutedEventArgs e)\n {\n if (BotDisplayNameBox == null) return;\n _botDisplayName = BotDisplayNameBox.Text.Trim();\n _aiSettings.BotDisplayName = _botDisplayName;\n }\n\n private static bool TryParseBbotCommand(string msg, out string userPrompt)\n {\n userPrompt = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n msg = msg.Trim();\n if (!msg.StartsWith(AiCommandToken, StringComparison.OrdinalIgnoreCase)) return false;\n if (msg.Length > AiCommandToken.Length)\n {\n char next = msg[AiCommandToken.Length];\n if (!char.IsWhiteSpace(next)) return false;\n userPrompt = msg[(AiCommandToken.Length + 1)..].Trim();\n }\n return true;\n }\n\n private static readonly Regex FirstCommandTokenRegex =\n new(@\"^(!\\S+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n private static bool TryGetFirstCommandToken(string msg, out string token)\n {\n token = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n var m = FirstCommandTokenRegex.Match(msg.Trim());\n if (!m.Success) return false;\n token = m.Groups[1].Value;\n return true;\n }\n\n private static bool CommandMatchesFirstToken(string msg, string cmd)\n {\n if (!TryGetFirstCommandToken(msg, out string token)) return false;\n return string.Equals(token, cmd, StringComparison.OrdinalIgnoreCase);\n }\n}"}, "faa913a710d08c0f0fa5a09de7d28c6b54c2c41b": {"t": 1785760683.6127439, "n": 17101, "h": "7038dfae29821791", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Windows;\nusing System.Windows.Controls;\n\nnamespace IMVUCompanion;\n\npublic partial class MainWindow\n{\n private static readonly string AiSettingsFile = UserDataPaths.GetConfigFile(\"ai_settings.json\");\n private const string AiCommandToken = \"!bbot\";\n private const string AiMaintenanceReply = \"Sorry, my conversation module is under maintenance.\";\n\n private static readonly string[] AiProviderNames =\n {\n \"Grok\", \"Claude\", \"Gemini\", \"GPT\", \"DeepSeek\", \"Mistral\", \"Qwen\", \"Llama\", \"Fireworks\"\n };\n\n private readonly Dictionary<string, FrameworkElement> _aiDynamicFields = new(StringComparer.OrdinalIgnoreCase);\n private AiSettings _aiSettings = new();\n private string _botDisplayName = \"\";\n private string? _aiFieldsBoundProvider;\n private bool _aiProviderUiSyncing;\n\n private sealed class ProviderConfig\n {\n public string ApiKey { get; set; } = \"\";\n public string Endpoint { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1024;\n public bool Enabled { get; set; }\n }\n\n private sealed class AiSettings\n {\n public string SelectedProvider { get; set; } = \"Grok\";\n public string BotDisplayName { get; set; } = \"\";\n /// <summary>Built-in paid AI name/trigger (AI Settings section) — not the same as provider keys.</summary>\n public string CompanionAiTrigger { get; set; } = \"\";\n public Dictionary<string, ProviderConfig> Providers { get; set; } =\n new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase);\n }\n\n private static ProviderConfig DefaultProviderConfig(string name) => name switch\n {\n \"Grok\" => new ProviderConfig\n {\n Endpoint = \"https://api.x.ai/v1\",\n Model = \"grok-2\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Claude\" => new ProviderConfig\n {\n Endpoint = \"https://api.anthropic.com/v1\",\n Model = \"claude-sonnet-4-20250514\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Gemini\" => new ProviderConfig\n {\n Endpoint = \"https://generativelanguage.googleapis.com/v1beta\",\n Model = \"gemini-2.0-flash\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"GPT\" => new ProviderConfig\n {\n Endpoint = \"https://api.openai.com/v1\",\n Model = \"gpt-4o\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"DeepSeek\" => new ProviderConfig\n {\n Endpoint = \"https://api.deepseek.com/v1\",\n Model = \"deepseek-chat\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Mistral\" => new ProviderConfig\n {\n Endpoint = \"https://api.mistral.ai/v1\",\n Model = \"mistral-large-latest\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Qwen\" => new ProviderConfig\n {\n Endpoint = \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n Model = \"qwen-plus\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Llama\" => new ProviderConfig\n {\n Endpoint = \"https://api.together.xyz/v1\",\n Model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Fireworks\" => new ProviderConfig\n {\n Endpoint = \"https://api.fireworks.ai/inference/v1\",\n Model = \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n _ => new ProviderConfig()\n };\n\n private void InitAiProvidersUi()\n {\n if (AiProviderCombo == null) return;\n\n AiProviderCombo.Items.Clear();\n foreach (string name in AiProviderNames)\n AiProviderCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });\n\n LoadAiSettings();\n\n _aiProviderUiSyncing = true;\n try\n {\n string selected = _aiSettings.SelectedProvider;\n for (int i = 0; i < AiProviderCombo.Items.Count; i++)\n {\n if (AiProviderCombo.Items[i] is ComboBoxItem cbi &&\n string.Equals(cbi.Tag?.ToString(), selected, StringComparison.OrdinalIgnoreCase))\n {\n AiProviderCombo.SelectedIndex = i;\n break;\n }\n }\n if (AiProviderCombo.SelectedIndex < 0 && AiProviderCombo.Items.Count > 0)\n AiProviderCombo.SelectedIndex = 0;\n }\n finally\n {\n _aiProviderUiSyncing = false;\n }\n\n if (BotDisplayNameBox != null)\n BotDisplayNameBox.Text = _aiSettings.BotDisplayName;\n\n _botDisplayName = _aiSettings.BotDisplayName?.Trim() ?? \"\";\n _aiFieldsBoundProvider = GetSelectedAiProviderName();\n RebuildAiProviderFields();\n }\n\n private void LoadAiSettings()\n {\n _aiSettings = new AiSettings();\n try\n {\n if (File.Exists(AiSettingsFile))\n {\n string json = File.ReadAllText(AiSettingsFile);\n var loaded = JsonSerializer.Deserialize<AiSettings>(json);\n if (loaded != null) _aiSettings = loaded;\n }\n }\n catch (Exception ex) { AppendLog(\"Load AI settings err: \" + ex.Message, LogCategory.Warning); }\n\n // API keys may be DPAPI-wrapped on disk; keep plaintext only in memory\n bool hadLegacyPlain = false;\n foreach (var kv in _aiSettings.Providers)\n {\n if (kv.Value == null) continue;\n string stored = kv.Value.ApiKey ?? \"\";\n if (!string.IsNullOrEmpty(stored) && !SecretProtector.IsProtected(stored))\n hadLegacyPlain = true;\n kv.Value.ApiKey = SecretProtector.Unprotect(stored);\n }\n\n foreach (string name in AiProviderNames)\n {\n if (!_aiSettings.Providers.ContainsKey(name))\n _aiSettings.Providers[name] = DefaultProviderConfig(name);\n }\n\n if (string.IsNullOrWhiteSpace(_aiSettings.SelectedProvider) ||\n !AiProviderNames.Contains(_aiSettings.SelectedProvider, StringComparer.OrdinalIgnoreCase))\n _aiSettings.SelectedProvider = \"Grok\";\n\n // One-time upgrade: rewrite file so keys are DPAPI-protected at rest\n if (hadLegacyPlain)\n {\n try { SaveAiSettingsCore(logSuccess: false); }\n catch { /* next explicit save will protect */ }\n }\n }\n\n private void SaveAiSettings() => SaveAiSettingsCore(logSuccess: true);\n\n private void SaveAiSettingsCore(bool logSuccess)\n {\n try\n {\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n if (BotDisplayNameBox != null)\n _aiSettings.BotDisplayName = BotDisplayNameBox.Text.Trim();\n _botDisplayName = _aiSettings.BotDisplayName;\n\n // Snapshot for disk: encrypt API keys (DPAPI CurrentUser); leave in-memory plaintext\n if (CompanionAiTriggerBox != null)\n _aiSettings.CompanionAiTrigger = CompanionAiTriggerBox.Text.Trim();\n\n var disk = new AiSettings\n {\n SelectedProvider = _aiSettings.SelectedProvider,\n BotDisplayName = _aiSettings.BotDisplayName,\n CompanionAiTrigger = _aiSettings.CompanionAiTrigger,\n Providers = new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase)\n };\n foreach (var kv in _aiSettings.Providers)\n {\n var src = kv.Value ?? DefaultProviderConfig(kv.Key);\n disk.Providers[kv.Key] = new ProviderConfig\n {\n ApiKey = SecretProtector.Protect(src.ApiKey),\n Endpoint = src.Endpoint,\n Model = src.Model,\n Temperature = src.Temperature,\n MaxTokens = src.MaxTokens,\n Enabled = src.Enabled\n };\n }\n\n Directory.CreateDirectory(UserDataPaths.Root);\n string json = JsonSerializer.Serialize(disk, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(AiSettingsFile, json);\n if (logSuccess)\n AppendLog(\"AI settings saved (API keys protected with Windows DPAPI).\", LogCategory.Info);\n }\n catch (Exception ex) { AppendLog(\"Save AI settings err: \" + ex.Message, LogCategory.Error); }\n }\n\n private string? GetSelectedAiProviderName()\n {\n if (AiProviderCombo?.SelectedItem is ComboBoxItem cbi)\n return cbi.Tag?.ToString() ?? cbi.Content?.ToString();\n return _aiSettings.SelectedProvider;\n }\n\n private ProviderConfig GetOrCreateProviderConfig(string name)\n {\n if (!_aiSettings.Providers.TryGetValue(name, out var cfg) || cfg == null)\n {\n cfg = DefaultProviderConfig(name);\n _aiSettings.Providers[name] = cfg;\n }\n return cfg;\n }\n\n private void FlushAiFieldsToSettings(string? providerName = null)\n {\n string? name = providerName ?? _aiFieldsBoundProvider ?? GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n var cfg = GetOrCreateProviderConfig(name);\n if (_aiDynamicFields.TryGetValue(\"ApiKey\", out var apiEl) && apiEl is PasswordBox apiPb)\n cfg.ApiKey = apiPb.Password;\n if (_aiDynamicFields.TryGetValue(\"Endpoint\", out var epEl) && epEl is TextBox epTb)\n cfg.Endpoint = epTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Model\", out var modelEl) && modelEl is TextBox modelTb)\n cfg.Model = modelTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Temperature\", out var tempEl) && tempEl is TextBox tempTb &&\n double.TryParse(tempTb.Text.Trim(), out double temp))\n cfg.Temperature = Math.Clamp(temp, 0, 2);\n if (_aiDynamicFields.TryGetValue(\"MaxTokens\", out var tokEl) && tokEl is TextBox tokTb &&\n int.TryParse(tokTb.Text.Trim(), out int tok))\n cfg.MaxTokens = Math.Max(1, tok);\n if (_aiDynamicFields.TryGetValue(\"Enabled\", out var enEl) && enEl is CheckBox enCb)\n cfg.Enabled = enCb.IsChecked == true;\n }\n\n private void RebuildAiProviderFields()\n {\n if (AiProviderFieldsPanel == null) return;\n\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n _aiSettings.SelectedProvider = name;\n _aiFieldsBoundProvider = name;\n var cfg = GetOrCreateProviderConfig(name);\n\n AiProviderFieldsPanel.Children.Clear();\n _aiDynamicFields.Clear();\n\n AiProviderFieldsPanel.Children.Add(MakeAiHint(\n name switch\n {\n \"Grok\" => \"xAI Grok — OpenAI-compatible chat completions.\",\n \"Claude\" => \"Anthropic Claude — Messages API compatible endpoint.\",\n \"Gemini\" => \"Google Gemini — API key from Google AI Studio.\",\n \"GPT\" => \"OpenAI GPT — standard OpenAI API.\",\n \"DeepSeek\" => \"DeepSeek — OpenAI-compatible API.\",\n \"Mistral\" => \"Mistral AI — La Plateforme API.\",\n \"Qwen\" => \"Alibaba Qwen — DashScope compatible mode.\",\n \"Llama\" => \"Meta Llama — via Together or compatible host.\",\n \"Fireworks\" => \"Fireworks AI — fast inference API.\",\n _ => \"Configure API credentials for this provider.\"\n }));\n\n AddAiPasswordField(\"ApiKey\", \"API Key\", cfg.ApiKey, \"Paste your API key\");\n AddAiTextField(\"Endpoint\", \"API Endpoint\", cfg.Endpoint, \"https://...\");\n AddAiTextField(\"Model\", \"Model\", cfg.Model, \"model-id\");\n AddAiTextField(\"Temperature\", \"Temperature\", cfg.Temperature.ToString(\"0.##\"), \"0.0 – 2.0\");\n AddAiTextField(\"MaxTokens\", \"Max Tokens\", cfg.MaxTokens.ToString(), \"e.g. 1024\");\n AddAiCheckField(\"Enabled\", \"Enable this provider\", cfg.Enabled);\n\n if (AiProviderStatusText != null)\n AiProviderStatusText.Text = cfg.Enabled\n ? $\"{name} enabled — AI wiring comes in a future update.\"\n : $\"{name} configured (disabled until enabled).\";\n }\n\n private static TextBlock MakeAiHint(string text) => new()\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x70, 0x70, 0x90)),\n TextWrapping = TextWrapping.Wrap,\n Margin = new Thickness(0, 0, 0, 6)\n };\n\n private void AddAiLabel(string text)\n {\n AiProviderFieldsPanel.Children.Add(new TextBlock\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xA0, 0xA0, 0xC8)),\n Margin = new Thickness(0, 4, 0, 2)\n });\n }\n\n private void AddAiTextField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var tb = new TextBox\n {\n Text = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(tb);\n _aiDynamicFields[key] = tb;\n }\n\n private void AddAiPasswordField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var pb = new PasswordBox\n {\n Password = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(pb);\n _aiDynamicFields[key] = pb;\n }\n\n private void AddAiCheckField(string key, string label, bool value)\n {\n var cb = new CheckBox\n {\n Content = label,\n IsChecked = value,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n FontSize = 10,\n Margin = new Thickness(0, 6, 0, 0)\n };\n AiProviderFieldsPanel.Children.Add(cb);\n _aiDynamicFields[key] = cb;\n }\n\n private void AiProviderCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n {\n if (_aiProviderUiSyncing || !IsLoaded || AiProviderFieldsPanel == null) return;\n\n // Save fields to the provider we're leaving before loading the new one\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n RebuildAiProviderFields();\n }\n\n private void SaveAiSettings_Click(object sender, RoutedEventArgs e) => SaveAiSettings();\n\n private void BotDisplayNameBox_LostFocus(object sender, RoutedEventArgs e)\n {\n if (BotDisplayNameBox == null) return;\n _botDisplayName = BotDisplayNameBox.Text.Trim();\n _aiSettings.BotDisplayName = _botDisplayName;\n }\n\n private static bool TryParseBbotCommand(string msg, out string userPrompt)\n {\n userPrompt = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n msg = msg.Trim();\n if (!msg.StartsWith(AiCommandToken, StringComparison.OrdinalIgnoreCase)) return false;\n if (msg.Length > AiCommandToken.Length)\n {\n char next = msg[AiCommandToken.Length];\n if (!char.IsWhiteSpace(next)) return false;\n userPrompt = msg[(AiCommandToken.Length + 1)..].Trim();\n }\n return true;\n }\n\n private static readonly Regex FirstCommandTokenRegex =\n new(@\"^(!\\S+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n private static bool TryGetFirstCommandToken(string msg, out string token)\n {\n token = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n var m = FirstCommandTokenRegex.Match(msg.Trim());\n if (!m.Success) return false;\n token = m.Groups[1].Value;\n return true;\n }\n\n private static bool CommandMatchesFirstToken(string msg, string cmd)\n {\n if (!TryGetFirstCommandToken(msg, out string token)) return false;\n return string.Equals(token, cmd, StringComparison.OrdinalIgnoreCase);\n }\n}"}, "0d383a08fd0324753b1e69eaa45377b26bd669cd": {"t": 1785760685.0707352, "n": 16723, "h": "a9ad35e9b657a55c", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Windows;\nusing System.Windows.Controls;\n\nnamespace IMVUCompanion;\n\npublic partial class MainWindow\n{\n private static readonly string AiSettingsFile = UserDataPaths.GetConfigFile(\"ai_settings.json\");\n private const string AiCommandToken = \"!bbot\";\n private const string AiMaintenanceReply = \"Sorry, my conversation module is under maintenance.\";\n\n private static readonly string[] AiProviderNames =\n {\n \"Grok\", \"Claude\", \"Gemini\", \"GPT\", \"DeepSeek\", \"Mistral\", \"Qwen\", \"Llama\", \"Fireworks\"\n };\n\n private readonly Dictionary<string, FrameworkElement> _aiDynamicFields = new(StringComparer.OrdinalIgnoreCase);\n private AiSettings _aiSettings = new();\n private string _botDisplayName = \"\";\n private string? _aiFieldsBoundProvider;\n private bool _aiProviderUiSyncing;\n\n private sealed class ProviderConfig\n {\n public string ApiKey { get; set; } = \"\";\n public string Endpoint { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1024;\n public bool Enabled { get; set; }\n }\n\n private sealed class AiSettings\n {\n public string SelectedProvider { get; set; } = \"Grok\";\n public string BotDisplayName { get; set; } = \"\";\n public Dictionary<string, ProviderConfig> Providers { get; set; } =\n new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase);\n }\n\n private static ProviderConfig DefaultProviderConfig(string name) => name switch\n {\n \"Grok\" => new ProviderConfig\n {\n Endpoint = \"https://api.x.ai/v1\",\n Model = \"grok-2\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Claude\" => new ProviderConfig\n {\n Endpoint = \"https://api.anthropic.com/v1\",\n Model = \"claude-sonnet-4-20250514\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Gemini\" => new ProviderConfig\n {\n Endpoint = \"https://generativelanguage.googleapis.com/v1beta\",\n Model = \"gemini-2.0-flash\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"GPT\" => new ProviderConfig\n {\n Endpoint = \"https://api.openai.com/v1\",\n Model = \"gpt-4o\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"DeepSeek\" => new ProviderConfig\n {\n Endpoint = \"https://api.deepseek.com/v1\",\n Model = \"deepseek-chat\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Mistral\" => new ProviderConfig\n {\n Endpoint = \"https://api.mistral.ai/v1\",\n Model = \"mistral-large-latest\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Qwen\" => new ProviderConfig\n {\n Endpoint = \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n Model = \"qwen-plus\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Llama\" => new ProviderConfig\n {\n Endpoint = \"https://api.together.xyz/v1\",\n Model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Fireworks\" => new ProviderConfig\n {\n Endpoint = \"https://api.fireworks.ai/inference/v1\",\n Model = \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n _ => new ProviderConfig()\n };\n\n private void InitAiProvidersUi()\n {\n if (AiProviderCombo == null) return;\n\n AiProviderCombo.Items.Clear();\n foreach (string name in AiProviderNames)\n AiProviderCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });\n\n LoadAiSettings();\n\n _aiProviderUiSyncing = true;\n try\n {\n string selected = _aiSettings.SelectedProvider;\n for (int i = 0; i < AiProviderCombo.Items.Count; i++)\n {\n if (AiProviderCombo.Items[i] is ComboBoxItem cbi &&\n string.Equals(cbi.Tag?.ToString(), selected, StringComparison.OrdinalIgnoreCase))\n {\n AiProviderCombo.SelectedIndex = i;\n break;\n }\n }\n if (AiProviderCombo.SelectedIndex < 0 && AiProviderCombo.Items.Count > 0)\n AiProviderCombo.SelectedIndex = 0;\n }\n finally\n {\n _aiProviderUiSyncing = false;\n }\n\n if (BotDisplayNameBox != null)\n BotDisplayNameBox.Text = _aiSettings.BotDisplayName;\n\n _botDisplayName = _aiSettings.BotDisplayName?.Trim() ?? \"\";\n _aiFieldsBoundProvider = GetSelectedAiProviderName();\n RebuildAiProviderFields();\n }\n\n private void LoadAiSettings()\n {\n _aiSettings = new AiSettings();\n try\n {\n if (File.Exists(AiSettingsFile))\n {\n string json = File.ReadAllText(AiSettingsFile);\n var loaded = JsonSerializer.Deserialize<AiSettings>(json);\n if (loaded != null) _aiSettings = loaded;\n }\n }\n catch (Exception ex) { AppendLog(\"Load AI settings err: \" + ex.Message, LogCategory.Warning); }\n\n // API keys may be DPAPI-wrapped on disk; keep plaintext only in memory\n bool hadLegacyPlain = false;\n foreach (var kv in _aiSettings.Providers)\n {\n if (kv.Value == null) continue;\n string stored = kv.Value.ApiKey ?? \"\";\n if (!string.IsNullOrEmpty(stored) && !SecretProtector.IsProtected(stored))\n hadLegacyPlain = true;\n kv.Value.ApiKey = SecretProtector.Unprotect(stored);\n }\n\n foreach (string name in AiProviderNames)\n {\n if (!_aiSettings.Providers.ContainsKey(name))\n _aiSettings.Providers[name] = DefaultProviderConfig(name);\n }\n\n if (string.IsNullOrWhiteSpace(_aiSettings.SelectedProvider) ||\n !AiProviderNames.Contains(_aiSettings.SelectedProvider, StringComparer.OrdinalIgnoreCase))\n _aiSettings.SelectedProvider = \"Grok\";\n\n // One-time upgrade: rewrite file so keys are DPAPI-protected at rest\n if (hadLegacyPlain)\n {\n try { SaveAiSettingsCore(logSuccess: false); }\n catch { /* next explicit save will protect */ }\n }\n }\n\n private void SaveAiSettings() => SaveAiSettingsCore(logSuccess: true);\n\n private void SaveAiSettingsCore(bool logSuccess)\n {\n try\n {\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n if (BotDisplayNameBox != null)\n _aiSettings.BotDisplayName = BotDisplayNameBox.Text.Trim();\n _botDisplayName = _aiSettings.BotDisplayName;\n\n // Snapshot for disk: encrypt API keys (DPAPI CurrentUser); leave in-memory plaintext\n var disk = new AiSettings\n {\n SelectedProvider = _aiSettings.SelectedProvider,\n BotDisplayName = _aiSettings.BotDisplayName,\n Providers = new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase)\n };\n foreach (var kv in _aiSettings.Providers)\n {\n var src = kv.Value ?? DefaultProviderConfig(kv.Key);\n disk.Providers[kv.Key] = new ProviderConfig\n {\n ApiKey = SecretProtector.Protect(src.ApiKey),\n Endpoint = src.Endpoint,\n Model = src.Model,\n Temperature = src.Temperature,\n MaxTokens = src.MaxTokens,\n Enabled = src.Enabled\n };\n }\n\n Directory.CreateDirectory(UserDataPaths.Root);\n string json = JsonSerializer.Serialize(disk, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(AiSettingsFile, json);\n if (logSuccess)\n AppendLog(\"AI settings saved (API keys protected with Windows DPAPI).\", LogCategory.Info);\n }\n catch (Exception ex) { AppendLog(\"Save AI settings err: \" + ex.Message, LogCategory.Error); }\n }\n\n private string? GetSelectedAiProviderName()\n {\n if (AiProviderCombo?.SelectedItem is ComboBoxItem cbi)\n return cbi.Tag?.ToString() ?? cbi.Content?.ToString();\n return _aiSettings.SelectedProvider;\n }\n\n private ProviderConfig GetOrCreateProviderConfig(string name)\n {\n if (!_aiSettings.Providers.TryGetValue(name, out var cfg) || cfg == null)\n {\n cfg = DefaultProviderConfig(name);\n _aiSettings.Providers[name] = cfg;\n }\n return cfg;\n }\n\n private void FlushAiFieldsToSettings(string? providerName = null)\n {\n string? name = providerName ?? _aiFieldsBoundProvider ?? GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n var cfg = GetOrCreateProviderConfig(name);\n if (_aiDynamicFields.TryGetValue(\"ApiKey\", out var apiEl) && apiEl is PasswordBox apiPb)\n cfg.ApiKey = apiPb.Password;\n if (_aiDynamicFields.TryGetValue(\"Endpoint\", out var epEl) && epEl is TextBox epTb)\n cfg.Endpoint = epTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Model\", out var modelEl) && modelEl is TextBox modelTb)\n cfg.Model = modelTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Temperature\", out var tempEl) && tempEl is TextBox tempTb &&\n double.TryParse(tempTb.Text.Trim(), out double temp))\n cfg.Temperature = Math.Clamp(temp, 0, 2);\n if (_aiDynamicFields.TryGetValue(\"MaxTokens\", out var tokEl) && tokEl is TextBox tokTb &&\n int.TryParse(tokTb.Text.Trim(), out int tok))\n cfg.MaxTokens = Math.Max(1, tok);\n if (_aiDynamicFields.TryGetValue(\"Enabled\", out var enEl) && enEl is CheckBox enCb)\n cfg.Enabled = enCb.IsChecked == true;\n }\n\n private void RebuildAiProviderFields()\n {\n if (AiProviderFieldsPanel == null) return;\n\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n _aiSettings.SelectedProvider = name;\n _aiFieldsBoundProvider = name;\n var cfg = GetOrCreateProviderConfig(name);\n\n AiProviderFieldsPanel.Children.Clear();\n _aiDynamicFields.Clear();\n\n AiProviderFieldsPanel.Children.Add(MakeAiHint(\n name switch\n {\n \"Grok\" => \"xAI Grok — OpenAI-compatible chat completions.\",\n \"Claude\" => \"Anthropic Claude — Messages API compatible endpoint.\",\n \"Gemini\" => \"Google Gemini — API key from Google AI Studio.\",\n \"GPT\" => \"OpenAI GPT — standard OpenAI API.\",\n \"DeepSeek\" => \"DeepSeek — OpenAI-compatible API.\",\n \"Mistral\" => \"Mistral AI — La Plateforme API.\",\n \"Qwen\" => \"Alibaba Qwen — DashScope compatible mode.\",\n \"Llama\" => \"Meta Llama — via Together or compatible host.\",\n \"Fireworks\" => \"Fireworks AI — fast inference API.\",\n _ => \"Configure API credentials for this provider.\"\n }));\n\n AddAiPasswordField(\"ApiKey\", \"API Key\", cfg.ApiKey, \"Paste your API key\");\n AddAiTextField(\"Endpoint\", \"API Endpoint\", cfg.Endpoint, \"https://...\");\n AddAiTextField(\"Model\", \"Model\", cfg.Model, \"model-id\");\n AddAiTextField(\"Temperature\", \"Temperature\", cfg.Temperature.ToString(\"0.##\"), \"0.0 – 2.0\");\n AddAiTextField(\"MaxTokens\", \"Max Tokens\", cfg.MaxTokens.ToString(), \"e.g. 1024\");\n AddAiCheckField(\"Enabled\", \"Enable this provider\", cfg.Enabled);\n\n if (AiProviderStatusText != null)\n AiProviderStatusText.Text = cfg.Enabled\n ? $\"{name} enabled — AI wiring comes in a future update.\"\n : $\"{name} configured (disabled until enabled).\";\n }\n\n private static TextBlock MakeAiHint(string text) => new()\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x70, 0x70, 0x90)),\n TextWrapping = TextWrapping.Wrap,\n Margin = new Thickness(0, 0, 0, 6)\n };\n\n private void AddAiLabel(string text)\n {\n AiProviderFieldsPanel.Children.Add(new TextBlock\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xA0, 0xA0, 0xC8)),\n Margin = new Thickness(0, 4, 0, 2)\n });\n }\n\n private void AddAiTextField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var tb = new TextBox\n {\n Text = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(tb);\n _aiDynamicFields[key] = tb;\n }\n\n private void AddAiPasswordField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var pb = new PasswordBox\n {\n Password = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(pb);\n _aiDynamicFields[key] = pb;\n }\n\n private void AddAiCheckField(string key, string label, bool value)\n {\n var cb = new CheckBox\n {\n Content = label,\n IsChecked = value,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n FontSize = 10,\n Margin = new Thickness(0, 6, 0, 0)\n };\n AiProviderFieldsPanel.Children.Add(cb);\n _aiDynamicFields[key] = cb;\n }\n\n private void AiProviderCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n {\n if (_aiProviderUiSyncing || !IsLoaded || AiProviderFieldsPanel == null) return;\n\n // Save fields to the provider we're leaving before loading the new one\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n RebuildAiProviderFields();\n }\n\n private void SaveAiSettings_Click(object sender, RoutedEventArgs e) => SaveAiSettings();\n\n private void BotDisplayNameBox_LostFocus(object sender, RoutedEventArgs e)\n {\n if (BotDisplayNameBox == null) return;\n _botDisplayName = BotDisplayNameBox.Text.Trim();\n _aiSettings.BotDisplayName = _botDisplayName;\n }\n\n private static bool TryParseBbotCommand(string msg, out string userPrompt)\n {\n userPrompt = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n msg = msg.Trim();\n if (!msg.StartsWith(AiCommandToken, StringComparison.OrdinalIgnoreCase)) return false;\n if (msg.Length > AiCommandToken.Length)\n {\n char next = msg[AiCommandToken.Length];\n if (!char.IsWhiteSpace(next)) return false;\n userPrompt = msg[(AiCommandToken.Length + 1)..].Trim();\n }\n return true;\n }\n\n private static readonly Regex FirstCommandTokenRegex =\n new(@\"^(!\\S+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n private static bool TryGetFirstCommandToken(string msg, out string token)\n {\n token = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n var m = FirstCommandTokenRegex.Match(msg.Trim());\n if (!m.Success) return false;\n token = m.Groups[1].Value;\n return true;\n }\n\n private static bool CommandMatchesFirstToken(string msg, string cmd)\n {\n if (!TryGetFirstCommandToken(msg, out string token)) return false;\n return string.Equals(token, cmd, StringComparison.OrdinalIgnoreCase);\n }\n}"}, "f7d34088d8a102c0dacddbd37f7170848fc9a16e": {"t": 1785760686.4294477, "n": 14932, "h": "6623e2c5cb2fed6c", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Windows;\nusing System.Windows.Controls;\n\nnamespace IMVUCompanion;\n\npublic partial class MainWindow\n{\n private static readonly string AiSettingsFile = UserDataPaths.GetConfigFile(\"ai_settings.json\");\n private const string AiCommandToken = \"!bbot\";\n private const string AiMaintenanceReply = \"Sorry, my conversation module is under maintenance.\";\n\n private static readonly string[] AiProviderNames =\n {\n \"Grok\", \"Claude\", \"Gemini\", \"GPT\", \"DeepSeek\", \"Mistral\", \"Qwen\", \"Llama\", \"Fireworks\"\n };\n\n private readonly Dictionary<string, FrameworkElement> _aiDynamicFields = new(StringComparer.OrdinalIgnoreCase);\n private AiSettings _aiSettings = new();\n private string _botDisplayName = \"\";\n private string? _aiFieldsBoundProvider;\n private bool _aiProviderUiSyncing;\n\n private sealed class ProviderConfig\n {\n public string ApiKey { get; set; } = \"\";\n public string Endpoint { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1024;\n public bool Enabled { get; set; }\n }\n\n private sealed class AiSettings\n {\n public string SelectedProvider { get; set; } = \"Grok\";\n public string BotDisplayName { get; set; } = \"\";\n public Dictionary<string, ProviderConfig> Providers { get; set; } =\n new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase);\n }\n\n private static ProviderConfig DefaultProviderConfig(string name) => name switch\n {\n \"Grok\" => new ProviderConfig\n {\n Endpoint = \"https://api.x.ai/v1\",\n Model = \"grok-2\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Claude\" => new ProviderConfig\n {\n Endpoint = \"https://api.anthropic.com/v1\",\n Model = \"claude-sonnet-4-20250514\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Gemini\" => new ProviderConfig\n {\n Endpoint = \"https://generativelanguage.googleapis.com/v1beta\",\n Model = \"gemini-2.0-flash\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"GPT\" => new ProviderConfig\n {\n Endpoint = \"https://api.openai.com/v1\",\n Model = \"gpt-4o\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"DeepSeek\" => new ProviderConfig\n {\n Endpoint = \"https://api.deepseek.com/v1\",\n Model = \"deepseek-chat\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Mistral\" => new ProviderConfig\n {\n Endpoint = \"https://api.mistral.ai/v1\",\n Model = \"mistral-large-latest\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Qwen\" => new ProviderConfig\n {\n Endpoint = \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n Model = \"qwen-plus\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Llama\" => new ProviderConfig\n {\n Endpoint = \"https://api.together.xyz/v1\",\n Model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Fireworks\" => new ProviderConfig\n {\n Endpoint = \"https://api.fireworks.ai/inference/v1\",\n Model = \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n _ => new ProviderConfig()\n };\n\n private void InitAiProvidersUi()\n {\n if (AiProviderCombo == null) return;\n\n AiProviderCombo.Items.Clear();\n foreach (string name in AiProviderNames)\n AiProviderCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });\n\n LoadAiSettings();\n\n _aiProviderUiSyncing = true;\n try\n {\n string selected = _aiSettings.SelectedProvider;\n for (int i = 0; i < AiProviderCombo.Items.Count; i++)\n {\n if (AiProviderCombo.Items[i] is ComboBoxItem cbi &&\n string.Equals(cbi.Tag?.ToString(), selected, StringComparison.OrdinalIgnoreCase))\n {\n AiProviderCombo.SelectedIndex = i;\n break;\n }\n }\n if (AiProviderCombo.SelectedIndex < 0 && AiProviderCombo.Items.Count > 0)\n AiProviderCombo.SelectedIndex = 0;\n }\n finally\n {\n _aiProviderUiSyncing = false;\n }\n\n if (BotDisplayNameBox != null)\n BotDisplayNameBox.Text = _aiSettings.BotDisplayName;\n\n _botDisplayName = _aiSettings.BotDisplayName?.Trim() ?? \"\";\n _aiFieldsBoundProvider = GetSelectedAiProviderName();\n RebuildAiProviderFields();\n }\n\n private void LoadAiSettings()\n {\n _aiSettings = new AiSettings();\n try\n {\n if (File.Exists(AiSettingsFile))\n {\n string json = File.ReadAllText(AiSettingsFile);\n var loaded = JsonSerializer.Deserialize<AiSettings>(json);\n if (loaded != null) _aiSettings = loaded;\n }\n }\n catch (Exception ex) { AppendLog(\"Load AI settings err: \" + ex.Message, LogCategory.Warning); }\n\n foreach (string name in AiProviderNames)\n {\n if (!_aiSettings.Providers.ContainsKey(name))\n _aiSettings.Providers[name] = DefaultProviderConfig(name);\n }\n\n if (string.IsNullOrWhiteSpace(_aiSettings.SelectedProvider) ||\n !AiProviderNames.Contains(_aiSettings.SelectedProvider, StringComparer.OrdinalIgnoreCase))\n _aiSettings.SelectedProvider = \"Grok\";\n }\n\n private void SaveAiSettings()\n {\n try\n {\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n if (BotDisplayNameBox != null)\n _aiSettings.BotDisplayName = BotDisplayNameBox.Text.Trim();\n _botDisplayName = _aiSettings.BotDisplayName;\n\n Directory.CreateDirectory(UserDataPaths.Root);\n string json = JsonSerializer.Serialize(_aiSettings, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(AiSettingsFile, json);\n AppendLog(\"AI settings saved to ai_settings.json.\", LogCategory.Info);\n }\n catch (Exception ex) { AppendLog(\"Save AI settings err: \" + ex.Message, LogCategory.Error); }\n }\n\n private string? GetSelectedAiProviderName()\n {\n if (AiProviderCombo?.SelectedItem is ComboBoxItem cbi)\n return cbi.Tag?.ToString() ?? cbi.Content?.ToString();\n return _aiSettings.SelectedProvider;\n }\n\n private ProviderConfig GetOrCreateProviderConfig(string name)\n {\n if (!_aiSettings.Providers.TryGetValue(name, out var cfg) || cfg == null)\n {\n cfg = DefaultProviderConfig(name);\n _aiSettings.Providers[name] = cfg;\n }\n return cfg;\n }\n\n private void FlushAiFieldsToSettings(string? providerName = null)\n {\n string? name = providerName ?? _aiFieldsBoundProvider ?? GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n var cfg = GetOrCreateProviderConfig(name);\n if (_aiDynamicFields.TryGetValue(\"ApiKey\", out var apiEl) && apiEl is PasswordBox apiPb)\n cfg.ApiKey = apiPb.Password;\n if (_aiDynamicFields.TryGetValue(\"Endpoint\", out var epEl) && epEl is TextBox epTb)\n cfg.Endpoint = epTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Model\", out var modelEl) && modelEl is TextBox modelTb)\n cfg.Model = modelTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Temperature\", out var tempEl) && tempEl is TextBox tempTb &&\n double.TryParse(tempTb.Text.Trim(), out double temp))\n cfg.Temperature = Math.Clamp(temp, 0, 2);\n if (_aiDynamicFields.TryGetValue(\"MaxTokens\", out var tokEl) && tokEl is TextBox tokTb &&\n int.TryParse(tokTb.Text.Trim(), out int tok))\n cfg.MaxTokens = Math.Max(1, tok);\n if (_aiDynamicFields.TryGetValue(\"Enabled\", out var enEl) && enEl is CheckBox enCb)\n cfg.Enabled = enCb.IsChecked == true;\n }\n\n private void RebuildAiProviderFields()\n {\n if (AiProviderFieldsPanel == null) return;\n\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n _aiSettings.SelectedProvider = name;\n _aiFieldsBoundProvider = name;\n var cfg = GetOrCreateProviderConfig(name);\n\n AiProviderFieldsPanel.Children.Clear();\n _aiDynamicFields.Clear();\n\n AiProviderFieldsPanel.Children.Add(MakeAiHint(\n name switch\n {\n \"Grok\" => \"xAI Grok — OpenAI-compatible chat completions.\",\n \"Claude\" => \"Anthropic Claude — Messages API compatible endpoint.\",\n \"Gemini\" => \"Google Gemini — API key from Google AI Studio.\",\n \"GPT\" => \"OpenAI GPT — standard OpenAI API.\",\n \"DeepSeek\" => \"DeepSeek — OpenAI-compatible API.\",\n \"Mistral\" => \"Mistral AI — La Plateforme API.\",\n \"Qwen\" => \"Alibaba Qwen — DashScope compatible mode.\",\n \"Llama\" => \"Meta Llama — via Together or compatible host.\",\n \"Fireworks\" => \"Fireworks AI — fast inference API.\",\n _ => \"Configure API credentials for this provider.\"\n }));\n\n AddAiPasswordField(\"ApiKey\", \"API Key\", cfg.ApiKey, \"Paste your API key\");\n AddAiTextField(\"Endpoint\", \"API Endpoint\", cfg.Endpoint, \"https://...\");\n AddAiTextField(\"Model\", \"Model\", cfg.Model, \"model-id\");\n AddAiTextField(\"Temperature\", \"Temperature\", cfg.Temperature.ToString(\"0.##\"), \"0.0 – 2.0\");\n AddAiTextField(\"MaxTokens\", \"Max Tokens\", cfg.MaxTokens.ToString(), \"e.g. 1024\");\n AddAiCheckField(\"Enabled\", \"Enable this provider\", cfg.Enabled);\n\n if (AiProviderStatusText != null)\n AiProviderStatusText.Text = cfg.Enabled\n ? $\"{name} enabled — AI wiring comes in a future update.\"\n : $\"{name} configured (disabled until enabled).\";\n }\n\n private static TextBlock MakeAiHint(string text) => new()\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x70, 0x70, 0x90)),\n TextWrapping = TextWrapping.Wrap,\n Margin = new Thickness(0, 0, 0, 6)\n };\n\n private void AddAiLabel(string text)\n {\n AiProviderFieldsPanel.Children.Add(new TextBlock\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xA0, 0xA0, 0xC8)),\n Margin = new Thickness(0, 4, 0, 2)\n });\n }\n\n private void AddAiTextField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var tb = new TextBox\n {\n Text = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(tb);\n _aiDynamicFields[key] = tb;\n }\n\n private void AddAiPasswordField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var pb = new PasswordBox\n {\n Password = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(pb);\n _aiDynamicFields[key] = pb;\n }\n\n private void AddAiCheckField(string key, string label, bool value)\n {\n var cb = new CheckBox\n {\n Content = label,\n IsChecked = value,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n FontSize = 10,\n Margin = new Thickness(0, 6, 0, 0)\n };\n AiProviderFieldsPanel.Children.Add(cb);\n _aiDynamicFields[key] = cb;\n }\n\n private void AiProviderCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n {\n if (_aiProviderUiSyncing || !IsLoaded || AiProviderFieldsPanel == null) return;\n\n // Save fields to the provider we're leaving before loading the new one\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n RebuildAiProviderFields();\n }\n\n private void SaveAiSettings_Click(object sender, RoutedEventArgs e) => SaveAiSettings();\n\n private void BotDisplayNameBox_LostFocus(object sender, RoutedEventArgs e)\n {\n if (BotDisplayNameBox == null) return;\n _botDisplayName = BotDisplayNameBox.Text.Trim();\n _aiSettings.BotDisplayName = _botDisplayName;\n }\n\n private static bool TryParseBbotCommand(string msg, out string userPrompt)\n {\n userPrompt = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n msg = msg.Trim();\n if (!msg.StartsWith(AiCommandToken, StringComparison.OrdinalIgnoreCase)) return false;\n if (msg.Length > AiCommandToken.Length)\n {\n char next = msg[AiCommandToken.Length];\n if (!char.IsWhiteSpace(next)) return false;\n userPrompt = msg[(AiCommandToken.Length + 1)..].Trim();\n }\n return true;\n }\n\n private static readonly Regex FirstCommandTokenRegex =\n new(@\"^(!\\S+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n private static bool TryGetFirstCommandToken(string msg, out string token)\n {\n token = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n var m = FirstCommandTokenRegex.Match(msg.Trim());\n if (!m.Success) return false;\n token = m.Groups[1].Value;\n return true;\n }\n\n private static bool CommandMatchesFirstToken(string msg, string cmd)\n {\n if (!TryGetFirstCommandToken(msg, out string token)) return false;\n return string.Equals(token, cmd, StringComparison.OrdinalIgnoreCase);\n }\n}"}, "e16c7824797421529003e3c7365d46ac6a3f1cf3": {"t": 1785760689.6815717, "n": 14834, "h": "b33f62de2afca9b8", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Windows;\nusing System.Windows.Controls;\n\nnamespace IMVUCompanion;\n\npublic partial class MainWindow\n{\n private const string AiSettingsFile = \"ai_settings.json\";\n private const string AiCommandToken = \"!bbot\";\n private const string AiMaintenanceReply = \"Sorry, my conversation module is under maintenance.\";\n\n private static readonly string[] AiProviderNames =\n {\n \"Grok\", \"Claude\", \"Gemini\", \"GPT\", \"DeepSeek\", \"Mistral\", \"Qwen\", \"Llama\", \"Fireworks\"\n };\n\n private readonly Dictionary<string, FrameworkElement> _aiDynamicFields = new(StringComparer.OrdinalIgnoreCase);\n private AiSettings _aiSettings = new();\n private string _botDisplayName = \"\";\n private string? _aiFieldsBoundProvider;\n private bool _aiProviderUiSyncing;\n\n private sealed class ProviderConfig\n {\n public string ApiKey { get; set; } = \"\";\n public string Endpoint { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1024;\n public bool Enabled { get; set; }\n }\n\n private sealed class AiSettings\n {\n public string SelectedProvider { get; set; } = \"Grok\";\n public string BotDisplayName { get; set; } = \"\";\n public Dictionary<string, ProviderConfig> Providers { get; set; } =\n new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase);\n }\n\n private static ProviderConfig DefaultProviderConfig(string name) => name switch\n {\n \"Grok\" => new ProviderConfig\n {\n Endpoint = \"https://api.x.ai/v1\",\n Model = \"grok-2\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Claude\" => new ProviderConfig\n {\n Endpoint = \"https://api.anthropic.com/v1\",\n Model = \"claude-sonnet-4-20250514\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Gemini\" => new ProviderConfig\n {\n Endpoint = \"https://generativelanguage.googleapis.com/v1beta\",\n Model = \"gemini-2.0-flash\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"GPT\" => new ProviderConfig\n {\n Endpoint = \"https://api.openai.com/v1\",\n Model = \"gpt-4o\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"DeepSeek\" => new ProviderConfig\n {\n Endpoint = \"https://api.deepseek.com/v1\",\n Model = \"deepseek-chat\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Mistral\" => new ProviderConfig\n {\n Endpoint = \"https://api.mistral.ai/v1\",\n Model = \"mistral-large-latest\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Qwen\" => new ProviderConfig\n {\n Endpoint = \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n Model = \"qwen-plus\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Llama\" => new ProviderConfig\n {\n Endpoint = \"https://api.together.xyz/v1\",\n Model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Fireworks\" => new ProviderConfig\n {\n Endpoint = \"https://api.fireworks.ai/inference/v1\",\n Model = \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n _ => new ProviderConfig()\n };\n\n private void InitAiProvidersUi()\n {\n if (AiProviderCombo == null) return;\n\n AiProviderCombo.Items.Clear();\n foreach (string name in AiProviderNames)\n AiProviderCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });\n\n LoadAiSettings();\n\n _aiProviderUiSyncing = true;\n try\n {\n string selected = _aiSettings.SelectedProvider;\n for (int i = 0; i < AiProviderCombo.Items.Count; i++)\n {\n if (AiProviderCombo.Items[i] is ComboBoxItem cbi &&\n string.Equals(cbi.Tag?.ToString(), selected, StringComparison.OrdinalIgnoreCase))\n {\n AiProviderCombo.SelectedIndex = i;\n break;\n }\n }\n if (AiProviderCombo.SelectedIndex < 0 && AiProviderCombo.Items.Count > 0)\n AiProviderCombo.SelectedIndex = 0;\n }\n finally\n {\n _aiProviderUiSyncing = false;\n }\n\n if (BotDisplayNameBox != null)\n BotDisplayNameBox.Text = _aiSettings.BotDisplayName;\n\n _botDisplayName = _aiSettings.BotDisplayName?.Trim() ?? \"\";\n _aiFieldsBoundProvider = GetSelectedAiProviderName();\n RebuildAiProviderFields();\n }\n\n private void LoadAiSettings()\n {\n _aiSettings = new AiSettings();\n try\n {\n if (File.Exists(AiSettingsFile))\n {\n string json = File.ReadAllText(AiSettingsFile);\n var loaded = JsonSerializer.Deserialize<AiSettings>(json);\n if (loaded != null) _aiSettings = loaded;\n }\n }\n catch (Exception ex) { AppendLog(\"Load AI settings err: \" + ex.Message, LogCategory.Warning); }\n\n foreach (string name in AiProviderNames)\n {\n if (!_aiSettings.Providers.ContainsKey(name))\n _aiSettings.Providers[name] = DefaultProviderConfig(name);\n }\n\n if (string.IsNullOrWhiteSpace(_aiSettings.SelectedProvider) ||\n !AiProviderNames.Contains(_aiSettings.SelectedProvider, StringComparer.OrdinalIgnoreCase))\n _aiSettings.SelectedProvider = \"Grok\";\n }\n\n private void SaveAiSettings()\n {\n try\n {\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n if (BotDisplayNameBox != null)\n _aiSettings.BotDisplayName = BotDisplayNameBox.Text.Trim();\n _botDisplayName = _aiSettings.BotDisplayName;\n\n string json = JsonSerializer.Serialize(_aiSettings, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(AiSettingsFile, json);\n AppendLog(\"AI settings saved to ai_settings.json.\", LogCategory.Info);\n }\n catch (Exception ex) { AppendLog(\"Save AI settings err: \" + ex.Message, LogCategory.Error); }\n }\n\n private string? GetSelectedAiProviderName()\n {\n if (AiProviderCombo?.SelectedItem is ComboBoxItem cbi)\n return cbi.Tag?.ToString() ?? cbi.Content?.ToString();\n return _aiSettings.SelectedProvider;\n }\n\n private ProviderConfig GetOrCreateProviderConfig(string name)\n {\n if (!_aiSettings.Providers.TryGetValue(name, out var cfg) || cfg == null)\n {\n cfg = DefaultProviderConfig(name);\n _aiSettings.Providers[name] = cfg;\n }\n return cfg;\n }\n\n private void FlushAiFieldsToSettings(string? providerName = null)\n {\n string? name = providerName ?? _aiFieldsBoundProvider ?? GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n var cfg = GetOrCreateProviderConfig(name);\n if (_aiDynamicFields.TryGetValue(\"ApiKey\", out var apiEl) && apiEl is PasswordBox apiPb)\n cfg.ApiKey = apiPb.Password;\n if (_aiDynamicFields.TryGetValue(\"Endpoint\", out var epEl) && epEl is TextBox epTb)\n cfg.Endpoint = epTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Model\", out var modelEl) && modelEl is TextBox modelTb)\n cfg.Model = modelTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Temperature\", out var tempEl) && tempEl is TextBox tempTb &&\n double.TryParse(tempTb.Text.Trim(), out double temp))\n cfg.Temperature = Math.Clamp(temp, 0, 2);\n if (_aiDynamicFields.TryGetValue(\"MaxTokens\", out var tokEl) && tokEl is TextBox tokTb &&\n int.TryParse(tokTb.Text.Trim(), out int tok))\n cfg.MaxTokens = Math.Max(1, tok);\n if (_aiDynamicFields.TryGetValue(\"Enabled\", out var enEl) && enEl is CheckBox enCb)\n cfg.Enabled = enCb.IsChecked == true;\n }\n\n private void RebuildAiProviderFields()\n {\n if (AiProviderFieldsPanel == null) return;\n\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n _aiSettings.SelectedProvider = name;\n _aiFieldsBoundProvider = name;\n var cfg = GetOrCreateProviderConfig(name);\n\n AiProviderFieldsPanel.Children.Clear();\n _aiDynamicFields.Clear();\n\n AiProviderFieldsPanel.Children.Add(MakeAiHint(\n name switch\n {\n \"Grok\" => \"xAI Grok — OpenAI-compatible chat completions.\",\n \"Claude\" => \"Anthropic Claude — Messages API compatible endpoint.\",\n \"Gemini\" => \"Google Gemini — API key from Google AI Studio.\",\n \"GPT\" => \"OpenAI GPT — standard OpenAI API.\",\n \"DeepSeek\" => \"DeepSeek — OpenAI-compatible API.\",\n \"Mistral\" => \"Mistral AI — La Plateforme API.\",\n \"Qwen\" => \"Alibaba Qwen — DashScope compatible mode.\",\n \"Llama\" => \"Meta Llama — via Together or compatible host.\",\n \"Fireworks\" => \"Fireworks AI — fast inference API.\",\n _ => \"Configure API credentials for this provider.\"\n }));\n\n AddAiPasswordField(\"ApiKey\", \"API Key\", cfg.ApiKey, \"Paste your API key\");\n AddAiTextField(\"Endpoint\", \"API Endpoint\", cfg.Endpoint, \"https://...\");\n AddAiTextField(\"Model\", \"Model\", cfg.Model, \"model-id\");\n AddAiTextField(\"Temperature\", \"Temperature\", cfg.Temperature.ToString(\"0.##\"), \"0.0 – 2.0\");\n AddAiTextField(\"MaxTokens\", \"Max Tokens\", cfg.MaxTokens.ToString(), \"e.g. 1024\");\n AddAiCheckField(\"Enabled\", \"Enable this provider\", cfg.Enabled);\n\n if (AiProviderStatusText != null)\n AiProviderStatusText.Text = cfg.Enabled\n ? $\"{name} enabled — AI wiring comes in a future update.\"\n : $\"{name} configured (disabled until enabled).\";\n }\n\n private static TextBlock MakeAiHint(string text) => new()\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x70, 0x70, 0x90)),\n TextWrapping = TextWrapping.Wrap,\n Margin = new Thickness(0, 0, 0, 6)\n };\n\n private void AddAiLabel(string text)\n {\n AiProviderFieldsPanel.Children.Add(new TextBlock\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xA0, 0xA0, 0xC8)),\n Margin = new Thickness(0, 4, 0, 2)\n });\n }\n\n private void AddAiTextField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var tb = new TextBox\n {\n Text = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(tb);\n _aiDynamicFields[key] = tb;\n }\n\n private void AddAiPasswordField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var pb = new PasswordBox\n {\n Password = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(pb);\n _aiDynamicFields[key] = pb;\n }\n\n private void AddAiCheckField(string key, string label, bool value)\n {\n var cb = new CheckBox\n {\n Content = label,\n IsChecked = value,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n FontSize = 10,\n Margin = new Thickness(0, 6, 0, 0)\n };\n AiProviderFieldsPanel.Children.Add(cb);\n _aiDynamicFields[key] = cb;\n }\n\n private void AiProviderCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n {\n if (_aiProviderUiSyncing || !IsLoaded || AiProviderFieldsPanel == null) return;\n\n // Save fields to the provider we're leaving before loading the new one\n FlushAiFieldsToSettings(_aiFieldsBoundProvider);\n RebuildAiProviderFields();\n }\n\n private void SaveAiSettings_Click(object sender, RoutedEventArgs e) => SaveAiSettings();\n\n private void BotDisplayNameBox_LostFocus(object sender, RoutedEventArgs e)\n {\n if (BotDisplayNameBox == null) return;\n _botDisplayName = BotDisplayNameBox.Text.Trim();\n _aiSettings.BotDisplayName = _botDisplayName;\n }\n\n private static bool TryParseBbotCommand(string msg, out string userPrompt)\n {\n userPrompt = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n msg = msg.Trim();\n if (!msg.StartsWith(AiCommandToken, StringComparison.OrdinalIgnoreCase)) return false;\n if (msg.Length > AiCommandToken.Length)\n {\n char next = msg[AiCommandToken.Length];\n if (!char.IsWhiteSpace(next)) return false;\n userPrompt = msg[(AiCommandToken.Length + 1)..].Trim();\n }\n return true;\n }\n\n private static readonly Regex FirstCommandTokenRegex =\n new(@\"^(!\\S+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n private static bool TryGetFirstCommandToken(string msg, out string token)\n {\n token = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n var m = FirstCommandTokenRegex.Match(msg.Trim());\n if (!m.Success) return false;\n token = m.Groups[1].Value;\n return true;\n }\n\n private static bool CommandMatchesFirstToken(string msg, string cmd)\n {\n if (!TryGetFirstCommandToken(msg, out string token)) return false;\n return string.Equals(token, cmd, StringComparison.OrdinalIgnoreCase);\n }\n}"}, "e192f36d751b4a35eacbe5efef2868fcb6214cd3": {"t": 1785760692.2077334, "n": 14238, "h": "31a9d77121331e7e", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Windows;\nusing System.Windows.Controls;\n\nnamespace IMVUCompanion;\n\npublic partial class MainWindow\n{\n private const string AiSettingsFile = \"ai_settings.json\";\n private const string AiCommandToken = \"!bbot\";\n private const string AiMaintenanceReply = \"Sorry, my conversation module is under maintenance.\";\n\n private static readonly string[] AiProviderNames =\n {\n \"Grok\", \"Claude\", \"Gemini\", \"GPT\", \"DeepSeek\", \"Mistral\", \"Qwen\", \"Llama\", \"Fireworks\"\n };\n\n private readonly Dictionary<string, FrameworkElement> _aiDynamicFields = new(StringComparer.OrdinalIgnoreCase);\n private AiSettings _aiSettings = new();\n private string _botDisplayName = \"\";\n\n private sealed class ProviderConfig\n {\n public string ApiKey { get; set; } = \"\";\n public string Endpoint { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1024;\n public bool Enabled { get; set; }\n }\n\n private sealed class AiSettings\n {\n public string SelectedProvider { get; set; } = \"Grok\";\n public string BotDisplayName { get; set; } = \"\";\n public Dictionary<string, ProviderConfig> Providers { get; set; } =\n new Dictionary<string, ProviderConfig>(StringComparer.OrdinalIgnoreCase);\n }\n\n private static ProviderConfig DefaultProviderConfig(string name) => name switch\n {\n \"Grok\" => new ProviderConfig\n {\n Endpoint = \"https://api.x.ai/v1\",\n Model = \"grok-2\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Claude\" => new ProviderConfig\n {\n Endpoint = \"https://api.anthropic.com/v1\",\n Model = \"claude-sonnet-4-20250514\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Gemini\" => new ProviderConfig\n {\n Endpoint = \"https://generativelanguage.googleapis.com/v1beta\",\n Model = \"gemini-2.0-flash\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"GPT\" => new ProviderConfig\n {\n Endpoint = \"https://api.openai.com/v1\",\n Model = \"gpt-4o\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"DeepSeek\" => new ProviderConfig\n {\n Endpoint = \"https://api.deepseek.com/v1\",\n Model = \"deepseek-chat\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Mistral\" => new ProviderConfig\n {\n Endpoint = \"https://api.mistral.ai/v1\",\n Model = \"mistral-large-latest\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Qwen\" => new ProviderConfig\n {\n Endpoint = \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n Model = \"qwen-plus\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Llama\" => new ProviderConfig\n {\n Endpoint = \"https://api.together.xyz/v1\",\n Model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n \"Fireworks\" => new ProviderConfig\n {\n Endpoint = \"https://api.fireworks.ai/inference/v1\",\n Model = \"accounts/fireworks/models/llama-v3p1-70b-instruct\",\n Temperature = 0.7,\n MaxTokens = 1024\n },\n _ => new ProviderConfig()\n };\n\n private void InitAiProvidersUi()\n {\n if (AiProviderCombo == null) return;\n\n AiProviderCombo.Items.Clear();\n foreach (string name in AiProviderNames)\n AiProviderCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });\n\n LoadAiSettings();\n\n string selected = _aiSettings.SelectedProvider;\n for (int i = 0; i < AiProviderCombo.Items.Count; i++)\n {\n if (AiProviderCombo.Items[i] is ComboBoxItem cbi &&\n string.Equals(cbi.Tag?.ToString(), selected, StringComparison.OrdinalIgnoreCase))\n {\n AiProviderCombo.SelectedIndex = i;\n break;\n }\n }\n if (AiProviderCombo.SelectedIndex < 0 && AiProviderCombo.Items.Count > 0)\n AiProviderCombo.SelectedIndex = 0;\n\n if (BotDisplayNameBox != null)\n BotDisplayNameBox.Text = _aiSettings.BotDisplayName;\n\n _botDisplayName = _aiSettings.BotDisplayName?.Trim() ?? \"\";\n RebuildAiProviderFields();\n }\n\n private void LoadAiSettings()\n {\n _aiSettings = new AiSettings();\n try\n {\n if (File.Exists(AiSettingsFile))\n {\n string json = File.ReadAllText(AiSettingsFile);\n var loaded = JsonSerializer.Deserialize<AiSettings>(json);\n if (loaded != null) _aiSettings = loaded;\n }\n }\n catch (Exception ex) { AppendLog(\"Load AI settings err: \" + ex.Message, LogCategory.Warning); }\n\n foreach (string name in AiProviderNames)\n {\n if (!_aiSettings.Providers.ContainsKey(name))\n _aiSettings.Providers[name] = DefaultProviderConfig(name);\n }\n\n if (string.IsNullOrWhiteSpace(_aiSettings.SelectedProvider) ||\n !AiProviderNames.Contains(_aiSettings.SelectedProvider, StringComparer.OrdinalIgnoreCase))\n _aiSettings.SelectedProvider = \"Grok\";\n }\n\n private void SaveAiSettings()\n {\n try\n {\n FlushAiFieldsToSettings();\n if (BotDisplayNameBox != null)\n _aiSettings.BotDisplayName = BotDisplayNameBox.Text.Trim();\n _botDisplayName = _aiSettings.BotDisplayName;\n\n string json = JsonSerializer.Serialize(_aiSettings, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(AiSettingsFile, json);\n AppendLog(\"AI settings saved to ai_settings.json.\", LogCategory.Info);\n }\n catch (Exception ex) { AppendLog(\"Save AI settings err: \" + ex.Message, LogCategory.Error); }\n }\n\n private string? GetSelectedAiProviderName()\n {\n if (AiProviderCombo?.SelectedItem is ComboBoxItem cbi)\n return cbi.Tag?.ToString() ?? cbi.Content?.ToString();\n return _aiSettings.SelectedProvider;\n }\n\n private ProviderConfig GetOrCreateProviderConfig(string name)\n {\n if (!_aiSettings.Providers.TryGetValue(name, out var cfg) || cfg == null)\n {\n cfg = DefaultProviderConfig(name);\n _aiSettings.Providers[name] = cfg;\n }\n return cfg;\n }\n\n private void FlushAiFieldsToSettings()\n {\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n var cfg = GetOrCreateProviderConfig(name);\n if (_aiDynamicFields.TryGetValue(\"ApiKey\", out var apiEl) && apiEl is PasswordBox apiPb)\n cfg.ApiKey = apiPb.Password;\n if (_aiDynamicFields.TryGetValue(\"Endpoint\", out var epEl) && epEl is TextBox epTb)\n cfg.Endpoint = epTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Model\", out var modelEl) && modelEl is TextBox modelTb)\n cfg.Model = modelTb.Text.Trim();\n if (_aiDynamicFields.TryGetValue(\"Temperature\", out var tempEl) && tempEl is TextBox tempTb &&\n double.TryParse(tempTb.Text.Trim(), out double temp))\n cfg.Temperature = Math.Clamp(temp, 0, 2);\n if (_aiDynamicFields.TryGetValue(\"MaxTokens\", out var tokEl) && tokEl is TextBox tokTb &&\n int.TryParse(tokTb.Text.Trim(), out int tok))\n cfg.MaxTokens = Math.Max(1, tok);\n if (_aiDynamicFields.TryGetValue(\"Enabled\", out var enEl) && enEl is CheckBox enCb)\n cfg.Enabled = enCb.IsChecked == true;\n }\n\n private void RebuildAiProviderFields()\n {\n if (AiProviderFieldsPanel == null) return;\n\n FlushAiFieldsToSettings();\n\n string? name = GetSelectedAiProviderName();\n if (string.IsNullOrEmpty(name)) return;\n\n _aiSettings.SelectedProvider = name;\n var cfg = GetOrCreateProviderConfig(name);\n\n AiProviderFieldsPanel.Children.Clear();\n _aiDynamicFields.Clear();\n\n AiProviderFieldsPanel.Children.Add(MakeAiHint(\n name switch\n {\n \"Grok\" => \"xAI Grok — OpenAI-compatible chat completions.\",\n \"Claude\" => \"Anthropic Claude — Messages API compatible endpoint.\",\n \"Gemini\" => \"Google Gemini — API key from Google AI Studio.\",\n \"GPT\" => \"OpenAI GPT — standard OpenAI API.\",\n \"DeepSeek\" => \"DeepSeek — OpenAI-compatible API.\",\n \"Mistral\" => \"Mistral AI — La Plateforme API.\",\n \"Qwen\" => \"Alibaba Qwen — DashScope compatible mode.\",\n \"Llama\" => \"Meta Llama — via Together or compatible host.\",\n \"Fireworks\" => \"Fireworks AI — fast inference API.\",\n _ => \"Configure API credentials for this provider.\"\n }));\n\n AddAiPasswordField(\"ApiKey\", \"API Key\", cfg.ApiKey, \"Paste your API key\");\n AddAiTextField(\"Endpoint\", \"API Endpoint\", cfg.Endpoint, \"https://...\");\n AddAiTextField(\"Model\", \"Model\", cfg.Model, \"model-id\");\n AddAiTextField(\"Temperature\", \"Temperature\", cfg.Temperature.ToString(\"0.##\"), \"0.0 – 2.0\");\n AddAiTextField(\"MaxTokens\", \"Max Tokens\", cfg.MaxTokens.ToString(), \"e.g. 1024\");\n AddAiCheckField(\"Enabled\", \"Enable this provider\", cfg.Enabled);\n\n if (AiProviderStatusText != null)\n AiProviderStatusText.Text = cfg.Enabled\n ? $\"{name} enabled — AI wiring comes in a future update.\"\n : $\"{name} configured (disabled until enabled).\";\n }\n\n private static TextBlock MakeAiHint(string text) => new()\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x70, 0x70, 0x90)),\n TextWrapping = TextWrapping.Wrap,\n Margin = new Thickness(0, 0, 0, 6)\n };\n\n private void AddAiLabel(string text)\n {\n AiProviderFieldsPanel.Children.Add(new TextBlock\n {\n Text = text,\n FontSize = 9,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xA0, 0xA0, 0xC8)),\n Margin = new Thickness(0, 4, 0, 2)\n });\n }\n\n private void AddAiTextField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var tb = new TextBox\n {\n Text = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(tb);\n _aiDynamicFields[key] = tb;\n }\n\n private void AddAiPasswordField(string key, string label, string value, string placeholder)\n {\n AddAiLabel(label);\n var pb = new PasswordBox\n {\n Password = value,\n Height = 22,\n Background = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0x14, 0x14, 0x28)),\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n ToolTip = placeholder\n };\n AiProviderFieldsPanel.Children.Add(pb);\n _aiDynamicFields[key] = pb;\n }\n\n private void AddAiCheckField(string key, string label, bool value)\n {\n var cb = new CheckBox\n {\n Content = label,\n IsChecked = value,\n Foreground = new System.Windows.Media.SolidColorBrush(\n System.Windows.Media.Color.FromRgb(0xC0, 0xC0, 0xE0)),\n FontSize = 10,\n Margin = new Thickness(0, 6, 0, 0)\n };\n AiProviderFieldsPanel.Children.Add(cb);\n _aiDynamicFields[key] = cb;\n }\n\n private void AiProviderCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n {\n if (!IsLoaded || AiProviderFieldsPanel == null) return;\n RebuildAiProviderFields();\n }\n\n private void SaveAiSettings_Click(object sender, RoutedEventArgs e) => SaveAiSettings();\n\n private void BotDisplayNameBox_LostFocus(object sender, RoutedEventArgs e)\n {\n if (BotDisplayNameBox == null) return;\n _botDisplayName = BotDisplayNameBox.Text.Trim();\n _aiSettings.BotDisplayName = _botDisplayName;\n }\n\n private static bool TryParseBbotCommand(string msg, out string userPrompt)\n {\n userPrompt = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n msg = msg.Trim();\n if (!msg.StartsWith(AiCommandToken, StringComparison.OrdinalIgnoreCase)) return false;\n if (msg.Length > AiCommandToken.Length)\n {\n char next = msg[AiCommandToken.Length];\n if (!char.IsWhiteSpace(next)) return false;\n userPrompt = msg[(AiCommandToken.Length + 1)..].Trim();\n }\n return true;\n }\n\n private static readonly Regex FirstCommandTokenRegex =\n new(@\"^(!\\S+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\n\n private static bool TryGetFirstCommandToken(string msg, out string token)\n {\n token = \"\";\n if (string.IsNullOrWhiteSpace(msg)) return false;\n var m = FirstCommandTokenRegex.Match(msg.Trim());\n if (!m.Success) return false;\n token = m.Groups[1].Value;\n return true;\n }\n\n private static bool CommandMatchesFirstToken(string msg, string cmd)\n {\n if (!TryGetFirstCommandToken(msg, out string token)) return false;\n return string.Equals(token, cmd, StringComparison.OrdinalIgnoreCase);\n }\n}"}}}} |