Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/badcharmvu_imvucompanion.json
T

1 line
97 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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}"}}}}