{"repo": "achuan-2/slidesci", "files": {"SlideSCI/components/AISidebarControl.cs": {"fdab6a504df7eee331d5c0676e6b1f60091db177": {"t": 1785746184.6312273, "n": 59906, "h": "9e04dab5d8dcad98", "c": "using System;\nusing System.Collections.Generic;\nusing System.Drawing;\nusing System.Drawing.Drawing2D;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Windows.Forms;\nusing Newtonsoft.Json;\nusing PowerPoint = Microsoft.Office.Interop.PowerPoint;\n\nnamespace SlideSCI\n{\n public class AISidebarControl : UserControl\n {\n private FlowLayoutPanel chatFlowLayout;\n \n private Panel inputPanel;\n private TextBox txtInput;\n private Button btnSend;\n private Button btnClear;\n private Button btnSettings;\n private ToolTip toolTip;\n\n private string configPath;\n private AIConfig currentConfig;\n private List conversationHistory = new List();\n private static readonly HttpClient httpClient = new HttpClient();\n private bool isGenerating = false;\n private CancellationTokenSource cts;\n\n public PowerPoint.DocumentWindow AssociatedWindow { get; set; }\n\n public AISidebarControl()\n {\n // Enable TLS 1.2 and TLS 1.3 for secure API calls\n try\n {\n System.Net.ServicePointManager.SecurityProtocol |= \n System.Net.SecurityProtocolType.Tls12 | \n System.Net.SecurityProtocolType.Tls11 |\n (System.Net.SecurityProtocolType)12288; // TLS 1.3\n }\n catch { }\n\n InitializeConfigPath();\n LoadConfig();\n InitializeComponent();\n InitializeSystemMessage();\n }\n\n private void InitializeConfigPath()\n {\n string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);\n string slideSciDir = Path.Combine(appData, \"SlideSCI\");\n if (!Directory.Exists(slideSciDir))\n {\n Directory.CreateDirectory(slideSciDir);\n }\n configPath = Path.Combine(slideSciDir, \"ai_config.json\");\n }\n\n private void LoadConfig()\n {\n if (File.Exists(configPath))\n {\n try\n {\n string json = File.ReadAllText(configPath);\n currentConfig = JsonConvert.DeserializeObject(json);\n\n // Migration: If no presets exist but we have old single-config fields, create a default preset\n if (currentConfig != null && (currentConfig.Presets == null || currentConfig.Presets.Count == 0))\n {\n var dict = JsonConvert.DeserializeObject>(json);\n string oldUrl = dict.ContainsKey(\"ApiUrl\") ? dict[\"ApiUrl\"]?.ToString() : null;\n string oldKey = dict.ContainsKey(\"ApiKey\") ? dict[\"ApiKey\"]?.ToString() : null;\n string oldModel = dict.ContainsKey(\"Model\") ? dict[\"Model\"]?.ToString() : null;\n string oldPrompt = dict.ContainsKey(\"SystemPrompt\") ? dict[\"SystemPrompt\"]?.ToString() : null;\n\n currentConfig.Presets = new List();\n if (!string.IsNullOrEmpty(oldUrl) || !string.IsNullOrEmpty(oldKey))\n {\n currentConfig.Presets.Add(new AIPreset\n {\n Name = \"Default\",\n ApiUrl = oldUrl ?? \"https://api.openai.com/v1\",\n ApiKey = oldKey ?? \"\",\n Model = oldModel ?? \"gpt-4o\",\n SystemPrompt = oldPrompt ?? GetDefaultSystemPrompt()\n });\n currentConfig.CurrentPresetName = \"Default\";\n }\n }\n }\n catch\n {\n currentConfig = new AIConfig();\n }\n }\n else\n {\n currentConfig = new AIConfig();\n }\n\n if (currentConfig == null) currentConfig = new AIConfig();\n\n // Ensure we have some default presets if list is still empty\n if (currentConfig.Presets == null || currentConfig.Presets.Count == 0)\n {\n currentConfig.Presets = new List\n {\n new AIPreset { Name = \"Kimi\", ApiUrl = \"https://api.moonshot.cn/v1\", Model = \"kimi-latest\", SystemPrompt = GetDefaultSystemPrompt() },\n new AIPreset { Name = \"DeepSeek\", ApiUrl = \"https://api.deepseek.com/v1\", Model = \"deepseek-chat\", SystemPrompt = GetDefaultSystemPrompt() },\n new AIPreset { Name = \"OpenAI\", ApiUrl = \"https://api.openai.com/v1\", Model = \"gpt-4o\", SystemPrompt = GetDefaultSystemPrompt() }\n };\n currentConfig.CurrentPresetName = \"Kimi\";\n SaveConfig();\n }\n\n if (string.IsNullOrWhiteSpace(currentConfig.SystemPrompt) ||\n currentConfig.SystemPrompt.Contains(\"Office.MsoShapeType.msoShapeRectangle\") ||\n currentConfig.SystemPrompt.Contains(\"msoShapeRoundRectangle\"))\n {\n currentConfig.SystemPrompt = GetDefaultSystemPrompt();\n SaveConfig();\n }\n }\n\n private void SaveConfig()\n {\n try\n {\n string json = JsonConvert.SerializeObject(currentConfig, Formatting.Indented);\n File.WriteAllText(configPath, json);\n }\n catch (Exception ex)\n {\n MessageBox.Show($\"保存设置失败: {ex.Message}\", \"错误\", MessageBoxButtons.OK, MessageBoxIcon.Error);\n }\n }\n\n private void InitializeComponent()\n {\n this.Dock = DockStyle.Fill;\n this.BackColor = Color.FromArgb(245, 246, 247);\n this.Font = new Font(\"Microsoft YaHei\", 9F, FontStyle.Regular, GraphicsUnit.Point);\n\n toolTip = new ToolTip();\n\n // 1. Input Panel (Docked at Bottom)\n inputPanel = new Panel\n {\n Dock = DockStyle.Bottom,\n Height = 105,\n BackColor = Color.White,\n Padding = new Padding(10, 8, 10, 8)\n };\n inputPanel.Paint += (s, e) =>\n {\n using (Pen pen = new Pen(Color.FromArgb(225, 225, 225), 1))\n {\n e.Graphics.DrawLine(pen, 0, 0, inputPanel.Width, 0);\n }\n };\n\n // Custom border panel for textbox\n Panel txtWrapper = new Panel\n {\n Location = new Point(10, 8),\n Width = this.Width - 20,\n Height = 52,\n BackColor = Color.White,\n Padding = new Padding(4)\n };\n txtWrapper.Paint += (s, e) =>\n {\n Color borderColor = txtInput.Focused ? Color.FromArgb(0, 120, 212) : Color.FromArgb(220, 220, 220);\n using (Pen pen = new Pen(borderColor, 1))\n {\n e.Graphics.DrawRectangle(pen, 0, 0, txtWrapper.Width - 1, txtWrapper.Height - 1);\n }\n };\n\n txtInput = new TextBox\n {\n Multiline = true,\n BorderStyle = BorderStyle.None,\n Dock = DockStyle.Fill,\n Font = new Font(\"Microsoft YaHei\", 9F),\n ScrollBars = ScrollBars.Vertical\n };\n txtInput.GotFocus += (s, e) => txtWrapper.Invalidate();\n txtInput.LostFocus += (s, e) => txtWrapper.Invalidate();\n txtInput.KeyDown += TxtInput_KeyDown;\n txtWrapper.Controls.Add(txtInput);\n inputPanel.Controls.Add(txtWrapper);\n\n // Icon-only Clear button\n btnClear = new Button\n {\n Image = CreateIcon(\"trash\", 16, Color.FromArgb(100, 100, 100)),\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.Transparent,\n Location = new Point(10, 68),\n Width = 28,\n Height = 26,\n Cursor = Cursors.Hand\n };\n btnClear.FlatAppearance.BorderSize = 0;\n btnClear.FlatAppearance.MouseOverBackColor = Color.FromArgb(240, 240, 240);\n btnClear.Click += (s, e) => ClearChat();\n toolTip.SetToolTip(btnClear, \"清空对话\");\n inputPanel.Controls.Add(btnClear);\n\n // Icon-only Settings button\n btnSettings = new Button\n {\n Image = CreateIcon(\"settings\", 16, Color.FromArgb(100, 100, 100)),\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.Transparent,\n Location = new Point(44, 68),\n Width = 28,\n Height = 26,\n Cursor = Cursors.Hand\n };\n btnSettings.FlatAppearance.BorderSize = 0;\n btnSettings.FlatAppearance.MouseOverBackColor = Color.FromArgb(240, 240, 240);\n btnSettings.Click += BtnSettings_Click;\n toolTip.SetToolTip(btnSettings, \"AI 设置\");\n inputPanel.Controls.Add(btnSettings);\n\n btnSend = new Button\n {\n Text = \"发送\",\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.FromArgb(0, 120, 212),\n ForeColor = Color.White,\n Font = new Font(\"Microsoft YaHei\", 9F, FontStyle.Bold),\n Location = new Point(this.Width - 75, 68),\n Width = 65,\n Height = 26,\n Cursor = Cursors.Hand\n };\n btnSend.FlatAppearance.BorderSize = 0;\n btnSend.Click += async (s, e) => await HandleSendAsync();\n inputPanel.Controls.Add(btnSend);\n\n this.Controls.Add(inputPanel);\n\n // 2. Chat Flow Layout Panel (Docked Fill)\n chatFlowLayout = new FlowLayoutPanel\n {\n Dock = DockStyle.Fill,\n AutoScroll = true,\n FlowDirection = FlowDirection.LeftToRight,\n WrapContents = true,\n Padding = new Padding(8),\n BackColor = Color.FromArgb(245, 246, 247)\n };\n typeof(FlowLayoutPanel).InvokeMember(\"DoubleBuffered\",\n System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,\n null, chatFlowLayout, new object[] { true });\n\n this.Controls.Add(chatFlowLayout);\n\n // Ensure Z-order prevents bottom layout overlapping\n chatFlowLayout.BringToFront();\n\n this.Resize += AISidebarControl_Resize;\n chatFlowLayout.Resize += (s, e) => UpdateBubbleWidths();\n }\n\n private void AISidebarControl_Resize(object sender, EventArgs e)\n {\n if (inputPanel != null)\n {\n foreach (Control ctrl in inputPanel.Controls)\n {\n if (ctrl is Panel p) // txtWrapper\n {\n p.Width = inputPanel.Width - 20;\n }\n else if (ctrl == btnSend)\n {\n btnSend.Left = inputPanel.Width - 75;\n }\n }\n }\n UpdateBubbleWidths();\n }\n\n private void BtnSettings_Click(object sender, EventArgs e)\n {\n using (var settingsForm = new AISettingsForm(currentConfig))\n {\n if (settingsForm.ShowDialog(this) == DialogResult.OK)\n {\n settingsForm.UpdateConfig(currentConfig);\n SaveConfig();\n\n // Update the active system message in the conversation history\n if (conversationHistory.Count > 0 && conversationHistory[0].Role == \"system\")\n {\n conversationHistory[0].Content = currentConfig.SystemPrompt;\n }\n }\n }\n }\n\n private void InitializeSystemMessage()\n {\n string systemPrompt = currentConfig.SystemPrompt;\n if (string.IsNullOrWhiteSpace(systemPrompt))\n {\n systemPrompt = GetDefaultSystemPrompt();\n currentConfig.SystemPrompt = systemPrompt;\n }\n\n conversationHistory.Add(new ChatMessage { Role = \"system\", Content = systemPrompt });\n AppendBubble(\"你好!我是你的 SlideSCI AI 助手。我可以执行以下命令:\\n1. 把选中的文字或文本框颜色改为红色、蓝色等。\\n2. 添加各种形状(矩形、圆形等)。\\n3. 新增一页空白幻灯片。\\n4. 将幻灯片中的指定文字进行查找替换。\\n\\n请在下方输入你的指令!\", isUser: false, isSystem: false);\n }\n\n public static string GetDefaultSystemPrompt()\n {\n return @\"你是一个PowerPoint AI助手,你可以通过编写和执行C#代码来帮用户操作当前幻灯片。\n你有调用 `execute_csharp_code` 工具的能力。\n当你需要对幻灯片进行操作时(如修改文字颜色、添加形状、新建页面、替换文字等),你应该写C#代码,然后通过调用 `execute_csharp_code` 来执行。\n\n编写C#代码时请遵守以下规则:\n1. 代码会在一个包含 `PowerPoint.Application Application` 参数的方法体内执行:`public static void Run(PowerPoint.Application Application)`。\n2. 已经默认导入了以下命名空间,请勿再在代码中写 using 声明:\n - System\n - System.Collections.Generic\n - System.Linq\n - System.Drawing\n - System.Windows.Forms\n - Microsoft.Office.Interop.PowerPoint (别名为 PowerPoint)\n - Microsoft.Office.Core (别名为 Office)\n3. 安全获取当前幻灯片对象 (Slide) 的方法:\n PowerPoint.Slide slide = null;\n try { slide = Application.ActiveWindow.View.Slide as PowerPoint.Slide; } catch {}\n if (slide == null) {\n try { slide = Application.ActivePresentation.Slides[Application.ActiveWindow.Selection.SlideRange[1].SlideIndex]; } catch {}\n }\n if (slide == null) {\n throw new Exception(\"\"没有找到活动的幻灯片,请确保PPT处于正常视图。\"\");\n }\n4. 新增幻灯片页面请直接对 Application.ActivePresentation.Slides 进行 Add 或 Add2 操作,例如:\n var pres = Application.ActivePresentation;\n pres.Slides.Add(pres.Slides.Count + 1, PowerPoint.PpSlideLayout.ppLayoutBlank);\n5. 操作文字颜色时,请使用 `System.Drawing.ColorTranslator.ToOle(Color.Red)` 将颜色转换为 OLE 颜色。例如:\n shape.TextFrame.TextRange.Font.Color.RGB = ColorTranslator.ToOle(Color.Red);\n6. 插入形状时,第一个参数必须是 `Office.MsoAutoShapeType` 枚举类型(注意:千万不可写错成 `Office.MsoShapeType`,否则会报编译错误)。常用的形状枚举如下:\n - 矩形:`Office.MsoAutoShapeType.msoShapeRectangle`\n - 圆角矩形(注意是 Rounded,不是 Round):`Office.MsoAutoShapeType.msoShapeRoundedRectangle`\n - 椭圆/圆形:`Office.MsoAutoShapeType.msoShapeOval`\n 示例:`slide.Shapes.AddShape(Office.MsoAutoShapeType.msoShapeRectangle, left, top, width, height)`。位置与尺寸参数皆为 float 类型。\n7. 修改文字内容时,如果需要查找并替换,可以遍历幻灯片中的 Shape 并检查 `shape.HasTextFrame == Office.MsoTriState.msoTrue && shape.TextFrame.HasText == Office.MsoTriState.msoTrue`。\n8. 请尽量编写健壮的代码,用 try-catch 包裹可能出错的地方,遇到不支持的操作时抛出异常。\n\n【核心PPT设计审美与排版规范(PPT Aesthetics & Layout Design Rules)】\n为了确保AI添加的PPT样式美观、专业且符合现代高端设计规范,请在编写代码生成/修改PPT内容时,严格遵守以下排版与审美原则:\n\n1. 配色系统与 60-30-10 黄金比例:\n - 禁止在没有用户明确要求的情况下使用刺眼的高饱和度纯原色(例如纯红 Color.Red、纯蓝 Color.Blue、纯绿 Color.Green、纯黄 Color.Yellow)。这会让 PPT 显得极不专业。\n - 使用成熟、高对比度且优雅的经典主题配色(使用 Color.FromArgb 定义颜色,然后使用 ColorTranslator.ToOle 转换):\n * 极简科技蓝主题:\n - 主背景/主色:浅灰白 Color.FromArgb(248, 249, 250) 或 极深蓝 Color.FromArgb(10, 34, 64)\n - 辅助色/卡片背景:浅天蓝 Color.FromArgb(232, 240, 254) 或 经典蓝 Color.FromArgb(26, 115, 232)\n - 点缀强调色(10%):温暖橙/金色 Color.FromArgb(245, 166, 35) 或 珊瑚红 Color.FromArgb(234, 67, 53)\n * 现代商务炭黑主题:\n - 主色:炭黑色 Color.FromArgb(43, 43, 43)\n - 辅助色:浅灰色 Color.FromArgb(240, 240, 240)\n - 点缀色:青/湖蓝色 Color.FromArgb(0, 168, 181)\n * 暖调人文/学术主题:\n - 主色:深林绿 Color.FromArgb(27, 67, 50)\n - 辅助色:柔和米色 Color.FromArgb(245, 242, 235)\n - 点缀色:赤陶红/土黄 Color.FromArgb(190, 130, 60)\n - 遵守 60-30-10 比例:背景占 60% 视觉面积,文字/卡片容器等次要元素占 30%,画龙点睛的重点高亮(如关键数字、重点结论词、边框线装饰)占 10%。\n\n2. 视觉层级与字号梯度(Visual Hierarchy & Text Scales):\n - 建立清晰鲜明的字号差,中文默认使用“微软雅黑” (Microsoft YaHei),英文默认使用“Segoe UI”:\n * 幻灯片大标题(Slide Title):36pt - 44pt,粗体 (Bold),深色高对比度。\n * 章节/卡片小标题(Card Header):20pt - 24pt,粗体。\n * 正文内容(Body Text):14pt - 18pt,常规体 (Regular),中等对比度(如深灰)。\n * 关键数据/大数字(Big Stat/Metric):48pt - 72pt 粗体,可搭配 12pt 的解释性小字。\n - 为确保文字排版完美,对所有新创建的文本框设置以下属性以防文本溢出或不对齐:\n * shape.TextFrame.WordWrap = Office.MsoTriState.msoTrue; // 开启自动换行\n * 适当清除或缩小默认内边距:\n shape.TextFrame.MarginLeft = 10f;\n shape.TextFrame.MarginRight = 10f;\n shape.TextFrame.MarginTop = 10f;\n shape.TextFrame.MarginBottom = 10f;\n\n3. 负空间与留白(Negative Space & Whitespace):\n - 不要把幻灯片塞满!页面必须保留至少 30% - 40% 的空白区域,让观众的眼睛有呼吸的空间。\n - 绝不允许使用大段文字,请将文字提炼为短句,以列表或卡片形式展现。\n\n4. 动态网格布局与防重叠(Dynamic Grid Layout & Math Positioning):\n - 永远通过代码动态获取幻灯片的物理大小,并在此基础上进行百分比定位或网格分栏计算,严禁使用写死的绝对坐标导致在不同比例的PPT中排版错乱:\n float slideWidth = Application.ActivePresentation.PageSetup.SlideWidth;\n float slideHeight = Application.ActivePresentation.PageSetup.SlideHeight;\n - 留出四周安全边距(Margin,通常设为 slideWidth * 0.06f 与 slideHeight * 0.08f)。\n - 如果需要展示并列的多个观点,应自动计算分栏位置:\n * 两栏布局:左栏占宽 45%,右栏占宽 45%,中间留空 10%。\n * 三分栏卡片布局示例:\n float margin = slideWidth * 0.05f;\n float availableWidth = slideWidth - 2 * margin;\n float gap = 20f; // 卡片间距\n float cardWidth = (availableWidth - 2 * gap) / 3f;\n float cardHeight = slideHeight * 0.5f;\n float top = slideHeight * 0.3f;\n for (int i = 0; i < 3; i++) {\n float left = margin + i * (cardWidth + gap);\n // 在 left, top, cardWidth, cardHeight 处绘制卡片容器\n }\n - 通过数学计算绝对保证生成的文本框、容器形状之间不会重叠。\n\n5. 扁平化容器卡片化(Flat Card-Based Design):\n - 列表或并列观点应使用“浅色卡片”作为容器装载,卡片背景填充非常浅的颜色(如 RGB: 245, 245, 245),并且必须去掉边框轮廓以符合现代扁平化审美:\n cardShape.Line.Visible = Office.MsoTriState.msoFalse;\n cardShape.Fill.ForeColor.RGB = ColorTranslator.ToOle(Color.FromArgb(245, 245, 245));\n - 在卡片之上叠放文字形状时,通过 Y 坐标偏移(如 cardTop + 15 放置标题,cardTop + 45 放置正文)使排版整洁大方。\n - 尽量使用无轮廓(`Line.Visible = msoFalse`)的形状,如果使用线条,应保持极细(`Line.Weight = 1.0f`)且使用温和的淡灰色。\";\n }\n\n private void TxtInput_KeyDown(object sender, KeyEventArgs e)\n {\n if (e.KeyCode == Keys.Enter && !e.Shift)\n {\n e.SuppressKeyPress = true; // Prevent newline\n btnSend.PerformClick();\n }\n }\n\n private void ClearChat()\n {\n chatFlowLayout.Controls.Clear();\n conversationHistory.Clear();\n InitializeSystemMessage();\n }\n\n private void AppendBubble(string text, bool isUser, bool isSystem, bool isError = false)\n {\n // Strip emojis from the text to prevent square box characters in RichTextBox\n text = StripEmojis(text);\n\n ChatRowPanel row = new ChatRowPanel(text, isUser, isSystem, isError, chatFlowLayout.Width - 25);\n chatFlowLayout.Controls.Add(row);\n \n chatFlowLayout.PerformLayout();\n chatFlowLayout.ScrollControlIntoView(row);\n \n try\n {\n chatFlowLayout.VerticalScroll.Value = chatFlowLayout.VerticalScroll.Maximum;\n }\n catch {}\n }\n\n private static string StripEmojis(string text)\n {\n if (string.IsNullOrEmpty(text)) return text;\n try\n {\n // Remove surrogate pairs and symbol ranges (emojis, icons, private use areas)\n return Regex.Replace(text, @\"\\p{Cs}|\\p{So}|\\p{Cn}\", \"\");\n }\n catch\n {\n return text;\n }\n }\n\n private void UpdateBubbleWidths()\n {\n if (chatFlowLayout == null) return;\n chatFlowLayout.SuspendLayout();\n int targetWidth = chatFlowLayout.Width - 25;\n if (targetWidth < 100) targetWidth = 100;\n foreach (Control ctrl in chatFlowLayout.Controls)\n {\n if (ctrl is ChatRowPanel row)\n {\n row.UpdateWidth(targetWidth);\n }\n }\n chatFlowLayout.ResumeLayout(true);\n }\n\n private async Task HandleSendAsync()\n {\n if (isGenerating)\n {\n cts?.Cancel();\n return;\n }\n\n string message = txtInput.Text.Trim();\n if (string.IsNullOrEmpty(message)) return;\n\n if (string.IsNullOrEmpty(currentConfig.ApiKey))\n {\n MessageBox.Show(\"请先配置 API Key!\", \"提示\", MessageBoxButtons.OK, MessageBoxIcon.Warning);\n BtnSettings_Click(null, null);\n return;\n }\n\n txtInput.Text = \"\";\n AppendBubble(message, isUser: true, isSystem: false);\n conversationHistory.Add(new ChatMessage { Role = \"user\", Content = message });\n\n isGenerating = true;\n btnSend.Text = \"停止\";\n btnSend.BackColor = Color.FromArgb(209, 52, 56);\n txtInput.Enabled = false;\n btnClear.Enabled = false;\n btnSettings.Enabled = false;\n\n cts = new CancellationTokenSource();\n\n try\n {\n await RunAgentLoopAsync(cts.Token);\n }\n catch (OperationCanceledException)\n {\n AppendBubble(\"[已终止]: 用户中止了 AI 响应。\", isUser: false, isSystem: true, isError: false);\n }\n catch (Exception ex)\n {\n AppendBubble($\"[AI 调用出错]:\\n{ex.ToString()}\", isUser: false, isSystem: true, isError: true);\n }\n finally\n {\n isGenerating = false;\n btnSend.Text = \"发送\";\n btnSend.BackColor = Color.FromArgb(0, 120, 212);\n \n txtInput.Enabled = true;\n btnClear.Enabled = true;\n btnSettings.Enabled = true;\n\n cts?.Dispose();\n cts = null;\n }\n }\n\n private async Task RunAgentLoopAsync(CancellationToken token)\n {\n int maxIterations = 5;\n int currentIteration = 0;\n\n var tools = new List\n {\n new ChatTool\n {\n Function = new ToolFunction\n {\n Name = \"execute_csharp_code\",\n Description = \"在幻灯片中动态编译并执行C#代码来操作系统。代码将被放置在 `public static void Run(PowerPoint.Application Application)` 方法体中运行。你可以使用任何 PowerPoint API 进行操作。\",\n Parameters = new ToolParameters\n {\n Properties = new Dictionary\n {\n {\n \"code\", new Dictionary\n {\n { \"type\", \"string\" },\n { \"description\", \"要执行的 C# 语句。必须使用合法的 C# 语法,不用声明 class 或 method,只写方法体内部的执行代码。\" }\n }\n }\n },\n Required = new List { \"code\" }\n }\n }\n }\n };\n\n while (currentIteration < maxIterations)\n {\n token.ThrowIfCancellationRequested();\n currentIteration++;\n\n var request = new ChatRequest\n {\n Model = currentConfig.Model,\n Messages = conversationHistory,\n Tools = tools,\n ToolChoice = \"auto\"\n };\n\n string requestJson = JsonConvert.SerializeObject(request);\n var content = new StringContent(requestJson, Encoding.UTF8, \"application/json\");\n\n httpClient.DefaultRequestHeaders.Clear();\n httpClient.DefaultRequestHeaders.Add(\"Authorization\", $\"Bearer {currentConfig.ApiKey}\");\n\n string url = currentConfig.ApiUrl.TrimEnd('/') + \"/chat/completions\";\n \n HttpResponseMessage response;\n try\n {\n response = await httpClient.PostAsync(url, content, token);\n }\n catch (OperationCanceledException)\n {\n throw;\n }\n catch (Exception ex)\n {\n throw new Exception($\"无法连接到 AI 服务,网络连接异常: {ex.Message}\\n请求地址: {url}\\n\\n详细信息:\\n{ex.ToString()}\");\n }\n\n token.ThrowIfCancellationRequested();\n string responseJson = await response.Content.ReadAsStringAsync();\n if (!response.IsSuccessStatusCode)\n {\n string errorDetails = \"\";\n try\n {\n var errRes = JsonConvert.DeserializeObject(responseJson);\n errorDetails = errRes?.Error?.Message;\n }\n catch {}\n if (string.IsNullOrEmpty(errorDetails))\n {\n errorDetails = responseJson;\n }\n throw new Exception($\"API 请求失败 (HTTP {response.StatusCode} - {response.ReasonPhrase}):\\n{errorDetails}\");\n }\n\n var chatRes = JsonConvert.DeserializeObject(responseJson);\n if (chatRes == null || chatRes.Choices == null || chatRes.Choices.Count == 0)\n {\n throw new Exception(\"API 返回了空响应,无法处理。\");\n }\n\n var choiceMessage = chatRes.Choices[0].Message;\n conversationHistory.Add(choiceMessage);\n\n if (!string.IsNullOrEmpty(choiceMessage.Content))\n {\n AppendBubble(choiceMessage.Content, isUser: false, isSystem: false);\n }\n\n if (choiceMessage.ToolCalls != null && choiceMessage.ToolCalls.Count > 0)\n {\n token.ThrowIfCancellationRequested();\n foreach (var toolCall in choiceMessage.ToolCalls)\n {\n if (toolCall.Function.Name == \"execute_csharp_code\")\n {\n string code = \"\";\n try\n {\n var args = JsonConvert.DeserializeObject>(toolCall.Function.Arguments);\n if (args != null && args.TryGetValue(\"code\", out string argCode))\n {\n code = argCode;\n }\n }\n catch (Exception ex)\n {\n string toolErr = $\"解析参数失败: {ex.Message}\";\n conversationHistory.Add(new ChatMessage\n {\n Role = \"tool\",\n Name = \"execute_csharp_code\",\n Content = toolErr,\n ToolCallId = toolCall.Id\n });\n AppendBubble($\"[工具调用解析失败]\\n{toolErr}\", isUser: false, isSystem: true, isError: true);\n continue;\n }\n\n AppendBubble($\"[AI 请求执行 C# 代码]:\\n{code}\", isUser: false, isSystem: true);\n\n string runResult = \"\";\n try\n {\n runResult = CSharpExecutor.Execute(code, Globals.ThisAddIn.Application);\n }\n catch (Exception ex)\n {\n runResult = $\"Critical Runtime Error: {ex.Message}\";\n }\n\n conversationHistory.Add(new ChatMessage\n {\n Role = \"tool\",\n Name = \"execute_csharp_code\",\n Content = runResult,\n ToolCallId = toolCall.Id\n });\n\n if (runResult == \"Success\")\n {\n AppendBubble(\"[执行结果]: 成功!\", isUser: false, isSystem: true);\n }\n else\n {\n AppendBubble($\"[执行结果]: 失败!\\n{runResult}\", isUser: false, isSystem: true, isError: true);\n }\n }\n }\n continue;\n }\n\n break;\n }\n }\n\n public static Bitmap CreateIcon(string type, int size, Color color)\n {\n Bitmap bmp = new Bitmap(size, size);\n using (Graphics g = Graphics.FromImage(bmp))\n {\n g.SmoothingMode = SmoothingMode.AntiAlias;\n g.Clear(Color.Transparent);\n using (Pen pen = new Pen(color, 1.8f))\n using (Brush brush = new SolidBrush(color))\n {\n if (type == \"settings\")\n {\n int r1 = size / 2 - 1;\n int r2 = size / 2 - 4;\n int cx = size / 2;\n int cy = size / 2;\n g.DrawEllipse(pen, cx - r2, cy - r2, r2 * 2, r2 * 2);\n \n for (int i = 0; i < 8; i++)\n {\n double angle = i * Math.PI / 4;\n float x1 = (float)(cx + r2 * Math.Cos(angle));\n float y1 = (float)(cy + r2 * Math.Sin(angle));\n float x2 = (float)(cx + r1 * Math.Cos(angle));\n float y2 = (float)(cy + r1 * Math.Sin(angle));\n g.DrawLine(pen, x1, y1, x2, y2);\n }\n }\n else if (type == \"trash\")\n {\n g.DrawRectangle(pen, 4, 5, size - 9, size - 8);\n g.DrawLine(pen, 2, 3, size - 3, 3);\n g.DrawLine(pen, 6, 1, size - 7, 1);\n g.DrawLine(pen, 7, 7, 7, size - 5);\n g.DrawLine(pen, size / 2, 7, size / 2, size - 5);\n g.DrawLine(pen, size - 8, 7, size - 8, size - 5);\n }\n }\n }\n return bmp;\n }\n }\n\n // --- Settings Dialog Form ---\n public class AISettingsForm : Form\n {\n private ComboBox cmbPresets;\n private Button btnAddPreset;\n private Button btnDeletePreset;\n private TextBox txtPresetName;\n private TextBox txtApiUrl;\n private TextBox txtApiKey;\n private CheckBox chkShowKey;\n private TextBox txtModel;\n private TextBox txtSystemPrompt;\n private Button btnSave;\n private Button btnCancel;\n private Button btnRestoreDefault;\n private AIConfig config;\n private List tempPresets;\n private string activePresetName;\n private AIPreset currentEditingPreset = null;\n\n public AISettingsForm(AIConfig currentConfig)\n {\n this.config = currentConfig;\n this.tempPresets = new List();\n foreach (var p in currentConfig.Presets)\n tempPresets.Add(new AIPreset { Name = p.Name, ApiUrl = p.ApiUrl, ApiKey = p.ApiKey, Model = p.Model, SystemPrompt = p.SystemPrompt });\n this.activePresetName = currentConfig.CurrentPresetName;\n InitializeComponent();\n LoadPresetsToCombo();\n }\n\n private void InitializeComponent()\n {\n this.Text = \"AI 助手设置\";\n this.Size = new Size(620, 580);\n this.FormBorderStyle = FormBorderStyle.FixedDialog;\n this.MaximizeBox = false;\n this.MinimizeBox = false;\n this.StartPosition = FormStartPosition.CenterParent;\n this.Font = new Font(\"Microsoft YaHei\", 9F);\n this.BackColor = Color.White;\n\n Label lblSelectPreset = new Label { Text = \"选择预设:\", Location = new Point(15, 20), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n cmbPresets = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Location = new Point(100, 20), Width = 280, Height = 24 };\n\n btnAddPreset = new Button { Text = \"新增\", Location = new Point(390, 18), Width = 90, Height = 28, BackColor = Color.FromArgb(240, 240, 240), FlatStyle = FlatStyle.Flat };\n btnAddPreset.FlatAppearance.BorderColor = Color.LightGray;\n btnDeletePreset = new Button { Text = \"删除\", Location = new Point(490, 18), Width = 90, Height = 28, BackColor = Color.FromArgb(240, 240, 240), FlatStyle = FlatStyle.Flat };\n btnDeletePreset.FlatAppearance.BorderColor = Color.LightGray;\n\n Label lblPresetName = new Label { Text = \"预设名称:\", Location = new Point(15, 60), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtPresetName = new TextBox { Location = new Point(100, 60), Width = 480 };\n txtPresetName.TextChanged += TxtPresetName_TextChanged;\n\n Label lblUrl = new Label { Text = \"API 地址:\", Location = new Point(15, 100), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtApiUrl = new TextBox { Location = new Point(100, 100), Width = 480 };\n\n Label lblKey = new Label { Text = \"API Key:\", Location = new Point(15, 140), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtApiKey = new TextBox { Location = new Point(100, 140), Width = 400, PasswordChar = '*' };\n chkShowKey = new CheckBox { Text = \"显示\", Location = new Point(510, 140), Width = 70, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n chkShowKey.CheckedChanged += (s, e) => { txtApiKey.PasswordChar = chkShowKey.Checked ? '\\0' : '*'; };\n\n Label lblModel = new Label { Text = \"模型名称:\", Location = new Point(15, 180), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtModel = new TextBox { Location = new Point(100, 180), Width = 480 };\n\n Label lblPrompt = new Label { Text = \"系统提示词:\", Location = new Point(15, 220), Width = 85, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtSystemPrompt = new TextBox { Location = new Point(100, 220), Width = 480, Height = 240, Multiline = true, ScrollBars = ScrollBars.Vertical, Font = new Font(\"Microsoft YaHei\", 9F) };\n\n btnRestoreDefault = new Button { Text = \"恢复默认提示词\", Location = new Point(100, 485), Width = 150, Height = 28, FlatStyle = FlatStyle.Flat };\n btnRestoreDefault.FlatAppearance.BorderColor = Color.LightGray;\n btnRestoreDefault.Click += (s, e) => { txtSystemPrompt.Text = AISidebarControl.GetDefaultSystemPrompt(); };\n\n btnSave = new Button { Text = \"保存\", DialogResult = DialogResult.OK, Location = new Point(380, 485), Width = 90, Height = 28, BackColor = Color.FromArgb(0, 120, 212), ForeColor = Color.White, FlatStyle = FlatStyle.Flat };\n btnSave.FlatAppearance.BorderSize = 0;\n btnCancel = new Button { Text = \"取消\", DialogResult = DialogResult.Cancel, Location = new Point(490, 485), Width = 90, Height = 28, FlatStyle = FlatStyle.Flat };\n btnCancel.FlatAppearance.BorderColor = Color.LightGray;\n\n this.Controls.AddRange(new Control[] {\n lblSelectPreset, cmbPresets, btnAddPreset, btnDeletePreset,\n lblPresetName, txtPresetName, lblUrl, txtApiUrl,\n lblKey, txtApiKey, chkShowKey, lblModel, txtModel,\n lblPrompt, txtSystemPrompt, btnRestoreDefault, btnSave, btnCancel\n });\n this.AcceptButton = btnSave;\n this.CancelButton = btnCancel;\n btnAddPreset.Click += BtnAddPreset_Click;\n btnDeletePreset.Click += BtnDeletePreset_Click;\n }\n\n private void LoadPresetsToCombo()\n {\n cmbPresets.SelectedIndexChanged -= CmbPresets_SelectedIndexChanged;\n cmbPresets.DataSource = null;\n cmbPresets.DataSource = tempPresets;\n cmbPresets.DisplayMember = \"Name\";\n int idx = tempPresets.FindIndex(p => p.Name == activePresetName);\n if (idx < 0 && tempPresets.Count > 0) idx = 0;\n cmbPresets.SelectedIndex = idx;\n if (idx >= 0) { LoadPresetToFields(tempPresets[idx]); activePresetName = tempPresets[idx].Name; }\n cmbPresets.SelectedIndexChanged += CmbPresets_SelectedIndexChanged;\n }\n\n private void SaveCurrentPresetEdits()\n {\n if (currentEditingPreset != null)\n {\n currentEditingPreset.Name = txtPresetName.Text.Trim();\n currentEditingPreset.ApiUrl = txtApiUrl.Text.Trim();\n currentEditingPreset.ApiKey = txtApiKey.Text.Trim();\n currentEditingPreset.Model = txtModel.Text.Trim();\n currentEditingPreset.SystemPrompt = txtSystemPrompt.Text;\n }\n }\n\n private void LoadPresetToFields(AIPreset preset)\n {\n currentEditingPreset = preset;\n if (preset != null)\n {\n txtPresetName.Text = preset.Name;\n txtApiUrl.Text = preset.ApiUrl;\n txtApiKey.Text = preset.ApiKey;\n txtModel.Text = preset.Model;\n txtSystemPrompt.Text = preset.SystemPrompt;\n }\n }\n\n private void CmbPresets_SelectedIndexChanged(object sender, EventArgs e)\n {\n SaveCurrentPresetEdits();\n if (cmbPresets.SelectedIndex >= 0)\n {\n var selected = cmbPresets.SelectedItem as AIPreset;\n LoadPresetToFields(selected);\n this.activePresetName = selected?.Name ?? \"\";\n }\n }\n\n private void TxtPresetName_TextChanged(object sender, EventArgs e)\n {\n if (currentEditingPreset != null && cmbPresets.SelectedItem == currentEditingPreset)\n {\n string newName = txtPresetName.Text.Trim();\n if (!string.IsNullOrEmpty(newName) && currentEditingPreset.Name != newName)\n {\n currentEditingPreset.Name = newName;\n int idx = cmbPresets.SelectedIndex;\n cmbPresets.SelectedIndexChanged -= CmbPresets_SelectedIndexChanged;\n cmbPresets.DataSource = null;\n cmbPresets.DataSource = tempPresets;\n cmbPresets.DisplayMember = \"Name\";\n cmbPresets.SelectedIndex = idx;\n cmbPresets.SelectedIndexChanged += CmbPresets_SelectedIndexChanged;\n }\n }\n }\n\n private void BtnAddPreset_Click(object sender, EventArgs e)\n {\n SaveCurrentPresetEdits();\n int count = 1;\n string newName = \"New Preset\";\n while (tempPresets.Exists(p => p.Name == $\"{newName} {count}\")) count++;\n string finalName = $\"{newName} {count}\";\n var newPreset = new AIPreset { Name = finalName, ApiUrl = \"https://api.openai.com/v1\", ApiKey = \"\", Model = \"gpt-4o\", SystemPrompt = AISidebarControl.GetDefaultSystemPrompt() };\n tempPresets.Add(newPreset);\n activePresetName = finalName;\n LoadPresetsToCombo();\n }\n\n private void BtnDeletePreset_Click(object sender, EventArgs e)\n {\n if (tempPresets.Count <= 1) { MessageBox.Show(\"必须保留至少一个预设!\", \"提示\", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }\n if (cmbPresets.SelectedItem is AIPreset selected)\n {\n if (MessageBox.Show($\"确定删除预设 '{selected.Name}' 吗?\", \"确认删除\", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)\n {\n tempPresets.Remove(selected);\n currentEditingPreset = null;\n activePresetName = tempPresets[0].Name;\n LoadPresetsToCombo();\n }\n }\n }\n\n public void UpdateConfig(AIConfig targetConfig)\n {\n SaveCurrentPresetEdits();\n targetConfig.Presets = this.tempPresets;\n targetConfig.CurrentPresetName = this.activePresetName;\n }\n }\n\n // --- Data Models for API ---\n public class AIPreset\n {\n public string Name { get; set; } = \"Default\";\n public string ApiUrl { get; set; } = \"https://api.openai.com/v1\";\n public string ApiKey { get; set; } = \"\";\n public string Model { get; set; } = \"gpt-4o\";\n public string SystemPrompt { get; set; } = \"\";\n public override string ToString() => Name;\n }\n\n public class AIConfig\n {\n public List Presets { get; set; } = new List();\n public string CurrentPresetName { get; set; } = \"\";\n\n [JsonIgnore]\n public AIPreset CurrentPreset\n {\n get\n {\n if (Presets == null || Presets.Count == 0)\n {\n Presets = new List { new AIPreset { Name = \"Default\" } };\n CurrentPresetName = \"Default\";\n }\n var active = Presets.Find(p => p.Name == CurrentPresetName);\n if (active == null) { active = Presets[0]; CurrentPresetName = active.Name; }\n return active;\n }\n }\n\n [JsonIgnore] public string ApiUrl { get => CurrentPreset.ApiUrl; set => CurrentPreset.ApiUrl = value; }\n [JsonIgnore] public string ApiKey { get => CurrentPreset.ApiKey; set => CurrentPreset.ApiKey = value; }\n [JsonIgnore] public string Model { get => CurrentPreset.Model; set => CurrentPreset.Model = value; }\n [JsonIgnore] public string SystemPrompt { get => CurrentPreset.SystemPrompt; set => CurrentPreset.SystemPrompt = value; }\n }\n\n public class ChatRequest\n {\n [JsonProperty(\"model\")]\n public string Model { get; set; }\n [JsonProperty(\"messages\")]\n public List Messages { get; set; }\n [JsonProperty(\"tools\", NullValueHandling = NullValueHandling.Ignore)]\n public List Tools { get; set; }\n [JsonProperty(\"tool_choice\", NullValueHandling = NullValueHandling.Ignore)]\n public string ToolChoice { get; set; }\n }\n\n public class ChatMessage\n {\n [JsonProperty(\"role\")]\n public string Role { get; set; }\n [JsonProperty(\"content\", NullValueHandling = NullValueHandling.Ignore)]\n public string Content { get; set; }\n [JsonProperty(\"reasoning_content\", NullValueHandling = NullValueHandling.Ignore)]\n public string ReasoningContent { get; set; }\n [JsonProperty(\"tool_calls\", NullValueHandling = NullValueHandling.Ignore)]\n public List ToolCalls { get; set; }\n [JsonProperty(\"tool_call_id\", NullValueHandling = NullValueHandling.Ignore)]\n public string ToolCallId { get; set; }\n [JsonProperty(\"name\", NullValueHandling = NullValueHandling.Ignore)]\n public string Name { get; set; }\n }\n\n public class ChatTool\n {\n [JsonProperty(\"type\")]\n public string Type { get; set; } = \"function\";\n [JsonProperty(\"function\")]\n public ToolFunction Function { get; set; }\n }\n\n public class ToolFunction\n {\n [JsonProperty(\"name\")]\n public string Name { get; set; }\n [JsonProperty(\"description\")]\n public string Description { get; set; }\n [JsonProperty(\"parameters\")]\n public ToolParameters Parameters { get; set; }\n }\n\n public class ToolParameters\n {\n [JsonProperty(\"type\")]\n public string Type { get; set; } = \"object\";\n [JsonProperty(\"properties\")]\n public Dictionary Properties { get; set; }\n [JsonProperty(\"required\")]\n public List Required { get; set; }\n }\n\n public class ToolCall\n {\n [JsonProperty(\"id\")]\n public string Id { get; set; }\n [JsonProperty(\"type\")]\n public string Type { get; set; }\n [JsonProperty(\"function\")]\n public ToolCallFunction Function { get; set; }\n }\n\n public class ToolCallFunction\n {\n [JsonProperty(\"name\")]\n public string Name { get; set; }\n [JsonProperty(\"arguments\")]\n public string Arguments { get; set; }\n }\n\n public class ChatResponse\n {\n [JsonProperty(\"choices\")]\n public List Choices { get; set; }\n [JsonProperty(\"error\")]\n public ApiError Error { get; set; }\n }\n\n public class ChatChoice\n {\n [JsonProperty(\"message\")]\n public ChatMessage Message { get; set; }\n }\n\n public class ApiError\n {\n [JsonProperty(\"message\")]\n public string Message { get; set; }\n }\n\n // --- Custom UI Controls ---\n public class ChatRowPanel : Panel\n {\n [System.Runtime.InteropServices.DllImport(\"user32.dll\")]\n private static extern bool HideCaret(IntPtr hWnd);\n\n private Panel bubblePanel;\n private RichTextBox rtbText;\n private bool isUser;\n private bool isSystem;\n private bool isError;\n\n // Collapsible properties\n private bool isCollapsible = false;\n private bool isExpanded = false;\n private string headerText = \"\";\n private string bodyText = \"\";\n private Panel headerPanel;\n private Button btnToggle;\n\n public ChatRowPanel(string text, bool isUser, bool isSystem, bool isError, int width)\n {\n this.isUser = isUser;\n this.isSystem = isSystem;\n this.isError = isError;\n this.Width = width;\n this.BackColor = Color.Transparent;\n\n // Check if it is a collapsible code block or output log\n if (isSystem && (text.StartsWith(\"[AI 请求执行 C# 代码]:\") || text.StartsWith(\"[执行结果]:\")))\n {\n int firstNewLine = text.IndexOf('\\n');\n if (firstNewLine >= 0)\n {\n headerText = text.Substring(0, firstNewLine).TrimEnd(':');\n bodyText = text.Substring(firstNewLine + 1);\n isCollapsible = !string.IsNullOrWhiteSpace(bodyText);\n }\n else\n {\n headerText = text;\n bodyText = \"\";\n isCollapsible = false;\n }\n isExpanded = false; // Collapsed by default\n }\n\n bubblePanel = new Panel\n {\n BackColor = Color.Transparent,\n Padding = new Padding(10, 8, 10, 8),\n AutoSize = false\n };\n\n rtbText = new RichTextBox\n {\n BorderStyle = BorderStyle.None,\n ScrollBars = RichTextBoxScrollBars.None,\n ReadOnly = true,\n Multiline = true,\n WordWrap = true,\n BackColor = Color.White,\n Font = new Font(\"Microsoft YaHei\", 9F)\n };\n\n rtbText.GotFocus += (s, e) => { HideCaret(rtbText.Handle); };\n rtbText.MouseDown += (s, e) => { HideCaret(rtbText.Handle); };\n\n if (isCollapsible)\n {\n headerPanel = new Panel\n {\n Height = 24,\n BackColor = Color.Transparent,\n Location = new Point(10, 6)\n };\n\n Label lblHeader = new Label\n {\n Text = headerText,\n Font = new Font(\"Microsoft YaHei\", 9F, FontStyle.Bold),\n ForeColor = isError ? Color.FromArgb(180, 40, 40) : Color.FromArgb(80, 80, 80),\n Location = new Point(0, 3),\n AutoSize = true\n };\n headerPanel.Controls.Add(lblHeader);\n\n btnToggle = new Button\n {\n Text = \"▶ 展开\",\n Font = new Font(\"Microsoft YaHei\", 8F),\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.Transparent,\n ForeColor = Color.FromArgb(0, 120, 212),\n Location = new Point(lblHeader.Right + 6, 1),\n Width = 48,\n Height = 22,\n Cursor = Cursors.Hand\n };\n btnToggle.FlatAppearance.BorderSize = 0;\n btnToggle.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 230, 230);\n btnToggle.Click += BtnToggle_Click;\n headerPanel.Controls.Add(btnToggle);\n\n lblHeader.SizeChanged += (s, e) => { btnToggle.Left = lblHeader.Right + 6; };\n\n bubblePanel.Controls.Add(headerPanel);\n\n rtbText.Text = bodyText;\n rtbText.Font = new Font(\"Consolas\", 8.5F);\n rtbText.ForeColor = Color.FromArgb(60, 60, 60);\n rtbText.Location = new Point(10, 32);\n rtbText.Visible = false;\n\n bubblePanel.Controls.Add(rtbText);\n }\n else\n {\n FormatMarkdown(rtbText, text, isUser);\n rtbText.Location = new Point(10, 8);\n bubblePanel.Controls.Add(rtbText);\n }\n\n bubblePanel.Paint += BubblePanel_Paint;\n this.Controls.Add(bubblePanel);\n UpdatePosition(width);\n }\n\n private void BtnToggle_Click(object sender, EventArgs e)\n {\n isExpanded = !isExpanded;\n rtbText.Visible = isExpanded;\n btnToggle.Text = isExpanded ? \"▼ 收起\" : \"▶ 展开\";\n \n UpdatePosition(this.Width);\n \n if (this.Parent is FlowLayoutPanel flp)\n {\n flp.PerformLayout();\n }\n }\n\n private void BubblePanel_Paint(object sender, PaintEventArgs e)\n {\n e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;\n Color backColor;\n if (isError) backColor = Color.FromArgb(253, 231, 233);\n else if (isSystem) backColor = Color.FromArgb(240, 240, 240);\n else if (isUser) backColor = Color.FromArgb(0, 120, 212);\n else backColor = Color.White;\n\n using (GraphicsPath path = GetRoundedPath(new Rectangle(0, 0, bubblePanel.Width - 1, bubblePanel.Height - 1), 8))\n using (SolidBrush brush = new SolidBrush(backColor))\n {\n e.Graphics.FillPath(brush, path);\n }\n\n if (!isUser && !isError)\n {\n using (GraphicsPath path = GetRoundedPath(new Rectangle(0, 0, bubblePanel.Width - 1, bubblePanel.Height - 1), 8))\n using (Pen pen = new Pen(Color.FromArgb(220, 220, 220), 1))\n {\n e.Graphics.DrawPath(pen, path);\n }\n }\n }\n\n private GraphicsPath GetRoundedPath(Rectangle rect, int radius)\n {\n GraphicsPath path = new GraphicsPath();\n int d = radius * 2;\n path.AddArc(rect.X, rect.Y, d, d, 180, 90);\n path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);\n path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);\n path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);\n path.CloseFigure();\n return path;\n }\n\n public void UpdateWidth(int width)\n {\n this.Width = width;\n UpdatePosition(width);\n }\n\n private void UpdatePosition(int width)\n {\n int maxBubbleWidth = Convert.ToInt32(width * 0.85);\n int innerPaddingWidth = 20;\n\n int textWidth = maxBubbleWidth - innerPaddingWidth;\n if (textWidth < 50) textWidth = 50;\n\n // 1. Calculate the final bubble and RichTextBox widths\n Size measuredSize = TextRenderer.MeasureText(rtbText.Text, rtbText.Font, new Size(textWidth - 20, 0), TextFormatFlags.WordBreak);\n \n int finalBubbleWidth;\n if (isCollapsible)\n {\n finalBubbleWidth = maxBubbleWidth;\n }\n else\n {\n finalBubbleWidth = Math.Min(measuredSize.Width + innerPaddingWidth + 10, maxBubbleWidth);\n }\n\n int finalRtbWidth = finalBubbleWidth - innerPaddingWidth;\n if (finalRtbWidth < 50) finalRtbWidth = 50;\n\n rtbText.Width = finalRtbWidth;\n\n // Force handle creation so GetPositionFromCharIndex is completely accurate\n var handle = rtbText.Handle;\n\n // 2. Measure precise text wrapping height natively\n int calculatedHeight = 0;\n if (!string.IsNullOrEmpty(rtbText.Text))\n {\n Point pos = rtbText.GetPositionFromCharIndex(rtbText.Text.Length - 1);\n \n // Fallback if handle isn't fully ready or pos.Y returns 0 unexpectedly for multi-line text\n if (pos.Y == 0 && rtbText.Text.Contains(\"\\n\"))\n {\n int lineCount = rtbText.Lines.Length;\n int listBonus = 0;\n foreach (var line in rtbText.Lines)\n {\n if (line.TrimStart().StartsWith(\"- \") || line.TrimStart().StartsWith(\"* \"))\n {\n listBonus += 15;\n }\n }\n int boldAndFontPadding = 24 + (lineCount * 4) + listBonus;\n calculatedHeight = measuredSize.Height + boldAndFontPadding;\n }\n else\n {\n calculatedHeight = pos.Y + rtbText.Font.Height + 16;\n }\n }\n\n // 3. Set control heights and layout bounds\n if (isCollapsible)\n {\n headerPanel.Width = finalRtbWidth;\n if (isExpanded)\n {\n rtbText.Height = calculatedHeight;\n bubblePanel.Width = maxBubbleWidth;\n bubblePanel.Height = rtbText.Bottom + 8;\n }\n else\n {\n bubblePanel.Width = maxBubbleWidth;\n bubblePanel.Height = headerPanel.Bottom + 6;\n }\n }\n else\n {\n rtbText.Height = calculatedHeight;\n bubblePanel.Width = finalBubbleWidth;\n bubblePanel.Height = rtbText.Height + 16;\n }\n\n Color bubbleColor;\n if (isError) bubbleColor = Color.FromArgb(253, 231, 233);\n else if (isSystem) bubbleColor = Color.FromArgb(240, 240, 240);\n else if (isUser) bubbleColor = Color.FromArgb(0, 120, 212);\n else bubbleColor = Color.White;\n\n rtbText.BackColor = bubbleColor;\n rtbText.ForeColor = isUser ? Color.White : Color.FromArgb(33, 33, 33);\n\n this.Height = bubblePanel.Height + 10;\n\n if (isUser)\n {\n bubblePanel.Left = width - bubblePanel.Width - 5;\n }\n else\n {\n bubblePanel.Left = 5;\n }\n bubblePanel.Top = 5;\n \n bubblePanel.Invalidate();\n }\n\n private static void FormatMarkdown(RichTextBox rtb, string markdown, bool isUser)\n {\n rtb.Text = \"\";\n rtb.SelectionBullet = false;\n rtb.SelectionIndent = 0;\n\n string[] lines = markdown.Split('\\n');\n for (int l = 0; l < lines.Length; l++)\n {\n string line = lines[l];\n \n if (line.TrimStart().StartsWith(\"- \") || line.TrimStart().StartsWith(\"* \"))\n {\n int bulletIndex = line.IndexOf('-');\n if (bulletIndex < 0) bulletIndex = line.IndexOf('*');\n line = line.Substring(bulletIndex + 2);\n rtb.SelectionBullet = true;\n rtb.SelectionIndent = 12;\n }\n else\n {\n rtb.SelectionBullet = false;\n rtb.SelectionIndent = 0;\n }\n\n int i = 0;\n while (i < line.Length)\n {\n if (i + 1 < line.Length && line[i] == '*' && line[i + 1] == '*')\n {\n int next = line.IndexOf(\"**\", i + 2);\n if (next >= 0)\n {\n string boldText = line.Substring(i + 2, next - (i + 2));\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Bold);\n rtb.AppendText(boldText);\n i = next + 2;\n }\n else\n {\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Regular);\n rtb.AppendText(\"**\");\n i += 2;\n }\n }\n else if (line[i] == '`')\n {\n int next = line.IndexOf('`', i + 1);\n if (next >= 0)\n {\n string codeText = line.Substring(i + 1, next - (i + 1));\n \n rtb.SelectionFont = new Font(\"Consolas\", rtb.Font.Size);\n rtb.SelectionBackColor = isUser ? Color.FromArgb(0, 60, 120) : Color.FromArgb(228, 229, 230);\n rtb.SelectionColor = isUser ? Color.FromArgb(200, 230, 255) : Color.FromArgb(190, 50, 50);\n\n rtb.AppendText(codeText);\n \n rtb.SelectionBackColor = rtb.BackColor;\n rtb.SelectionColor = rtb.ForeColor;\n \n i = next + 1;\n }\n else\n {\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Regular);\n rtb.AppendText(\"`\");\n i++;\n }\n }\n else\n {\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Regular);\n rtb.SelectionColor = isUser ? Color.White : Color.FromArgb(33, 33, 33);\n rtb.AppendText(line[i].ToString());\n i++;\n }\n }\n\n if (l < lines.Length - 1)\n {\n rtb.AppendText(Environment.NewLine);\n }\n }\n }\n }\n}\n"}, "9cdb239520d74d34e14ee1600da7f5ae3470e87f": {"t": 1785760380.6516836, "n": 59906, "h": "9e04dab5d8dcad98", "c": "using System;\nusing System.Collections.Generic;\nusing System.Drawing;\nusing System.Drawing.Drawing2D;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Windows.Forms;\nusing Newtonsoft.Json;\nusing PowerPoint = Microsoft.Office.Interop.PowerPoint;\n\nnamespace SlideSCI\n{\n public class AISidebarControl : UserControl\n {\n private FlowLayoutPanel chatFlowLayout;\n \n private Panel inputPanel;\n private TextBox txtInput;\n private Button btnSend;\n private Button btnClear;\n private Button btnSettings;\n private ToolTip toolTip;\n\n private string configPath;\n private AIConfig currentConfig;\n private List conversationHistory = new List();\n private static readonly HttpClient httpClient = new HttpClient();\n private bool isGenerating = false;\n private CancellationTokenSource cts;\n\n public PowerPoint.DocumentWindow AssociatedWindow { get; set; }\n\n public AISidebarControl()\n {\n // Enable TLS 1.2 and TLS 1.3 for secure API calls\n try\n {\n System.Net.ServicePointManager.SecurityProtocol |= \n System.Net.SecurityProtocolType.Tls12 | \n System.Net.SecurityProtocolType.Tls11 |\n (System.Net.SecurityProtocolType)12288; // TLS 1.3\n }\n catch { }\n\n InitializeConfigPath();\n LoadConfig();\n InitializeComponent();\n InitializeSystemMessage();\n }\n\n private void InitializeConfigPath()\n {\n string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);\n string slideSciDir = Path.Combine(appData, \"SlideSCI\");\n if (!Directory.Exists(slideSciDir))\n {\n Directory.CreateDirectory(slideSciDir);\n }\n configPath = Path.Combine(slideSciDir, \"ai_config.json\");\n }\n\n private void LoadConfig()\n {\n if (File.Exists(configPath))\n {\n try\n {\n string json = File.ReadAllText(configPath);\n currentConfig = JsonConvert.DeserializeObject(json);\n\n // Migration: If no presets exist but we have old single-config fields, create a default preset\n if (currentConfig != null && (currentConfig.Presets == null || currentConfig.Presets.Count == 0))\n {\n var dict = JsonConvert.DeserializeObject>(json);\n string oldUrl = dict.ContainsKey(\"ApiUrl\") ? dict[\"ApiUrl\"]?.ToString() : null;\n string oldKey = dict.ContainsKey(\"ApiKey\") ? dict[\"ApiKey\"]?.ToString() : null;\n string oldModel = dict.ContainsKey(\"Model\") ? dict[\"Model\"]?.ToString() : null;\n string oldPrompt = dict.ContainsKey(\"SystemPrompt\") ? dict[\"SystemPrompt\"]?.ToString() : null;\n\n currentConfig.Presets = new List();\n if (!string.IsNullOrEmpty(oldUrl) || !string.IsNullOrEmpty(oldKey))\n {\n currentConfig.Presets.Add(new AIPreset\n {\n Name = \"Default\",\n ApiUrl = oldUrl ?? \"https://api.openai.com/v1\",\n ApiKey = oldKey ?? \"\",\n Model = oldModel ?? \"gpt-4o\",\n SystemPrompt = oldPrompt ?? GetDefaultSystemPrompt()\n });\n currentConfig.CurrentPresetName = \"Default\";\n }\n }\n }\n catch\n {\n currentConfig = new AIConfig();\n }\n }\n else\n {\n currentConfig = new AIConfig();\n }\n\n if (currentConfig == null) currentConfig = new AIConfig();\n\n // Ensure we have some default presets if list is still empty\n if (currentConfig.Presets == null || currentConfig.Presets.Count == 0)\n {\n currentConfig.Presets = new List\n {\n new AIPreset { Name = \"Kimi\", ApiUrl = \"https://api.moonshot.cn/v1\", Model = \"kimi-latest\", SystemPrompt = GetDefaultSystemPrompt() },\n new AIPreset { Name = \"DeepSeek\", ApiUrl = \"https://api.deepseek.com/v1\", Model = \"deepseek-chat\", SystemPrompt = GetDefaultSystemPrompt() },\n new AIPreset { Name = \"OpenAI\", ApiUrl = \"https://api.openai.com/v1\", Model = \"gpt-4o\", SystemPrompt = GetDefaultSystemPrompt() }\n };\n currentConfig.CurrentPresetName = \"Kimi\";\n SaveConfig();\n }\n\n if (string.IsNullOrWhiteSpace(currentConfig.SystemPrompt) ||\n currentConfig.SystemPrompt.Contains(\"Office.MsoShapeType.msoShapeRectangle\") ||\n currentConfig.SystemPrompt.Contains(\"msoShapeRoundRectangle\"))\n {\n currentConfig.SystemPrompt = GetDefaultSystemPrompt();\n SaveConfig();\n }\n }\n\n private void SaveConfig()\n {\n try\n {\n string json = JsonConvert.SerializeObject(currentConfig, Formatting.Indented);\n File.WriteAllText(configPath, json);\n }\n catch (Exception ex)\n {\n MessageBox.Show($\"保存设置失败: {ex.Message}\", \"错误\", MessageBoxButtons.OK, MessageBoxIcon.Error);\n }\n }\n\n private void InitializeComponent()\n {\n this.Dock = DockStyle.Fill;\n this.BackColor = Color.FromArgb(245, 246, 247);\n this.Font = new Font(\"Microsoft YaHei\", 9F, FontStyle.Regular, GraphicsUnit.Point);\n\n toolTip = new ToolTip();\n\n // 1. Input Panel (Docked at Bottom)\n inputPanel = new Panel\n {\n Dock = DockStyle.Bottom,\n Height = 105,\n BackColor = Color.White,\n Padding = new Padding(10, 8, 10, 8)\n };\n inputPanel.Paint += (s, e) =>\n {\n using (Pen pen = new Pen(Color.FromArgb(225, 225, 225), 1))\n {\n e.Graphics.DrawLine(pen, 0, 0, inputPanel.Width, 0);\n }\n };\n\n // Custom border panel for textbox\n Panel txtWrapper = new Panel\n {\n Location = new Point(10, 8),\n Width = this.Width - 20,\n Height = 52,\n BackColor = Color.White,\n Padding = new Padding(4)\n };\n txtWrapper.Paint += (s, e) =>\n {\n Color borderColor = txtInput.Focused ? Color.FromArgb(0, 120, 212) : Color.FromArgb(220, 220, 220);\n using (Pen pen = new Pen(borderColor, 1))\n {\n e.Graphics.DrawRectangle(pen, 0, 0, txtWrapper.Width - 1, txtWrapper.Height - 1);\n }\n };\n\n txtInput = new TextBox\n {\n Multiline = true,\n BorderStyle = BorderStyle.None,\n Dock = DockStyle.Fill,\n Font = new Font(\"Microsoft YaHei\", 9F),\n ScrollBars = ScrollBars.Vertical\n };\n txtInput.GotFocus += (s, e) => txtWrapper.Invalidate();\n txtInput.LostFocus += (s, e) => txtWrapper.Invalidate();\n txtInput.KeyDown += TxtInput_KeyDown;\n txtWrapper.Controls.Add(txtInput);\n inputPanel.Controls.Add(txtWrapper);\n\n // Icon-only Clear button\n btnClear = new Button\n {\n Image = CreateIcon(\"trash\", 16, Color.FromArgb(100, 100, 100)),\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.Transparent,\n Location = new Point(10, 68),\n Width = 28,\n Height = 26,\n Cursor = Cursors.Hand\n };\n btnClear.FlatAppearance.BorderSize = 0;\n btnClear.FlatAppearance.MouseOverBackColor = Color.FromArgb(240, 240, 240);\n btnClear.Click += (s, e) => ClearChat();\n toolTip.SetToolTip(btnClear, \"清空对话\");\n inputPanel.Controls.Add(btnClear);\n\n // Icon-only Settings button\n btnSettings = new Button\n {\n Image = CreateIcon(\"settings\", 16, Color.FromArgb(100, 100, 100)),\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.Transparent,\n Location = new Point(44, 68),\n Width = 28,\n Height = 26,\n Cursor = Cursors.Hand\n };\n btnSettings.FlatAppearance.BorderSize = 0;\n btnSettings.FlatAppearance.MouseOverBackColor = Color.FromArgb(240, 240, 240);\n btnSettings.Click += BtnSettings_Click;\n toolTip.SetToolTip(btnSettings, \"AI 设置\");\n inputPanel.Controls.Add(btnSettings);\n\n btnSend = new Button\n {\n Text = \"发送\",\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.FromArgb(0, 120, 212),\n ForeColor = Color.White,\n Font = new Font(\"Microsoft YaHei\", 9F, FontStyle.Bold),\n Location = new Point(this.Width - 75, 68),\n Width = 65,\n Height = 26,\n Cursor = Cursors.Hand\n };\n btnSend.FlatAppearance.BorderSize = 0;\n btnSend.Click += async (s, e) => await HandleSendAsync();\n inputPanel.Controls.Add(btnSend);\n\n this.Controls.Add(inputPanel);\n\n // 2. Chat Flow Layout Panel (Docked Fill)\n chatFlowLayout = new FlowLayoutPanel\n {\n Dock = DockStyle.Fill,\n AutoScroll = true,\n FlowDirection = FlowDirection.LeftToRight,\n WrapContents = true,\n Padding = new Padding(8),\n BackColor = Color.FromArgb(245, 246, 247)\n };\n typeof(FlowLayoutPanel).InvokeMember(\"DoubleBuffered\",\n System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,\n null, chatFlowLayout, new object[] { true });\n\n this.Controls.Add(chatFlowLayout);\n\n // Ensure Z-order prevents bottom layout overlapping\n chatFlowLayout.BringToFront();\n\n this.Resize += AISidebarControl_Resize;\n chatFlowLayout.Resize += (s, e) => UpdateBubbleWidths();\n }\n\n private void AISidebarControl_Resize(object sender, EventArgs e)\n {\n if (inputPanel != null)\n {\n foreach (Control ctrl in inputPanel.Controls)\n {\n if (ctrl is Panel p) // txtWrapper\n {\n p.Width = inputPanel.Width - 20;\n }\n else if (ctrl == btnSend)\n {\n btnSend.Left = inputPanel.Width - 75;\n }\n }\n }\n UpdateBubbleWidths();\n }\n\n private void BtnSettings_Click(object sender, EventArgs e)\n {\n using (var settingsForm = new AISettingsForm(currentConfig))\n {\n if (settingsForm.ShowDialog(this) == DialogResult.OK)\n {\n settingsForm.UpdateConfig(currentConfig);\n SaveConfig();\n\n // Update the active system message in the conversation history\n if (conversationHistory.Count > 0 && conversationHistory[0].Role == \"system\")\n {\n conversationHistory[0].Content = currentConfig.SystemPrompt;\n }\n }\n }\n }\n\n private void InitializeSystemMessage()\n {\n string systemPrompt = currentConfig.SystemPrompt;\n if (string.IsNullOrWhiteSpace(systemPrompt))\n {\n systemPrompt = GetDefaultSystemPrompt();\n currentConfig.SystemPrompt = systemPrompt;\n }\n\n conversationHistory.Add(new ChatMessage { Role = \"system\", Content = systemPrompt });\n AppendBubble(\"你好!我是你的 SlideSCI AI 助手。我可以执行以下命令:\\n1. 把选中的文字或文本框颜色改为红色、蓝色等。\\n2. 添加各种形状(矩形、圆形等)。\\n3. 新增一页空白幻灯片。\\n4. 将幻灯片中的指定文字进行查找替换。\\n\\n请在下方输入你的指令!\", isUser: false, isSystem: false);\n }\n\n public static string GetDefaultSystemPrompt()\n {\n return @\"你是一个PowerPoint AI助手,你可以通过编写和执行C#代码来帮用户操作当前幻灯片。\n你有调用 `execute_csharp_code` 工具的能力。\n当你需要对幻灯片进行操作时(如修改文字颜色、添加形状、新建页面、替换文字等),你应该写C#代码,然后通过调用 `execute_csharp_code` 来执行。\n\n编写C#代码时请遵守以下规则:\n1. 代码会在一个包含 `PowerPoint.Application Application` 参数的方法体内执行:`public static void Run(PowerPoint.Application Application)`。\n2. 已经默认导入了以下命名空间,请勿再在代码中写 using 声明:\n - System\n - System.Collections.Generic\n - System.Linq\n - System.Drawing\n - System.Windows.Forms\n - Microsoft.Office.Interop.PowerPoint (别名为 PowerPoint)\n - Microsoft.Office.Core (别名为 Office)\n3. 安全获取当前幻灯片对象 (Slide) 的方法:\n PowerPoint.Slide slide = null;\n try { slide = Application.ActiveWindow.View.Slide as PowerPoint.Slide; } catch {}\n if (slide == null) {\n try { slide = Application.ActivePresentation.Slides[Application.ActiveWindow.Selection.SlideRange[1].SlideIndex]; } catch {}\n }\n if (slide == null) {\n throw new Exception(\"\"没有找到活动的幻灯片,请确保PPT处于正常视图。\"\");\n }\n4. 新增幻灯片页面请直接对 Application.ActivePresentation.Slides 进行 Add 或 Add2 操作,例如:\n var pres = Application.ActivePresentation;\n pres.Slides.Add(pres.Slides.Count + 1, PowerPoint.PpSlideLayout.ppLayoutBlank);\n5. 操作文字颜色时,请使用 `System.Drawing.ColorTranslator.ToOle(Color.Red)` 将颜色转换为 OLE 颜色。例如:\n shape.TextFrame.TextRange.Font.Color.RGB = ColorTranslator.ToOle(Color.Red);\n6. 插入形状时,第一个参数必须是 `Office.MsoAutoShapeType` 枚举类型(注意:千万不可写错成 `Office.MsoShapeType`,否则会报编译错误)。常用的形状枚举如下:\n - 矩形:`Office.MsoAutoShapeType.msoShapeRectangle`\n - 圆角矩形(注意是 Rounded,不是 Round):`Office.MsoAutoShapeType.msoShapeRoundedRectangle`\n - 椭圆/圆形:`Office.MsoAutoShapeType.msoShapeOval`\n 示例:`slide.Shapes.AddShape(Office.MsoAutoShapeType.msoShapeRectangle, left, top, width, height)`。位置与尺寸参数皆为 float 类型。\n7. 修改文字内容时,如果需要查找并替换,可以遍历幻灯片中的 Shape 并检查 `shape.HasTextFrame == Office.MsoTriState.msoTrue && shape.TextFrame.HasText == Office.MsoTriState.msoTrue`。\n8. 请尽量编写健壮的代码,用 try-catch 包裹可能出错的地方,遇到不支持的操作时抛出异常。\n\n【核心PPT设计审美与排版规范(PPT Aesthetics & Layout Design Rules)】\n为了确保AI添加的PPT样式美观、专业且符合现代高端设计规范,请在编写代码生成/修改PPT内容时,严格遵守以下排版与审美原则:\n\n1. 配色系统与 60-30-10 黄金比例:\n - 禁止在没有用户明确要求的情况下使用刺眼的高饱和度纯原色(例如纯红 Color.Red、纯蓝 Color.Blue、纯绿 Color.Green、纯黄 Color.Yellow)。这会让 PPT 显得极不专业。\n - 使用成熟、高对比度且优雅的经典主题配色(使用 Color.FromArgb 定义颜色,然后使用 ColorTranslator.ToOle 转换):\n * 极简科技蓝主题:\n - 主背景/主色:浅灰白 Color.FromArgb(248, 249, 250) 或 极深蓝 Color.FromArgb(10, 34, 64)\n - 辅助色/卡片背景:浅天蓝 Color.FromArgb(232, 240, 254) 或 经典蓝 Color.FromArgb(26, 115, 232)\n - 点缀强调色(10%):温暖橙/金色 Color.FromArgb(245, 166, 35) 或 珊瑚红 Color.FromArgb(234, 67, 53)\n * 现代商务炭黑主题:\n - 主色:炭黑色 Color.FromArgb(43, 43, 43)\n - 辅助色:浅灰色 Color.FromArgb(240, 240, 240)\n - 点缀色:青/湖蓝色 Color.FromArgb(0, 168, 181)\n * 暖调人文/学术主题:\n - 主色:深林绿 Color.FromArgb(27, 67, 50)\n - 辅助色:柔和米色 Color.FromArgb(245, 242, 235)\n - 点缀色:赤陶红/土黄 Color.FromArgb(190, 130, 60)\n - 遵守 60-30-10 比例:背景占 60% 视觉面积,文字/卡片容器等次要元素占 30%,画龙点睛的重点高亮(如关键数字、重点结论词、边框线装饰)占 10%。\n\n2. 视觉层级与字号梯度(Visual Hierarchy & Text Scales):\n - 建立清晰鲜明的字号差,中文默认使用“微软雅黑” (Microsoft YaHei),英文默认使用“Segoe UI”:\n * 幻灯片大标题(Slide Title):36pt - 44pt,粗体 (Bold),深色高对比度。\n * 章节/卡片小标题(Card Header):20pt - 24pt,粗体。\n * 正文内容(Body Text):14pt - 18pt,常规体 (Regular),中等对比度(如深灰)。\n * 关键数据/大数字(Big Stat/Metric):48pt - 72pt 粗体,可搭配 12pt 的解释性小字。\n - 为确保文字排版完美,对所有新创建的文本框设置以下属性以防文本溢出或不对齐:\n * shape.TextFrame.WordWrap = Office.MsoTriState.msoTrue; // 开启自动换行\n * 适当清除或缩小默认内边距:\n shape.TextFrame.MarginLeft = 10f;\n shape.TextFrame.MarginRight = 10f;\n shape.TextFrame.MarginTop = 10f;\n shape.TextFrame.MarginBottom = 10f;\n\n3. 负空间与留白(Negative Space & Whitespace):\n - 不要把幻灯片塞满!页面必须保留至少 30% - 40% 的空白区域,让观众的眼睛有呼吸的空间。\n - 绝不允许使用大段文字,请将文字提炼为短句,以列表或卡片形式展现。\n\n4. 动态网格布局与防重叠(Dynamic Grid Layout & Math Positioning):\n - 永远通过代码动态获取幻灯片的物理大小,并在此基础上进行百分比定位或网格分栏计算,严禁使用写死的绝对坐标导致在不同比例的PPT中排版错乱:\n float slideWidth = Application.ActivePresentation.PageSetup.SlideWidth;\n float slideHeight = Application.ActivePresentation.PageSetup.SlideHeight;\n - 留出四周安全边距(Margin,通常设为 slideWidth * 0.06f 与 slideHeight * 0.08f)。\n - 如果需要展示并列的多个观点,应自动计算分栏位置:\n * 两栏布局:左栏占宽 45%,右栏占宽 45%,中间留空 10%。\n * 三分栏卡片布局示例:\n float margin = slideWidth * 0.05f;\n float availableWidth = slideWidth - 2 * margin;\n float gap = 20f; // 卡片间距\n float cardWidth = (availableWidth - 2 * gap) / 3f;\n float cardHeight = slideHeight * 0.5f;\n float top = slideHeight * 0.3f;\n for (int i = 0; i < 3; i++) {\n float left = margin + i * (cardWidth + gap);\n // 在 left, top, cardWidth, cardHeight 处绘制卡片容器\n }\n - 通过数学计算绝对保证生成的文本框、容器形状之间不会重叠。\n\n5. 扁平化容器卡片化(Flat Card-Based Design):\n - 列表或并列观点应使用“浅色卡片”作为容器装载,卡片背景填充非常浅的颜色(如 RGB: 245, 245, 245),并且必须去掉边框轮廓以符合现代扁平化审美:\n cardShape.Line.Visible = Office.MsoTriState.msoFalse;\n cardShape.Fill.ForeColor.RGB = ColorTranslator.ToOle(Color.FromArgb(245, 245, 245));\n - 在卡片之上叠放文字形状时,通过 Y 坐标偏移(如 cardTop + 15 放置标题,cardTop + 45 放置正文)使排版整洁大方。\n - 尽量使用无轮廓(`Line.Visible = msoFalse`)的形状,如果使用线条,应保持极细(`Line.Weight = 1.0f`)且使用温和的淡灰色。\";\n }\n\n private void TxtInput_KeyDown(object sender, KeyEventArgs e)\n {\n if (e.KeyCode == Keys.Enter && !e.Shift)\n {\n e.SuppressKeyPress = true; // Prevent newline\n btnSend.PerformClick();\n }\n }\n\n private void ClearChat()\n {\n chatFlowLayout.Controls.Clear();\n conversationHistory.Clear();\n InitializeSystemMessage();\n }\n\n private void AppendBubble(string text, bool isUser, bool isSystem, bool isError = false)\n {\n // Strip emojis from the text to prevent square box characters in RichTextBox\n text = StripEmojis(text);\n\n ChatRowPanel row = new ChatRowPanel(text, isUser, isSystem, isError, chatFlowLayout.Width - 25);\n chatFlowLayout.Controls.Add(row);\n \n chatFlowLayout.PerformLayout();\n chatFlowLayout.ScrollControlIntoView(row);\n \n try\n {\n chatFlowLayout.VerticalScroll.Value = chatFlowLayout.VerticalScroll.Maximum;\n }\n catch {}\n }\n\n private static string StripEmojis(string text)\n {\n if (string.IsNullOrEmpty(text)) return text;\n try\n {\n // Remove surrogate pairs and symbol ranges (emojis, icons, private use areas)\n return Regex.Replace(text, @\"\\p{Cs}|\\p{So}|\\p{Cn}\", \"\");\n }\n catch\n {\n return text;\n }\n }\n\n private void UpdateBubbleWidths()\n {\n if (chatFlowLayout == null) return;\n chatFlowLayout.SuspendLayout();\n int targetWidth = chatFlowLayout.Width - 25;\n if (targetWidth < 100) targetWidth = 100;\n foreach (Control ctrl in chatFlowLayout.Controls)\n {\n if (ctrl is ChatRowPanel row)\n {\n row.UpdateWidth(targetWidth);\n }\n }\n chatFlowLayout.ResumeLayout(true);\n }\n\n private async Task HandleSendAsync()\n {\n if (isGenerating)\n {\n cts?.Cancel();\n return;\n }\n\n string message = txtInput.Text.Trim();\n if (string.IsNullOrEmpty(message)) return;\n\n if (string.IsNullOrEmpty(currentConfig.ApiKey))\n {\n MessageBox.Show(\"请先配置 API Key!\", \"提示\", MessageBoxButtons.OK, MessageBoxIcon.Warning);\n BtnSettings_Click(null, null);\n return;\n }\n\n txtInput.Text = \"\";\n AppendBubble(message, isUser: true, isSystem: false);\n conversationHistory.Add(new ChatMessage { Role = \"user\", Content = message });\n\n isGenerating = true;\n btnSend.Text = \"停止\";\n btnSend.BackColor = Color.FromArgb(209, 52, 56);\n txtInput.Enabled = false;\n btnClear.Enabled = false;\n btnSettings.Enabled = false;\n\n cts = new CancellationTokenSource();\n\n try\n {\n await RunAgentLoopAsync(cts.Token);\n }\n catch (OperationCanceledException)\n {\n AppendBubble(\"[已终止]: 用户中止了 AI 响应。\", isUser: false, isSystem: true, isError: false);\n }\n catch (Exception ex)\n {\n AppendBubble($\"[AI 调用出错]:\\n{ex.ToString()}\", isUser: false, isSystem: true, isError: true);\n }\n finally\n {\n isGenerating = false;\n btnSend.Text = \"发送\";\n btnSend.BackColor = Color.FromArgb(0, 120, 212);\n \n txtInput.Enabled = true;\n btnClear.Enabled = true;\n btnSettings.Enabled = true;\n\n cts?.Dispose();\n cts = null;\n }\n }\n\n private async Task RunAgentLoopAsync(CancellationToken token)\n {\n int maxIterations = 5;\n int currentIteration = 0;\n\n var tools = new List\n {\n new ChatTool\n {\n Function = new ToolFunction\n {\n Name = \"execute_csharp_code\",\n Description = \"在幻灯片中动态编译并执行C#代码来操作系统。代码将被放置在 `public static void Run(PowerPoint.Application Application)` 方法体中运行。你可以使用任何 PowerPoint API 进行操作。\",\n Parameters = new ToolParameters\n {\n Properties = new Dictionary\n {\n {\n \"code\", new Dictionary\n {\n { \"type\", \"string\" },\n { \"description\", \"要执行的 C# 语句。必须使用合法的 C# 语法,不用声明 class 或 method,只写方法体内部的执行代码。\" }\n }\n }\n },\n Required = new List { \"code\" }\n }\n }\n }\n };\n\n while (currentIteration < maxIterations)\n {\n token.ThrowIfCancellationRequested();\n currentIteration++;\n\n var request = new ChatRequest\n {\n Model = currentConfig.Model,\n Messages = conversationHistory,\n Tools = tools,\n ToolChoice = \"auto\"\n };\n\n string requestJson = JsonConvert.SerializeObject(request);\n var content = new StringContent(requestJson, Encoding.UTF8, \"application/json\");\n\n httpClient.DefaultRequestHeaders.Clear();\n httpClient.DefaultRequestHeaders.Add(\"Authorization\", $\"Bearer {currentConfig.ApiKey}\");\n\n string url = currentConfig.ApiUrl.TrimEnd('/') + \"/chat/completions\";\n \n HttpResponseMessage response;\n try\n {\n response = await httpClient.PostAsync(url, content, token);\n }\n catch (OperationCanceledException)\n {\n throw;\n }\n catch (Exception ex)\n {\n throw new Exception($\"无法连接到 AI 服务,网络连接异常: {ex.Message}\\n请求地址: {url}\\n\\n详细信息:\\n{ex.ToString()}\");\n }\n\n token.ThrowIfCancellationRequested();\n string responseJson = await response.Content.ReadAsStringAsync();\n if (!response.IsSuccessStatusCode)\n {\n string errorDetails = \"\";\n try\n {\n var errRes = JsonConvert.DeserializeObject(responseJson);\n errorDetails = errRes?.Error?.Message;\n }\n catch {}\n if (string.IsNullOrEmpty(errorDetails))\n {\n errorDetails = responseJson;\n }\n throw new Exception($\"API 请求失败 (HTTP {response.StatusCode} - {response.ReasonPhrase}):\\n{errorDetails}\");\n }\n\n var chatRes = JsonConvert.DeserializeObject(responseJson);\n if (chatRes == null || chatRes.Choices == null || chatRes.Choices.Count == 0)\n {\n throw new Exception(\"API 返回了空响应,无法处理。\");\n }\n\n var choiceMessage = chatRes.Choices[0].Message;\n conversationHistory.Add(choiceMessage);\n\n if (!string.IsNullOrEmpty(choiceMessage.Content))\n {\n AppendBubble(choiceMessage.Content, isUser: false, isSystem: false);\n }\n\n if (choiceMessage.ToolCalls != null && choiceMessage.ToolCalls.Count > 0)\n {\n token.ThrowIfCancellationRequested();\n foreach (var toolCall in choiceMessage.ToolCalls)\n {\n if (toolCall.Function.Name == \"execute_csharp_code\")\n {\n string code = \"\";\n try\n {\n var args = JsonConvert.DeserializeObject>(toolCall.Function.Arguments);\n if (args != null && args.TryGetValue(\"code\", out string argCode))\n {\n code = argCode;\n }\n }\n catch (Exception ex)\n {\n string toolErr = $\"解析参数失败: {ex.Message}\";\n conversationHistory.Add(new ChatMessage\n {\n Role = \"tool\",\n Name = \"execute_csharp_code\",\n Content = toolErr,\n ToolCallId = toolCall.Id\n });\n AppendBubble($\"[工具调用解析失败]\\n{toolErr}\", isUser: false, isSystem: true, isError: true);\n continue;\n }\n\n AppendBubble($\"[AI 请求执行 C# 代码]:\\n{code}\", isUser: false, isSystem: true);\n\n string runResult = \"\";\n try\n {\n runResult = CSharpExecutor.Execute(code, Globals.ThisAddIn.Application);\n }\n catch (Exception ex)\n {\n runResult = $\"Critical Runtime Error: {ex.Message}\";\n }\n\n conversationHistory.Add(new ChatMessage\n {\n Role = \"tool\",\n Name = \"execute_csharp_code\",\n Content = runResult,\n ToolCallId = toolCall.Id\n });\n\n if (runResult == \"Success\")\n {\n AppendBubble(\"[执行结果]: 成功!\", isUser: false, isSystem: true);\n }\n else\n {\n AppendBubble($\"[执行结果]: 失败!\\n{runResult}\", isUser: false, isSystem: true, isError: true);\n }\n }\n }\n continue;\n }\n\n break;\n }\n }\n\n public static Bitmap CreateIcon(string type, int size, Color color)\n {\n Bitmap bmp = new Bitmap(size, size);\n using (Graphics g = Graphics.FromImage(bmp))\n {\n g.SmoothingMode = SmoothingMode.AntiAlias;\n g.Clear(Color.Transparent);\n using (Pen pen = new Pen(color, 1.8f))\n using (Brush brush = new SolidBrush(color))\n {\n if (type == \"settings\")\n {\n int r1 = size / 2 - 1;\n int r2 = size / 2 - 4;\n int cx = size / 2;\n int cy = size / 2;\n g.DrawEllipse(pen, cx - r2, cy - r2, r2 * 2, r2 * 2);\n \n for (int i = 0; i < 8; i++)\n {\n double angle = i * Math.PI / 4;\n float x1 = (float)(cx + r2 * Math.Cos(angle));\n float y1 = (float)(cy + r2 * Math.Sin(angle));\n float x2 = (float)(cx + r1 * Math.Cos(angle));\n float y2 = (float)(cy + r1 * Math.Sin(angle));\n g.DrawLine(pen, x1, y1, x2, y2);\n }\n }\n else if (type == \"trash\")\n {\n g.DrawRectangle(pen, 4, 5, size - 9, size - 8);\n g.DrawLine(pen, 2, 3, size - 3, 3);\n g.DrawLine(pen, 6, 1, size - 7, 1);\n g.DrawLine(pen, 7, 7, 7, size - 5);\n g.DrawLine(pen, size / 2, 7, size / 2, size - 5);\n g.DrawLine(pen, size - 8, 7, size - 8, size - 5);\n }\n }\n }\n return bmp;\n }\n }\n\n // --- Settings Dialog Form ---\n public class AISettingsForm : Form\n {\n private ComboBox cmbPresets;\n private Button btnAddPreset;\n private Button btnDeletePreset;\n private TextBox txtPresetName;\n private TextBox txtApiUrl;\n private TextBox txtApiKey;\n private CheckBox chkShowKey;\n private TextBox txtModel;\n private TextBox txtSystemPrompt;\n private Button btnSave;\n private Button btnCancel;\n private Button btnRestoreDefault;\n private AIConfig config;\n private List tempPresets;\n private string activePresetName;\n private AIPreset currentEditingPreset = null;\n\n public AISettingsForm(AIConfig currentConfig)\n {\n this.config = currentConfig;\n this.tempPresets = new List();\n foreach (var p in currentConfig.Presets)\n tempPresets.Add(new AIPreset { Name = p.Name, ApiUrl = p.ApiUrl, ApiKey = p.ApiKey, Model = p.Model, SystemPrompt = p.SystemPrompt });\n this.activePresetName = currentConfig.CurrentPresetName;\n InitializeComponent();\n LoadPresetsToCombo();\n }\n\n private void InitializeComponent()\n {\n this.Text = \"AI 助手设置\";\n this.Size = new Size(620, 580);\n this.FormBorderStyle = FormBorderStyle.FixedDialog;\n this.MaximizeBox = false;\n this.MinimizeBox = false;\n this.StartPosition = FormStartPosition.CenterParent;\n this.Font = new Font(\"Microsoft YaHei\", 9F);\n this.BackColor = Color.White;\n\n Label lblSelectPreset = new Label { Text = \"选择预设:\", Location = new Point(15, 20), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n cmbPresets = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Location = new Point(100, 20), Width = 280, Height = 24 };\n\n btnAddPreset = new Button { Text = \"新增\", Location = new Point(390, 18), Width = 90, Height = 28, BackColor = Color.FromArgb(240, 240, 240), FlatStyle = FlatStyle.Flat };\n btnAddPreset.FlatAppearance.BorderColor = Color.LightGray;\n btnDeletePreset = new Button { Text = \"删除\", Location = new Point(490, 18), Width = 90, Height = 28, BackColor = Color.FromArgb(240, 240, 240), FlatStyle = FlatStyle.Flat };\n btnDeletePreset.FlatAppearance.BorderColor = Color.LightGray;\n\n Label lblPresetName = new Label { Text = \"预设名称:\", Location = new Point(15, 60), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtPresetName = new TextBox { Location = new Point(100, 60), Width = 480 };\n txtPresetName.TextChanged += TxtPresetName_TextChanged;\n\n Label lblUrl = new Label { Text = \"API 地址:\", Location = new Point(15, 100), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtApiUrl = new TextBox { Location = new Point(100, 100), Width = 480 };\n\n Label lblKey = new Label { Text = \"API Key:\", Location = new Point(15, 140), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtApiKey = new TextBox { Location = new Point(100, 140), Width = 400, PasswordChar = '*' };\n chkShowKey = new CheckBox { Text = \"显示\", Location = new Point(510, 140), Width = 70, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n chkShowKey.CheckedChanged += (s, e) => { txtApiKey.PasswordChar = chkShowKey.Checked ? '\\0' : '*'; };\n\n Label lblModel = new Label { Text = \"模型名称:\", Location = new Point(15, 180), Width = 80, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtModel = new TextBox { Location = new Point(100, 180), Width = 480 };\n\n Label lblPrompt = new Label { Text = \"系统提示词:\", Location = new Point(15, 220), Width = 85, Height = 22, TextAlign = ContentAlignment.MiddleLeft };\n txtSystemPrompt = new TextBox { Location = new Point(100, 220), Width = 480, Height = 240, Multiline = true, ScrollBars = ScrollBars.Vertical, Font = new Font(\"Microsoft YaHei\", 9F) };\n\n btnRestoreDefault = new Button { Text = \"恢复默认提示词\", Location = new Point(100, 485), Width = 150, Height = 28, FlatStyle = FlatStyle.Flat };\n btnRestoreDefault.FlatAppearance.BorderColor = Color.LightGray;\n btnRestoreDefault.Click += (s, e) => { txtSystemPrompt.Text = AISidebarControl.GetDefaultSystemPrompt(); };\n\n btnSave = new Button { Text = \"保存\", DialogResult = DialogResult.OK, Location = new Point(380, 485), Width = 90, Height = 28, BackColor = Color.FromArgb(0, 120, 212), ForeColor = Color.White, FlatStyle = FlatStyle.Flat };\n btnSave.FlatAppearance.BorderSize = 0;\n btnCancel = new Button { Text = \"取消\", DialogResult = DialogResult.Cancel, Location = new Point(490, 485), Width = 90, Height = 28, FlatStyle = FlatStyle.Flat };\n btnCancel.FlatAppearance.BorderColor = Color.LightGray;\n\n this.Controls.AddRange(new Control[] {\n lblSelectPreset, cmbPresets, btnAddPreset, btnDeletePreset,\n lblPresetName, txtPresetName, lblUrl, txtApiUrl,\n lblKey, txtApiKey, chkShowKey, lblModel, txtModel,\n lblPrompt, txtSystemPrompt, btnRestoreDefault, btnSave, btnCancel\n });\n this.AcceptButton = btnSave;\n this.CancelButton = btnCancel;\n btnAddPreset.Click += BtnAddPreset_Click;\n btnDeletePreset.Click += BtnDeletePreset_Click;\n }\n\n private void LoadPresetsToCombo()\n {\n cmbPresets.SelectedIndexChanged -= CmbPresets_SelectedIndexChanged;\n cmbPresets.DataSource = null;\n cmbPresets.DataSource = tempPresets;\n cmbPresets.DisplayMember = \"Name\";\n int idx = tempPresets.FindIndex(p => p.Name == activePresetName);\n if (idx < 0 && tempPresets.Count > 0) idx = 0;\n cmbPresets.SelectedIndex = idx;\n if (idx >= 0) { LoadPresetToFields(tempPresets[idx]); activePresetName = tempPresets[idx].Name; }\n cmbPresets.SelectedIndexChanged += CmbPresets_SelectedIndexChanged;\n }\n\n private void SaveCurrentPresetEdits()\n {\n if (currentEditingPreset != null)\n {\n currentEditingPreset.Name = txtPresetName.Text.Trim();\n currentEditingPreset.ApiUrl = txtApiUrl.Text.Trim();\n currentEditingPreset.ApiKey = txtApiKey.Text.Trim();\n currentEditingPreset.Model = txtModel.Text.Trim();\n currentEditingPreset.SystemPrompt = txtSystemPrompt.Text;\n }\n }\n\n private void LoadPresetToFields(AIPreset preset)\n {\n currentEditingPreset = preset;\n if (preset != null)\n {\n txtPresetName.Text = preset.Name;\n txtApiUrl.Text = preset.ApiUrl;\n txtApiKey.Text = preset.ApiKey;\n txtModel.Text = preset.Model;\n txtSystemPrompt.Text = preset.SystemPrompt;\n }\n }\n\n private void CmbPresets_SelectedIndexChanged(object sender, EventArgs e)\n {\n SaveCurrentPresetEdits();\n if (cmbPresets.SelectedIndex >= 0)\n {\n var selected = cmbPresets.SelectedItem as AIPreset;\n LoadPresetToFields(selected);\n this.activePresetName = selected?.Name ?? \"\";\n }\n }\n\n private void TxtPresetName_TextChanged(object sender, EventArgs e)\n {\n if (currentEditingPreset != null && cmbPresets.SelectedItem == currentEditingPreset)\n {\n string newName = txtPresetName.Text.Trim();\n if (!string.IsNullOrEmpty(newName) && currentEditingPreset.Name != newName)\n {\n currentEditingPreset.Name = newName;\n int idx = cmbPresets.SelectedIndex;\n cmbPresets.SelectedIndexChanged -= CmbPresets_SelectedIndexChanged;\n cmbPresets.DataSource = null;\n cmbPresets.DataSource = tempPresets;\n cmbPresets.DisplayMember = \"Name\";\n cmbPresets.SelectedIndex = idx;\n cmbPresets.SelectedIndexChanged += CmbPresets_SelectedIndexChanged;\n }\n }\n }\n\n private void BtnAddPreset_Click(object sender, EventArgs e)\n {\n SaveCurrentPresetEdits();\n int count = 1;\n string newName = \"New Preset\";\n while (tempPresets.Exists(p => p.Name == $\"{newName} {count}\")) count++;\n string finalName = $\"{newName} {count}\";\n var newPreset = new AIPreset { Name = finalName, ApiUrl = \"https://api.openai.com/v1\", ApiKey = \"\", Model = \"gpt-4o\", SystemPrompt = AISidebarControl.GetDefaultSystemPrompt() };\n tempPresets.Add(newPreset);\n activePresetName = finalName;\n LoadPresetsToCombo();\n }\n\n private void BtnDeletePreset_Click(object sender, EventArgs e)\n {\n if (tempPresets.Count <= 1) { MessageBox.Show(\"必须保留至少一个预设!\", \"提示\", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }\n if (cmbPresets.SelectedItem is AIPreset selected)\n {\n if (MessageBox.Show($\"确定删除预设 '{selected.Name}' 吗?\", \"确认删除\", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)\n {\n tempPresets.Remove(selected);\n currentEditingPreset = null;\n activePresetName = tempPresets[0].Name;\n LoadPresetsToCombo();\n }\n }\n }\n\n public void UpdateConfig(AIConfig targetConfig)\n {\n SaveCurrentPresetEdits();\n targetConfig.Presets = this.tempPresets;\n targetConfig.CurrentPresetName = this.activePresetName;\n }\n }\n\n // --- Data Models for API ---\n public class AIPreset\n {\n public string Name { get; set; } = \"Default\";\n public string ApiUrl { get; set; } = \"https://api.openai.com/v1\";\n public string ApiKey { get; set; } = \"\";\n public string Model { get; set; } = \"gpt-4o\";\n public string SystemPrompt { get; set; } = \"\";\n public override string ToString() => Name;\n }\n\n public class AIConfig\n {\n public List Presets { get; set; } = new List();\n public string CurrentPresetName { get; set; } = \"\";\n\n [JsonIgnore]\n public AIPreset CurrentPreset\n {\n get\n {\n if (Presets == null || Presets.Count == 0)\n {\n Presets = new List { new AIPreset { Name = \"Default\" } };\n CurrentPresetName = \"Default\";\n }\n var active = Presets.Find(p => p.Name == CurrentPresetName);\n if (active == null) { active = Presets[0]; CurrentPresetName = active.Name; }\n return active;\n }\n }\n\n [JsonIgnore] public string ApiUrl { get => CurrentPreset.ApiUrl; set => CurrentPreset.ApiUrl = value; }\n [JsonIgnore] public string ApiKey { get => CurrentPreset.ApiKey; set => CurrentPreset.ApiKey = value; }\n [JsonIgnore] public string Model { get => CurrentPreset.Model; set => CurrentPreset.Model = value; }\n [JsonIgnore] public string SystemPrompt { get => CurrentPreset.SystemPrompt; set => CurrentPreset.SystemPrompt = value; }\n }\n\n public class ChatRequest\n {\n [JsonProperty(\"model\")]\n public string Model { get; set; }\n [JsonProperty(\"messages\")]\n public List Messages { get; set; }\n [JsonProperty(\"tools\", NullValueHandling = NullValueHandling.Ignore)]\n public List Tools { get; set; }\n [JsonProperty(\"tool_choice\", NullValueHandling = NullValueHandling.Ignore)]\n public string ToolChoice { get; set; }\n }\n\n public class ChatMessage\n {\n [JsonProperty(\"role\")]\n public string Role { get; set; }\n [JsonProperty(\"content\", NullValueHandling = NullValueHandling.Ignore)]\n public string Content { get; set; }\n [JsonProperty(\"reasoning_content\", NullValueHandling = NullValueHandling.Ignore)]\n public string ReasoningContent { get; set; }\n [JsonProperty(\"tool_calls\", NullValueHandling = NullValueHandling.Ignore)]\n public List ToolCalls { get; set; }\n [JsonProperty(\"tool_call_id\", NullValueHandling = NullValueHandling.Ignore)]\n public string ToolCallId { get; set; }\n [JsonProperty(\"name\", NullValueHandling = NullValueHandling.Ignore)]\n public string Name { get; set; }\n }\n\n public class ChatTool\n {\n [JsonProperty(\"type\")]\n public string Type { get; set; } = \"function\";\n [JsonProperty(\"function\")]\n public ToolFunction Function { get; set; }\n }\n\n public class ToolFunction\n {\n [JsonProperty(\"name\")]\n public string Name { get; set; }\n [JsonProperty(\"description\")]\n public string Description { get; set; }\n [JsonProperty(\"parameters\")]\n public ToolParameters Parameters { get; set; }\n }\n\n public class ToolParameters\n {\n [JsonProperty(\"type\")]\n public string Type { get; set; } = \"object\";\n [JsonProperty(\"properties\")]\n public Dictionary Properties { get; set; }\n [JsonProperty(\"required\")]\n public List Required { get; set; }\n }\n\n public class ToolCall\n {\n [JsonProperty(\"id\")]\n public string Id { get; set; }\n [JsonProperty(\"type\")]\n public string Type { get; set; }\n [JsonProperty(\"function\")]\n public ToolCallFunction Function { get; set; }\n }\n\n public class ToolCallFunction\n {\n [JsonProperty(\"name\")]\n public string Name { get; set; }\n [JsonProperty(\"arguments\")]\n public string Arguments { get; set; }\n }\n\n public class ChatResponse\n {\n [JsonProperty(\"choices\")]\n public List Choices { get; set; }\n [JsonProperty(\"error\")]\n public ApiError Error { get; set; }\n }\n\n public class ChatChoice\n {\n [JsonProperty(\"message\")]\n public ChatMessage Message { get; set; }\n }\n\n public class ApiError\n {\n [JsonProperty(\"message\")]\n public string Message { get; set; }\n }\n\n // --- Custom UI Controls ---\n public class ChatRowPanel : Panel\n {\n [System.Runtime.InteropServices.DllImport(\"user32.dll\")]\n private static extern bool HideCaret(IntPtr hWnd);\n\n private Panel bubblePanel;\n private RichTextBox rtbText;\n private bool isUser;\n private bool isSystem;\n private bool isError;\n\n // Collapsible properties\n private bool isCollapsible = false;\n private bool isExpanded = false;\n private string headerText = \"\";\n private string bodyText = \"\";\n private Panel headerPanel;\n private Button btnToggle;\n\n public ChatRowPanel(string text, bool isUser, bool isSystem, bool isError, int width)\n {\n this.isUser = isUser;\n this.isSystem = isSystem;\n this.isError = isError;\n this.Width = width;\n this.BackColor = Color.Transparent;\n\n // Check if it is a collapsible code block or output log\n if (isSystem && (text.StartsWith(\"[AI 请求执行 C# 代码]:\") || text.StartsWith(\"[执行结果]:\")))\n {\n int firstNewLine = text.IndexOf('\\n');\n if (firstNewLine >= 0)\n {\n headerText = text.Substring(0, firstNewLine).TrimEnd(':');\n bodyText = text.Substring(firstNewLine + 1);\n isCollapsible = !string.IsNullOrWhiteSpace(bodyText);\n }\n else\n {\n headerText = text;\n bodyText = \"\";\n isCollapsible = false;\n }\n isExpanded = false; // Collapsed by default\n }\n\n bubblePanel = new Panel\n {\n BackColor = Color.Transparent,\n Padding = new Padding(10, 8, 10, 8),\n AutoSize = false\n };\n\n rtbText = new RichTextBox\n {\n BorderStyle = BorderStyle.None,\n ScrollBars = RichTextBoxScrollBars.None,\n ReadOnly = true,\n Multiline = true,\n WordWrap = true,\n BackColor = Color.White,\n Font = new Font(\"Microsoft YaHei\", 9F)\n };\n\n rtbText.GotFocus += (s, e) => { HideCaret(rtbText.Handle); };\n rtbText.MouseDown += (s, e) => { HideCaret(rtbText.Handle); };\n\n if (isCollapsible)\n {\n headerPanel = new Panel\n {\n Height = 24,\n BackColor = Color.Transparent,\n Location = new Point(10, 6)\n };\n\n Label lblHeader = new Label\n {\n Text = headerText,\n Font = new Font(\"Microsoft YaHei\", 9F, FontStyle.Bold),\n ForeColor = isError ? Color.FromArgb(180, 40, 40) : Color.FromArgb(80, 80, 80),\n Location = new Point(0, 3),\n AutoSize = true\n };\n headerPanel.Controls.Add(lblHeader);\n\n btnToggle = new Button\n {\n Text = \"▶ 展开\",\n Font = new Font(\"Microsoft YaHei\", 8F),\n FlatStyle = FlatStyle.Flat,\n BackColor = Color.Transparent,\n ForeColor = Color.FromArgb(0, 120, 212),\n Location = new Point(lblHeader.Right + 6, 1),\n Width = 48,\n Height = 22,\n Cursor = Cursors.Hand\n };\n btnToggle.FlatAppearance.BorderSize = 0;\n btnToggle.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 230, 230);\n btnToggle.Click += BtnToggle_Click;\n headerPanel.Controls.Add(btnToggle);\n\n lblHeader.SizeChanged += (s, e) => { btnToggle.Left = lblHeader.Right + 6; };\n\n bubblePanel.Controls.Add(headerPanel);\n\n rtbText.Text = bodyText;\n rtbText.Font = new Font(\"Consolas\", 8.5F);\n rtbText.ForeColor = Color.FromArgb(60, 60, 60);\n rtbText.Location = new Point(10, 32);\n rtbText.Visible = false;\n\n bubblePanel.Controls.Add(rtbText);\n }\n else\n {\n FormatMarkdown(rtbText, text, isUser);\n rtbText.Location = new Point(10, 8);\n bubblePanel.Controls.Add(rtbText);\n }\n\n bubblePanel.Paint += BubblePanel_Paint;\n this.Controls.Add(bubblePanel);\n UpdatePosition(width);\n }\n\n private void BtnToggle_Click(object sender, EventArgs e)\n {\n isExpanded = !isExpanded;\n rtbText.Visible = isExpanded;\n btnToggle.Text = isExpanded ? \"▼ 收起\" : \"▶ 展开\";\n \n UpdatePosition(this.Width);\n \n if (this.Parent is FlowLayoutPanel flp)\n {\n flp.PerformLayout();\n }\n }\n\n private void BubblePanel_Paint(object sender, PaintEventArgs e)\n {\n e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;\n Color backColor;\n if (isError) backColor = Color.FromArgb(253, 231, 233);\n else if (isSystem) backColor = Color.FromArgb(240, 240, 240);\n else if (isUser) backColor = Color.FromArgb(0, 120, 212);\n else backColor = Color.White;\n\n using (GraphicsPath path = GetRoundedPath(new Rectangle(0, 0, bubblePanel.Width - 1, bubblePanel.Height - 1), 8))\n using (SolidBrush brush = new SolidBrush(backColor))\n {\n e.Graphics.FillPath(brush, path);\n }\n\n if (!isUser && !isError)\n {\n using (GraphicsPath path = GetRoundedPath(new Rectangle(0, 0, bubblePanel.Width - 1, bubblePanel.Height - 1), 8))\n using (Pen pen = new Pen(Color.FromArgb(220, 220, 220), 1))\n {\n e.Graphics.DrawPath(pen, path);\n }\n }\n }\n\n private GraphicsPath GetRoundedPath(Rectangle rect, int radius)\n {\n GraphicsPath path = new GraphicsPath();\n int d = radius * 2;\n path.AddArc(rect.X, rect.Y, d, d, 180, 90);\n path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);\n path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);\n path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);\n path.CloseFigure();\n return path;\n }\n\n public void UpdateWidth(int width)\n {\n this.Width = width;\n UpdatePosition(width);\n }\n\n private void UpdatePosition(int width)\n {\n int maxBubbleWidth = Convert.ToInt32(width * 0.85);\n int innerPaddingWidth = 20;\n\n int textWidth = maxBubbleWidth - innerPaddingWidth;\n if (textWidth < 50) textWidth = 50;\n\n // 1. Calculate the final bubble and RichTextBox widths\n Size measuredSize = TextRenderer.MeasureText(rtbText.Text, rtbText.Font, new Size(textWidth - 20, 0), TextFormatFlags.WordBreak);\n \n int finalBubbleWidth;\n if (isCollapsible)\n {\n finalBubbleWidth = maxBubbleWidth;\n }\n else\n {\n finalBubbleWidth = Math.Min(measuredSize.Width + innerPaddingWidth + 10, maxBubbleWidth);\n }\n\n int finalRtbWidth = finalBubbleWidth - innerPaddingWidth;\n if (finalRtbWidth < 50) finalRtbWidth = 50;\n\n rtbText.Width = finalRtbWidth;\n\n // Force handle creation so GetPositionFromCharIndex is completely accurate\n var handle = rtbText.Handle;\n\n // 2. Measure precise text wrapping height natively\n int calculatedHeight = 0;\n if (!string.IsNullOrEmpty(rtbText.Text))\n {\n Point pos = rtbText.GetPositionFromCharIndex(rtbText.Text.Length - 1);\n \n // Fallback if handle isn't fully ready or pos.Y returns 0 unexpectedly for multi-line text\n if (pos.Y == 0 && rtbText.Text.Contains(\"\\n\"))\n {\n int lineCount = rtbText.Lines.Length;\n int listBonus = 0;\n foreach (var line in rtbText.Lines)\n {\n if (line.TrimStart().StartsWith(\"- \") || line.TrimStart().StartsWith(\"* \"))\n {\n listBonus += 15;\n }\n }\n int boldAndFontPadding = 24 + (lineCount * 4) + listBonus;\n calculatedHeight = measuredSize.Height + boldAndFontPadding;\n }\n else\n {\n calculatedHeight = pos.Y + rtbText.Font.Height + 16;\n }\n }\n\n // 3. Set control heights and layout bounds\n if (isCollapsible)\n {\n headerPanel.Width = finalRtbWidth;\n if (isExpanded)\n {\n rtbText.Height = calculatedHeight;\n bubblePanel.Width = maxBubbleWidth;\n bubblePanel.Height = rtbText.Bottom + 8;\n }\n else\n {\n bubblePanel.Width = maxBubbleWidth;\n bubblePanel.Height = headerPanel.Bottom + 6;\n }\n }\n else\n {\n rtbText.Height = calculatedHeight;\n bubblePanel.Width = finalBubbleWidth;\n bubblePanel.Height = rtbText.Height + 16;\n }\n\n Color bubbleColor;\n if (isError) bubbleColor = Color.FromArgb(253, 231, 233);\n else if (isSystem) bubbleColor = Color.FromArgb(240, 240, 240);\n else if (isUser) bubbleColor = Color.FromArgb(0, 120, 212);\n else bubbleColor = Color.White;\n\n rtbText.BackColor = bubbleColor;\n rtbText.ForeColor = isUser ? Color.White : Color.FromArgb(33, 33, 33);\n\n this.Height = bubblePanel.Height + 10;\n\n if (isUser)\n {\n bubblePanel.Left = width - bubblePanel.Width - 5;\n }\n else\n {\n bubblePanel.Left = 5;\n }\n bubblePanel.Top = 5;\n \n bubblePanel.Invalidate();\n }\n\n private static void FormatMarkdown(RichTextBox rtb, string markdown, bool isUser)\n {\n rtb.Text = \"\";\n rtb.SelectionBullet = false;\n rtb.SelectionIndent = 0;\n\n string[] lines = markdown.Split('\\n');\n for (int l = 0; l < lines.Length; l++)\n {\n string line = lines[l];\n \n if (line.TrimStart().StartsWith(\"- \") || line.TrimStart().StartsWith(\"* \"))\n {\n int bulletIndex = line.IndexOf('-');\n if (bulletIndex < 0) bulletIndex = line.IndexOf('*');\n line = line.Substring(bulletIndex + 2);\n rtb.SelectionBullet = true;\n rtb.SelectionIndent = 12;\n }\n else\n {\n rtb.SelectionBullet = false;\n rtb.SelectionIndent = 0;\n }\n\n int i = 0;\n while (i < line.Length)\n {\n if (i + 1 < line.Length && line[i] == '*' && line[i + 1] == '*')\n {\n int next = line.IndexOf(\"**\", i + 2);\n if (next >= 0)\n {\n string boldText = line.Substring(i + 2, next - (i + 2));\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Bold);\n rtb.AppendText(boldText);\n i = next + 2;\n }\n else\n {\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Regular);\n rtb.AppendText(\"**\");\n i += 2;\n }\n }\n else if (line[i] == '`')\n {\n int next = line.IndexOf('`', i + 1);\n if (next >= 0)\n {\n string codeText = line.Substring(i + 1, next - (i + 1));\n \n rtb.SelectionFont = new Font(\"Consolas\", rtb.Font.Size);\n rtb.SelectionBackColor = isUser ? Color.FromArgb(0, 60, 120) : Color.FromArgb(228, 229, 230);\n rtb.SelectionColor = isUser ? Color.FromArgb(200, 230, 255) : Color.FromArgb(190, 50, 50);\n\n rtb.AppendText(codeText);\n \n rtb.SelectionBackColor = rtb.BackColor;\n rtb.SelectionColor = rtb.ForeColor;\n \n i = next + 1;\n }\n else\n {\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Regular);\n rtb.AppendText(\"`\");\n i++;\n }\n }\n else\n {\n rtb.SelectionFont = new Font(rtb.Font, FontStyle.Regular);\n rtb.SelectionColor = isUser ? Color.White : Color.FromArgb(33, 33, 33);\n rtb.AppendText(line[i].ToString());\n i++;\n }\n }\n\n if (l < lines.Length - 1)\n {\n rtb.AppendText(Environment.NewLine);\n }\n }\n }\n }\n}\n"}}}}