diff --git a/.github/actions/version-tools/action.yml b/.github/actions/version-tools/action.yml index 7f2135ce..2f19d05e 100644 --- a/.github/actions/version-tools/action.yml +++ b/.github/actions/version-tools/action.yml @@ -104,8 +104,8 @@ runs: # Determine color and status based on version if [[ $VERSION == *"-dev"* ]]; then - COLOR="red" - STATUS="Unstable Development" + COLOR="brown" + STATUS="Unstable%20Development" elif [[ $VERSION == *"-alpha"* ]]; then COLOR="orange" STATUS="Alpha" @@ -113,10 +113,10 @@ runs: COLOR="yellow" STATUS="Beta" elif [[ $VERSION == *"-rc"* ]]; then - COLOR="blue" - STATUS="Release Candidate" + COLOR="lightblue" + STATUS="Release%20Candidate" else - COLOR="brightgreen" + COLOR="lightgreen" STATUS="Stable" fi @@ -131,56 +131,50 @@ runs: echo "New version badge: $VERSION_BADGE" echo "New status badge: $STATUS_BADGE" + BADGES_CHANGED=false + # Update README.md with new badges if [[ -f "README.md" ]]; then - BADGES_CHANGED=false - - # Get the current version from Solution.props - echo "Current version from Solution.props: $VERSION" - - # Create new badge URLs - VERSION_BADGE_URL="https://img.shields.io/badge/version-$ENCODED_VERSION-$COLOR" - STATUS_BADGE_URL="https://img.shields.io/badge/status-$STATUS-$COLOR" - - # Create badge markdown - VERSION_BADGE="[![Version]($VERSION_BADGE_URL)](https://github.com/architects-toolkit/SmartHopper/releases)" - STATUS_BADGE="[![Status]($STATUS_BADGE_URL)](https://github.com/architects-toolkit/SmartHopper/releases)" - - echo "New version badge: $VERSION_BADGE" - echo "New status badge: $STATUS_BADGE" - - # Check if README contains version badge + # Check if README contains version badge and if it needs updating if grep -q "\[\!\[Version\]" README.md; then echo "Found version badge in README" - # Always update the badges with the current version from Solution.props - sed -i "s|\[\!\[Version\](https://img\.shields\.io/badge/version[^)]*)|[![Version]($VERSION_BADGE_URL)|g" README.md - echo "Updated version badge" - BADGES_CHANGED=true + # Extract current badge URL to check if color matches + CURRENT_VERSION_BADGE=$(grep -o "\[\!\[Version\](https://img\.shields\.io/badge/version[^)]*)" README.md || echo "") + + # Check if the badge needs updating (different version or color) + if [[ "$CURRENT_VERSION_BADGE" != *"$ENCODED_VERSION"* || "$CURRENT_VERSION_BADGE" != *"-$COLOR"* ]]; then + sed -i "s|\[\!\[Version\](https://img\.shields\.io/badge/version[^)]*)|[![Version]($VERSION_BADGE_URL)|g" README.md + echo "Updated version badge" + BADGES_CHANGED=true + else + echo "Version badge is already up to date" + fi else echo "No version badge found in README to replace" fi - # Check if README contains status badge + # Check if README contains status badge and if it needs updating if grep -q "\[\!\[Status\]" README.md; then echo "Found status badge in README" - # Always update the status badge - sed -i "s|\[\!\[Status\](https://img\.shields\.io/badge/status[^)]*)|[![Status]($STATUS_BADGE_URL)|g" README.md - echo "Updated status badge" - BADGES_CHANGED=true + # Extract current badge URL to check if status or color matches + CURRENT_STATUS_BADGE=$(grep -o "\[\!\[Status\](https://img\.shields\.io/badge/status[^)]*)" README.md || echo "") + + # Check if the badge needs updating (different status or color) + if [[ "$CURRENT_STATUS_BADGE" != *"$STATUS"* || "$CURRENT_STATUS_BADGE" != *"-$COLOR"* ]]; then + sed -i "s|\[\!\[Status\](https://img\.shields\.io/badge/status[^)]*)|[![Status]($STATUS_BADGE_URL)|g" README.md + echo "Updated status badge" + BADGES_CHANGED=true + else + echo "Status badge is already up to date" + fi else echo "No status badge found in README to replace" fi - # Check if badges were changed - if [ "$BADGES_CHANGED" = true ]; then - echo "README.md badges were updated" - echo "badges-changed=true" >> $GITHUB_OUTPUT - else - echo "No changes to README.md badges" - echo "badges-changed=false" >> $GITHUB_OUTPUT - fi + # Output whether badges were changed + echo "badges-changed=$BADGES_CHANGED" >> $GITHUB_OUTPUT else echo "README.md not found, skipping badge update" echo "badges-changed=false" >> $GITHUB_OUTPUT diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cacb69..24a88814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Modified AIChatComponent to always run when the Run parameter is true, regardless of input changes. +- Improved version badge workflow to also update badges when color doesn't match the requirements based on version type. - Improved ChatDialog UI with numerous enhancements: - Modern chat-like interface featuring message bubbles and visual styling. - Better layout with proper text wrapping to prevent horizontal scrolling. @@ -49,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Automatically build and attach artifacts to published releases. - Create platform-specific zip files (Rhino8-Windows, Rhino8-Mac) instead of a single zip with subfolders. - Improved error handling in the AIStatefulAsyncComponentBase. +- Updated settings menu to use Eto.Forms and Eto.Drawing. ### Removed diff --git a/README.md b/README.md index 01f55b3f..88cf4bda 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # SmartHopper - AI-Powered Grasshopper3D Plugin -[![Version](https://img.shields.io/badge/version-0%2E1%2E3--dev%2E250330-red)](https://github.com/architects-toolkit/SmartHopper/releases) -[![Status](https://img.shields.io/badge/status-Development-red)](https://github.com/architects-toolkit/SmartHopper/releases) +[![Version](https://img.shields.io/badge/version-0%2E2%2E0--dev%2E250331-red)](https://github.com/architects-toolkit/SmartHopper/releases) +[![Status](https://img.shields.io/badge/status-Unstable%20Development-red)](https://github.com/architects-toolkit/SmartHopper/releases) [![Grasshopper](https://img.shields.io/badge/plugin_for-Grasshopper3D-darkgreen?logo=rhinoceros)](https://www.rhino3d.com/) [![MistralAI](https://img.shields.io/badge/AI--powered-MistralAI-orange)](https://mistral.ai/) [![OpenAI](https://img.shields.io/badge/AI--powered-OpenAI-blue?logo=openai)](https://openai.com/) diff --git a/Solution.props b/Solution.props index c7f00988..48d0126a 100644 --- a/Solution.props +++ b/Solution.props @@ -1,5 +1,5 @@ - 0.1.3-dev.250330 + 0.2.0-dev.250331 \ No newline at end of file diff --git a/design/icons/export/mistralai_icon-old.png b/design/icons/export/mistralai_icon-old.png new file mode 100644 index 00000000..a48b4327 Binary files /dev/null and b/design/icons/export/mistralai_icon-old.png differ diff --git a/design/icons/export/mistralai_icon.png b/design/icons/export/mistralai_icon.png index a48b4327..19d0fa44 100644 Binary files a/design/icons/export/mistralai_icon.png and b/design/icons/export/mistralai_icon.png differ diff --git a/design/icons/export/openai_icon-old.png b/design/icons/export/openai_icon-old.png new file mode 100644 index 00000000..bdf9c6d9 Binary files /dev/null and b/design/icons/export/openai_icon-old.png differ diff --git a/design/icons/export/openai_icon.png b/design/icons/export/openai_icon.png index bdf9c6d9..f0d9ad57 100644 Binary files a/design/icons/export/openai_icon.png and b/design/icons/export/openai_icon.png differ diff --git a/src/SmartHopper.Config/Resources/mistralai_icon.png b/src/SmartHopper.Config/Resources/mistralai_icon.png index a48b4327..19d0fa44 100644 Binary files a/src/SmartHopper.Config/Resources/mistralai_icon.png and b/src/SmartHopper.Config/Resources/mistralai_icon.png differ diff --git a/src/SmartHopper.Config/Resources/openai_icon.png b/src/SmartHopper.Config/Resources/openai_icon.png index bdf9c6d9..f0d9ad57 100644 Binary files a/src/SmartHopper.Config/Resources/openai_icon.png and b/src/SmartHopper.Config/Resources/openai_icon.png differ diff --git a/src/SmartHopper.Menu/Dialogs/AboutDialog.cs b/src/SmartHopper.Menu/Dialogs/AboutDialog.cs index 7f7142b1..04b4e8e5 100644 --- a/src/SmartHopper.Menu/Dialogs/AboutDialog.cs +++ b/src/SmartHopper.Menu/Dialogs/AboutDialog.cs @@ -52,7 +52,7 @@ public AboutDialog(string version) ); } - private Control CreateLogoPanel() + private static Control CreateLogoPanel() { // Create an ImageView with the SmartHopper logo from the resources var imageView = new ImageView(); @@ -199,7 +199,7 @@ private Control CreateContentPanel(string version) }; } - private LinkButton CreateLinkButton(string text, string url) + private static LinkButton CreateLinkButton(string text, string url) { var link = new LinkButton { @@ -212,7 +212,7 @@ private LinkButton CreateLinkButton(string text, string url) return link; } - private void OpenUrl(string url) + private static void OpenUrl(string url) { try { diff --git a/src/SmartHopper.Menu/Items/SettingsMenuItem.cs b/src/SmartHopper.Menu/Items/SettingsMenuItem.cs index cf26d0c2..2844d226 100644 --- a/src/SmartHopper.Menu/Items/SettingsMenuItem.cs +++ b/src/SmartHopper.Menu/Items/SettingsMenuItem.cs @@ -12,298 +12,339 @@ using SmartHopper.Config.Models; using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; -using System.Windows.Forms; +using Rhino; namespace SmartHopper.Menu.Items { internal static class SettingsMenuItem { - private static readonly Dictionary> ControlFactories = new Dictionary> + private static readonly Dictionary> ControlFactories = new Dictionary> { - [typeof(string)] = descriptor => new TextBox + [typeof(string)] = descriptor => { - UseSystemPasswordChar = descriptor.IsSecret, - Dock = DockStyle.Fill + if (descriptor.IsSecret) + return new Eto.Forms.PasswordBox(); + else + return new Eto.Forms.TextBox(); }, - [typeof(int)] = descriptor => new NumericUpDown + [typeof(int)] = descriptor => new Eto.Forms.NumericStepper { - Minimum = 1, - Maximum = 4096, - Value = Convert.ToInt32(descriptor.DefaultValue), - Dock = DockStyle.Fill + MinValue = 1, + MaxValue = 4096, + Value = Convert.ToInt32(descriptor.DefaultValue) } }; - public static ToolStripMenuItem Create() + public static System.Windows.Forms.ToolStripMenuItem Create() { - var item = new ToolStripMenuItem("Settings"); + var item = new System.Windows.Forms.ToolStripMenuItem("Settings"); item.Click += (sender, e) => ShowSettingsDialog(); return item; } private static void ShowSettingsDialog() { - using (var form = new Form()) + // Use RhinoApp.InvokeOnUiThread to ensure UI operations run on Rhino's main UI thread + RhinoApp.InvokeOnUiThread(new Action(() => { - form.Text = "SmartHopper Settings"; - form.Size = new Size(500, 400); - form.StartPosition = FormStartPosition.CenterScreen; - form.AutoScroll = true; - - var providers = SmartHopperSettings.DiscoverProviders().ToArray(); - var settings = SmartHopperSettings.Load(); - - System.Diagnostics.Debug.WriteLine($"Number of providers: {providers.Length}"); - - var outerPanel = new Panel - { - Dock = DockStyle.Fill, - AutoScroll = true - }; - form.Controls.Add(outerPanel); - - var panel = new TableLayoutPanel - { - Dock = DockStyle.Top, - AutoSize = true, - Padding = new Padding(10) - }; - outerPanel.Controls.Add(panel); - - // Calculate total rows needed - int totalRows = providers.Sum(p => p.GetSettingDescriptors().Count() * 2 + 1); // *2 for description rows, +1 for provider header - totalRows += 4; // Add 4 rows: 1 for general header, 3 for default provider selection and debounce time (control + description) - panel.RowCount = totalRows; - panel.ColumnCount = 2; - - // Set column widths - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40F)); // First column takes 40% of the width - panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60F)); // Second column takes 60% of the width - - var row = 0; - var allControls = new Dictionary>(); - - // Add general settings section - var generalHeader = new Label - { - Text = "General Settings", - Font = new Font(form.Font, FontStyle.Bold), - Dock = DockStyle.Fill, - Padding = new Padding(0, 15, 0, 10), - AutoSize = true - }; - panel.Controls.Add(generalHeader, 0, row); - panel.SetColumnSpan(generalHeader, 2); - row++; - - // Add default provider selection - panel.Controls.Add(new Label - { - Text = "Default AI Provider:", - Dock = DockStyle.Fill, - AutoSize = true - }, 0, row); - - var defaultProviderComboBox = new ComboBox - { - Dock = DockStyle.Fill, - DropDownStyle = ComboBoxStyle.DropDownList - }; - - // Add all providers to the dropdown - foreach (var provider in providers) - { - defaultProviderComboBox.Items.Add(provider.Name); - } - - // Select the current default provider if set - if (!string.IsNullOrEmpty(settings.DefaultAIProvider) && - defaultProviderComboBox.Items.Contains(settings.DefaultAIProvider)) - { - defaultProviderComboBox.SelectedItem = settings.DefaultAIProvider; - } - else if (defaultProviderComboBox.Items.Count > 0) - { - defaultProviderComboBox.SelectedIndex = 0; - } - - panel.Controls.Add(defaultProviderComboBox, 1, row); - row++; - - // Add default provider description - var defaultProviderDescription = new Label - { - Text = "The default AI provider to use when 'Default' is selected in components", - ForeColor = SystemColors.GrayText, - Font = new Font(form.Font.FontFamily, form.Font.Size - 1), - Dock = DockStyle.Fill, - AutoSize = true, - Padding = new Padding(5, 0, 0, 5) - }; - panel.Controls.Add(defaultProviderDescription, 0, row); - panel.SetColumnSpan(defaultProviderDescription, 2); - row++; - - // Add debounce time setting - panel.Controls.Add(new Label + using (var dialog = new Eto.Forms.Dialog()) { - Text = "Debounce Time (ms):", - Dock = DockStyle.Fill, - AutoSize = true - }, 0, row); + dialog.Title = "SmartHopper Settings"; + dialog.Size = new Eto.Drawing.Size(500, 400); + dialog.MinimumSize = new Eto.Drawing.Size(400, 300); + dialog.Resizable = true; + dialog.Padding = new Eto.Drawing.Padding(10); + + // Center the dialog on screen + dialog.Location = new Eto.Drawing.Point( + (int)((Eto.Forms.Screen.PrimaryScreen.Bounds.Width - dialog.Size.Width) / 2), + (int)((Eto.Forms.Screen.PrimaryScreen.Bounds.Height - dialog.Size.Height) / 2) + ); + + var providers = SmartHopperSettings.DiscoverProviders().ToArray(); + var settings = SmartHopperSettings.Load(); + + // Create the main layout + var layout = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5), Padding = new Eto.Drawing.Padding(10) }; + var scrollable = new Eto.Forms.Scrollable { Content = layout }; + + // Dictionary to store all controls for later retrieval + var allControls = new Dictionary>(); + // Dictionary to track original values to avoid overwriting unchanged sensitive data + var originalValues = new Dictionary>(); + + // Add general settings section + layout.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label + { + Text = "General Settings", + Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Bold, 12), + VerticalAlignment = Eto.Forms.VerticalAlignment.Center + }) + )); + + // Add default provider selection + var defaultProviderRow = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5) }; + var defaultProviderComboBox = new Eto.Forms.DropDown(); + + // Add all providers to the dropdown and select current default + foreach (var provider in providers) + { + defaultProviderComboBox.Items.Add(new Eto.Forms.ListItem { Text = provider.Name }); + } + + if (!string.IsNullOrEmpty(settings.DefaultAIProvider)) + { + for (int i = 0; i < defaultProviderComboBox.Items.Count; i++) + { + if (defaultProviderComboBox.Items[i].Text == settings.DefaultAIProvider) + { + defaultProviderComboBox.SelectedIndex = i; + break; + } + } + } + else if (defaultProviderComboBox.Items.Count > 0) + { + defaultProviderComboBox.SelectedIndex = 0; + } - var debounceControl = new NumericUpDown - { - Minimum = 1000, - Maximum = 5000, - Value = settings.DebounceTime, - Dock = DockStyle.Fill - }; - panel.Controls.Add(debounceControl, 1, row); - row++; - - // Add debounce description - var debounceDescription = new Label - { - Text = "Time to wait before sending a new request (in milliseconds)", - ForeColor = SystemColors.GrayText, - Font = new Font(form.Font.FontFamily, form.Font.Size - 1), - Dock = DockStyle.Fill, - AutoSize = true, - Padding = new Padding(5, 0, 0, 5) - }; - panel.Controls.Add(debounceDescription, 0, row); - panel.SetColumnSpan(debounceDescription, 2); - row++; - - foreach (var provider in providers) - { - System.Diagnostics.Debug.WriteLine($"Provider: {provider.Name}"); - var descriptors = provider.GetSettingDescriptors().ToList(); - System.Diagnostics.Debug.WriteLine($"Number of descriptors: {descriptors.Count}"); + defaultProviderRow.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label { Text = "Default AI Provider:", VerticalAlignment = Eto.Forms.VerticalAlignment.Center }), + new Eto.Forms.TableCell(defaultProviderComboBox) + )); + layout.Rows.Add(defaultProviderRow); - // Provider header - var header = new Label + // Add default provider description + layout.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label + { + Text = "The default AI provider to use when the 'Default' provider is selected", + TextColor = Eto.Drawing.Colors.Gray, + Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10) + }) + )); + + // Add debounce time setting + var debounceRow = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5) }; + var debounceControl = new Eto.Forms.NumericStepper { - Text = provider.Name, - Font = new Font(form.Font, FontStyle.Bold), - Dock = DockStyle.Fill, - Padding = new Padding(0, 15, 0, 10), - AutoSize = true + MinValue = 1000, + MaxValue = 5000, + Value = settings.DebounceTime }; - panel.Controls.Add(header, 0, row); - panel.SetColumnSpan(header, 2); - row++; - var controls = new Dictionary(); - foreach (var descriptor in descriptors) + debounceRow.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label { Text = "Debounce Time (ms):", VerticalAlignment = Eto.Forms.VerticalAlignment.Center }), + new Eto.Forms.TableCell(debounceControl) + )); + layout.Rows.Add(debounceRow); + + // Add debounce description + layout.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label + { + Text = "Time to wait before sending a new request (in milliseconds)", + TextColor = Eto.Drawing.Colors.Gray, + Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10) + }) + )); + + // Add provider settings + foreach (var provider in providers) { - System.Diagnostics.Debug.WriteLine($"Creating control for: {descriptor.Name} ({descriptor.DisplayName})"); + var descriptors = provider.GetSettingDescriptors().ToList(); + var controls = new Dictionary(); + originalValues[provider.Name] = new Dictionary(); - // Add label and control - panel.Controls.Add(new Label + // Create provider header with icon + var headerLayout = new Eto.Forms.StackLayout { - Text = descriptor.DisplayName + ":", - Dock = DockStyle.Fill, - AutoSize = true - }, 0, row); - - var control = ControlFactories[descriptor.Type](descriptor); - panel.Controls.Add(control, 1, row); - controls[descriptor.Name] = control; - row++; - - // Add description - if (!string.IsNullOrWhiteSpace(descriptor.Description)) + Orientation = Eto.Forms.Orientation.Horizontal, + Spacing = 5, + VerticalContentAlignment = Eto.Forms.VerticalAlignment.Center, + Padding = new Eto.Drawing.Padding(0, 15, 0, 10) + }; + + // Add provider icon if available + if (provider.Icon != null) { - var descriptionLabel = new Label + using (var ms = new System.IO.MemoryStream()) { - Text = descriptor.Description, - ForeColor = SystemColors.GrayText, - Font = new Font(form.Font.FontFamily, form.Font.Size - 1), - Dock = DockStyle.Fill, - AutoSize = true, - Padding = new Padding(5, 0, 0, 5) - }; - panel.Controls.Add(descriptionLabel, 0, row); - panel.SetColumnSpan(descriptionLabel, 2); - row++; + provider.Icon.Save(ms, System.Drawing.Imaging.ImageFormat.Png); + ms.Position = 0; + headerLayout.Items.Add(new Eto.Forms.ImageView + { + Image = new Eto.Drawing.Bitmap(ms), + Size = new Eto.Drawing.Size(16, 16) + }); + } } - // Load current value if exists - if (settings.ProviderSettings.ContainsKey(provider.Name) && - settings.ProviderSettings[provider.Name].ContainsKey(descriptor.Name)) + // Add provider name + headerLayout.Items.Add(new Eto.Forms.Label { - var value = settings.ProviderSettings[provider.Name][descriptor.Name]; - if (control is TextBox textBox) - textBox.Text = value?.ToString() ?? ""; - else if (control is NumericUpDown numericUpDown && value != null) - numericUpDown.Value = Convert.ToInt32(value); - } - else if (descriptor.DefaultValue != null) + Text = provider.Name, + Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Bold, 12), + VerticalAlignment = Eto.Forms.VerticalAlignment.Center + }); + + layout.Rows.Add(new Eto.Forms.TableRow(new Eto.Forms.TableCell(headerLayout))); + + // Add settings for this provider + foreach (var descriptor in descriptors) { - if (control is TextBox textBox) - textBox.Text = descriptor.DefaultValue.ToString(); - else if (control is NumericUpDown numericUpDown) - numericUpDown.Value = Convert.ToInt32(descriptor.DefaultValue); + // Create control for this setting + var control = ControlFactories[descriptor.Type](descriptor); + controls[descriptor.Name] = control; + + // Add label and control + var settingRow = new Eto.Forms.TableLayout { Spacing = new Eto.Drawing.Size(5, 5) }; + settingRow.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label { + Text = descriptor.DisplayName + ":", + VerticalAlignment = Eto.Forms.VerticalAlignment.Center + }), + new Eto.Forms.TableCell(control) + )); + layout.Rows.Add(settingRow); + + // Add description if available + if (!string.IsNullOrWhiteSpace(descriptor.Description)) + { + layout.Rows.Add(new Eto.Forms.TableRow( + new Eto.Forms.TableCell(new Eto.Forms.Label + { + Text = descriptor.Description, + TextColor = Eto.Drawing.Colors.Gray, + Font = new Eto.Drawing.Font(Eto.Drawing.SystemFont.Default, 10) + }) + )); + } + + // Load current value + string currentValue = null; + if (settings.ProviderSettings.ContainsKey(provider.Name) && + settings.ProviderSettings[provider.Name].ContainsKey(descriptor.Name)) + { + currentValue = settings.ProviderSettings[provider.Name][descriptor.Name]?.ToString(); + } + else if (descriptor.DefaultValue != null) + { + currentValue = descriptor.DefaultValue.ToString(); + } + + // Set value to control and store original for comparison + if (currentValue != null) + { + if (control is Eto.Forms.TextBox textBox) + textBox.Text = currentValue; + else if (control is Eto.Forms.PasswordBox passwordBox) + passwordBox.Text = currentValue; + else if (control is Eto.Forms.NumericStepper numericStepper) + numericStepper.Value = Convert.ToInt32(currentValue); + + // Store original value for comparison + originalValues[provider.Name][descriptor.Name] = currentValue; + } } - } - allControls[provider.Name] = controls; - } + allControls[provider.Name] = controls; + } - // Add save button at the bottom - var buttonPanel = new Panel - { - Dock = DockStyle.Bottom, - Height = 40, - Padding = new Padding(5) - }; + // Add a spacer row at the end + layout.Rows.Add(Eto.Forms.TableLayout.AutoSized(null)); - var saveButton = new Button - { - Text = "Save", - DialogResult = DialogResult.OK, - Dock = DockStyle.Right - }; - buttonPanel.Controls.Add(saveButton); - form.Controls.Add(buttonPanel); - form.AcceptButton = saveButton; - - if (form.ShowDialog() == DialogResult.OK) - { - // Save settings - foreach (var provider in providers) + // Create buttons + var buttonLayout = new Eto.Forms.StackLayout { - if (!settings.ProviderSettings.ContainsKey(provider.Name)) - settings.ProviderSettings[provider.Name] = new Dictionary(); + Orientation = Eto.Forms.Orientation.Horizontal, + Spacing = 5, + HorizontalContentAlignment = Eto.Forms.HorizontalAlignment.Right + }; - var controls = allControls[provider.Name]; - foreach (var descriptor in provider.GetSettingDescriptors()) - { - var control = controls[descriptor.Name]; - object value = null; + var saveButton = new Eto.Forms.Button { Text = "Save" }; + var cancelButton = new Eto.Forms.Button { Text = "Cancel" }; - if (control is TextBox textBox) - value = textBox.Text; - else if (control is NumericUpDown numericUpDown) - value = (int)numericUpDown.Value; + buttonLayout.Items.Add(new Eto.Forms.StackLayoutItem(null, true)); // Spacer + buttonLayout.Items.Add(saveButton); + buttonLayout.Items.Add(cancelButton); - settings.ProviderSettings[provider.Name][descriptor.Name] = value; + // Set up the dialog content + var content = new Eto.Forms.DynamicLayout(); + content.Add(scrollable, yscale: true); + content.Add(buttonLayout); + + dialog.Content = content; + dialog.DefaultButton = saveButton; + dialog.AbortButton = cancelButton; + + // Handle button clicks + saveButton.Click += (sender, e) => + { + // Create a copy of the current settings to preserve encrypted values + var updatedSettings = new Dictionary>(); + foreach (var providerSetting in settings.ProviderSettings) + { + updatedSettings[providerSetting.Key] = new Dictionary(providerSetting.Value); } - } + + // Update with new values from the UI + foreach (var provider in providers) + { + if (!updatedSettings.ContainsKey(provider.Name)) + updatedSettings[provider.Name] = new Dictionary(); - // Save debounce time - settings.DebounceTime = (int)debounceControl.Value; - - // Save default provider - settings.DefaultAIProvider = defaultProviderComboBox.SelectedItem?.ToString() ?? ""; + var controls = allControls[provider.Name]; + foreach (var descriptor in provider.GetSettingDescriptors()) + { + var control = controls[descriptor.Name]; + + // Get new value from control + object newValue = null; + if (control is Eto.Forms.TextBox textBox) + newValue = textBox.Text; + else if (control is Eto.Forms.PasswordBox passwordBox) + newValue = passwordBox.Text; + else if (control is Eto.Forms.NumericStepper numericStepper) + newValue = (int)numericStepper.Value; + + // For sensitive data, only update if changed and not empty + if (descriptor.IsSecret && newValue is string strValue) + { + if (string.IsNullOrEmpty(strValue)) + continue; // Keep existing value + + if (originalValues[provider.Name].ContainsKey(descriptor.Name) && + strValue == originalValues[provider.Name][descriptor.Name]) + continue; // Skip unchanged values + } + + // Update the setting + updatedSettings[provider.Name][descriptor.Name] = newValue; + } + } + + // Update settings + settings.ProviderSettings = updatedSettings; + settings.DebounceTime = (int)debounceControl.Value; + + // Save default provider + if (defaultProviderComboBox.SelectedIndex >= 0) + settings.DefaultAIProvider = defaultProviderComboBox.Items[defaultProviderComboBox.SelectedIndex].Text; + + // Save settings (this will handle encryption) + settings.Save(); + dialog.Close(); + }; - settings.Save(); + cancelButton.Click += (sender, e) => dialog.Close(); + + // Show the dialog + dialog.ShowModal(); } - } + })); } } }