Files
opencode_gesis/GesisOpenCodeInstaller.cs
T
2026-06-29 19:36:31 +02:00

1175 lines
41 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace GesisOpenCodeInstaller
{
internal static class Program
{
[STAThread]
private static void Main()
{
TryEnableDpiAwareness();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new InstallerForm());
}
[DllImport("user32.dll")]
private static extern bool SetProcessDPIAware();
private static void TryEnableDpiAwareness()
{
try
{
SetProcessDPIAware();
}
catch
{
}
}
}
internal sealed class InstallerForm : Form
{
private const string Repo = "anomalyco/opencode";
private const string ReleaseApiUrl = "https://api.github.com/repos/" + Repo + "/releases/latest";
private const string WindowsAssetName = "opencode-desktop-win-x64.exe";
private const string BaseUrl = "https://ai-openwebui.gesis.org/api";
private const int DesktopInstallerWaitMilliseconds = 15000;
private const int DownloadTimeoutMilliseconds = 60000;
private readonly TextBox apiKeyBox;
private readonly TextBox modelFilterBox;
private readonly TableLayoutPanel modelGridPanel;
private readonly Label modelIntroLabel;
private readonly Button checkApiKeyButton;
private readonly Button backButton;
private readonly Button installButton;
private readonly Button selectAllButton;
private readonly Button selectNoneButton;
private readonly ProgressBar progressBar;
private readonly Label statusLabel;
private readonly CheckBox skipDesktopCheckBox;
private readonly List<ModelOption> availableModels = new List<ModelOption>();
public InstallerForm()
{
Text = "GESIS OpenCode Installer";
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.Sizable;
MaximizeBox = true;
MinimizeBox = true;
ClientSize = new Size(760, 560);
MinimumSize = new Size(620, 500);
Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
BackColor = Color.White;
AutoScaleMode = AutoScaleMode.Dpi;
AutoScroll = false;
var logoBox = new PictureBox
{
Margin = new Padding(0, 0, 0, 18),
Size = new Size(330, 92),
SizeMode = PictureBoxSizeMode.Zoom,
Image = LoadEmbeddedImage("GesisLogo")
};
var title = new Label
{
Text = "Install OpenCode Desktop for GESIS",
Font = new Font(Font.FontFamily, 15F, FontStyle.Bold),
AutoSize = true,
MaximumSize = new Size(680, 0),
Margin = new Padding(0, 0, 0, 8)
};
var description = new Label
{
Text = "This installer checks your personal API key, lets you choose the GESIS AI Server models to show in OpenCode, and downloads the latest OpenCode Desktop for Windows.",
AutoSize = true,
MaximumSize = new Size(680, 0),
Margin = new Padding(0, 0, 0, 22)
};
var apiKeyLabel = new Label
{
Text = "Personal GESIS API key",
AutoSize = true,
Margin = new Padding(0, 0, 0, 6)
};
apiKeyBox = new TextBox
{
Dock = DockStyle.Fill,
Margin = new Padding(0, 0, 0, 14),
MinimumSize = new Size(420, 30),
UseSystemPasswordChar = true
};
checkApiKeyButton = new Button
{
Text = "Check API key",
AutoSize = true,
MinimumSize = new Size(150, 40),
Padding = new Padding(12, 4, 12, 4),
Margin = new Padding(0, 0, 0, 18)
};
checkApiKeyButton.Click += CheckApiKeyButton_Click;
modelIntroLabel = new Label
{
Text = "These are all models currently available to your API key on the GESIS AI Server. Select the models you want to use in OpenCode.",
AutoSize = true,
MaximumSize = new Size(680, 0),
Margin = new Padding(0, 0, 0, 10),
Visible = false
};
modelFilterBox = new TextBox
{
Dock = DockStyle.Fill,
Margin = new Padding(0, 0, 0, 10),
MinimumSize = new Size(420, 30),
Visible = false
};
modelFilterBox.TextChanged += ModelFilterBox_TextChanged;
modelGridPanel = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
BackColor = Color.White,
ColumnCount = 1,
Dock = DockStyle.Fill,
Margin = new Padding(0, 0, 0, 10),
Visible = false
};
selectAllButton = new Button
{
Text = "Select all",
AutoSize = true,
MinimumSize = new Size(110, 36),
Padding = new Padding(10, 3, 10, 3),
Visible = false
};
selectAllButton.Click += SelectAllButton_Click;
selectNoneButton = new Button
{
Text = "Select none",
AutoSize = true,
MinimumSize = new Size(110, 36),
Padding = new Padding(10, 3, 10, 3),
Visible = false
};
selectNoneButton.Click += SelectNoneButton_Click;
skipDesktopCheckBox = new CheckBox
{
Text = "Only update the OpenCode configuration",
AutoSize = true,
Margin = new Padding(0, 0, 0, 18),
Visible = false
};
installButton = new Button
{
Text = "Install selected models",
AutoSize = true,
MinimumSize = new Size(190, 40),
Padding = new Padding(12, 4, 12, 4),
Anchor = AnchorStyles.Right,
Margin = new Padding(8, 0, 0, 18),
Visible = false
};
installButton.Click += InstallButton_Click;
backButton = new Button
{
Text = "Back",
AutoSize = true,
MinimumSize = new Size(100, 40),
Padding = new Padding(12, 4, 12, 4),
Margin = new Padding(8, 0, 0, 18),
Visible = false
};
backButton.Click += BackButton_Click;
progressBar = new ProgressBar
{
Dock = DockStyle.Fill,
MinimumSize = new Size(420, 18),
Margin = new Padding(0, 0, 0, 12),
Style = ProgressBarStyle.Continuous
};
statusLabel = new Label
{
Text = "Ready.",
AutoSize = true,
MaximumSize = new Size(680, 0),
Margin = new Padding(0)
};
var icon = LoadEmbeddedIcon("GesisIcon");
if (icon != null)
{
Icon = icon;
}
var selectionButtonPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = true,
Margin = new Padding(0, 0, 0, 12)
};
selectionButtonPanel.Controls.Add(selectAllButton);
selectionButtonPanel.Controls.Add(selectNoneButton);
var primaryButtonPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
FlowDirection = FlowDirection.RightToLeft,
WrapContents = true
};
primaryButtonPanel.Controls.Add(installButton);
primaryButtonPanel.Controls.Add(backButton);
primaryButtonPanel.Controls.Add(checkApiKeyButton);
var footerLayout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
BackColor = Color.White,
ColumnCount = 1,
RowCount = 4,
Padding = new Padding(32, 12, 32, 16)
};
footerLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
footerLayout.Controls.Add(skipDesktopCheckBox, 0, 0);
footerLayout.Controls.Add(primaryButtonPanel, 0, 1);
footerLayout.Controls.Add(progressBar, 0, 2);
footerLayout.Controls.Add(statusLabel, 0, 3);
var layout = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
BackColor = Color.White,
ColumnCount = 1,
Padding = new Padding(32),
RowCount = 10
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
for (var i = 0; i < layout.RowCount; i++)
{
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
}
layout.Controls.Add(logoBox, 0, 0);
layout.Controls.Add(title, 0, 1);
layout.Controls.Add(description, 0, 2);
layout.Controls.Add(apiKeyLabel, 0, 3);
layout.Controls.Add(apiKeyBox, 0, 4);
layout.Controls.Add(modelIntroLabel, 0, 5);
layout.Controls.Add(modelFilterBox, 0, 6);
layout.Controls.Add(selectionButtonPanel, 0, 7);
layout.Controls.Add(modelGridPanel, 0, 8);
var contentPanel = new Panel
{
Dock = DockStyle.Fill,
AutoScroll = true,
BackColor = Color.White
};
contentPanel.Controls.Add(layout);
var rootLayout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
BackColor = Color.White,
ColumnCount = 1,
RowCount = 2
};
rootLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
rootLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
rootLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
rootLayout.Controls.Add(contentPanel, 0, 0);
rootLayout.Controls.Add(footerLayout, 0, 1);
Controls.Add(rootLayout);
AcceptButton = checkApiKeyButton;
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (WindowState == FormWindowState.Minimized)
{
WindowState = FormWindowState.Normal;
}
Show();
Activate();
BringToFront();
TopMost = true;
TopMost = false;
}
private static Image LoadEmbeddedImage(string resourceName)
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
if (stream == null)
{
return null;
}
using (stream)
{
return Image.FromStream(stream);
}
}
private static Icon LoadEmbeddedIcon(string resourceName)
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
if (stream == null)
{
return null;
}
return new Icon(stream);
}
private async void CheckApiKeyButton_Click(object sender, EventArgs e)
{
var apiKey = apiKeyBox.Text.Trim();
if (apiKey.Length == 0)
{
MessageBox.Show(this, "Please enter your personal GESIS API key.", "API key required", MessageBoxButtons.OK, MessageBoxIcon.Warning);
apiKeyBox.Focus();
return;
}
SetBusy(true);
try
{
var models = await Task.Run(new Func<List<ModelOption>>(() => ProbeAvailableModels(apiKey)));
if (models.Count == 0)
{
throw new InvalidOperationException("No models are available for this API key.");
}
availableModels.Clear();
availableModels.AddRange(models);
foreach (var model in availableModels)
{
model.IsSelected = true;
}
WindowState = FormWindowState.Maximized;
ShowModelSelection();
RefreshModelList();
ResizeForModelGrid();
progressBar.Value = 100;
statusLabel.Text = "Found " + availableModels.Count + " available model(s). Choose which models should appear in OpenCode.";
}
catch (Exception ex)
{
statusLabel.Text = "API key check failed.";
MessageBox.Show(this, ex.Message, "API key check failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetBusy(false);
}
}
private async void InstallButton_Click(object sender, EventArgs e)
{
var apiKey = apiKeyBox.Text.Trim();
var selectedModels = GetSelectedModels();
if (selectedModels.Count == 0)
{
MessageBox.Show(this, "Please select at least one model to use in OpenCode.", "Model selection required", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
SetBusy(true);
try
{
await Task.Run(new Action(() => WriteOpenCodeConfig(apiKey, selectedModels)));
if (!skipDesktopCheckBox.Checked)
{
await Task.Run(new Action(InstallOpenCodeDesktop));
}
progressBar.Value = 100;
statusLabel.Text = "Installation complete.";
MessageBox.Show(this, "OpenCode is configured for the GESIS AI Server. If the OpenCode Desktop installer is still open, please follow its prompts to finish.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
statusLabel.Text = "Installation failed.";
MessageBox.Show(this, ex.Message, "Installation failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetBusy(false);
}
}
private void BackButton_Click(object sender, EventArgs e)
{
ShowApiKeyScreen();
}
private void ModelFilterBox_TextChanged(object sender, EventArgs e)
{
SyncVisibleModelSelection();
RefreshModelList();
}
private void SelectAllButton_Click(object sender, EventArgs e)
{
SetVisibleModelSelection(true);
}
private void SelectNoneButton_Click(object sender, EventArgs e)
{
SetVisibleModelSelection(false);
}
private void ShowModelSelection()
{
checkApiKeyButton.Visible = false;
apiKeyBox.Enabled = false;
modelIntroLabel.Visible = true;
modelFilterBox.Visible = true;
modelGridPanel.Visible = true;
selectAllButton.Visible = true;
selectNoneButton.Visible = true;
skipDesktopCheckBox.Visible = true;
installButton.Visible = true;
backButton.Visible = true;
AcceptButton = installButton;
}
private void ShowApiKeyScreen()
{
WindowState = FormWindowState.Normal;
checkApiKeyButton.Visible = true;
apiKeyBox.Enabled = true;
modelIntroLabel.Visible = false;
modelFilterBox.Visible = false;
modelGridPanel.Visible = false;
selectAllButton.Visible = false;
selectNoneButton.Visible = false;
skipDesktopCheckBox.Visible = false;
installButton.Visible = false;
backButton.Visible = false;
progressBar.Value = 0;
statusLabel.Text = "Ready.";
AcceptButton = checkApiKeyButton;
}
private void InstallOpenCodeDesktop()
{
if (!Environment.Is64BitOperatingSystem)
{
throw new InvalidOperationException("This installer requires 64-bit Windows.");
}
SetStatus("Finding the latest OpenCode Desktop release...", 10);
var releaseJson = DownloadString(ReleaseApiUrl);
var serializer = new JavaScriptSerializer();
var release = (Dictionary<string, object>)serializer.DeserializeObject(releaseJson);
var tagName = release.ContainsKey("tag_name") ? Convert.ToString(release["tag_name"]) : "latest";
var assets = release["assets"] as object[];
Dictionary<string, object> selectedAsset = null;
if (assets != null)
{
foreach (var item in assets)
{
var asset = item as Dictionary<string, object>;
if (asset == null || !asset.ContainsKey("name"))
{
continue;
}
if (String.Equals(Convert.ToString(asset["name"]), WindowsAssetName, StringComparison.OrdinalIgnoreCase))
{
selectedAsset = asset;
break;
}
}
}
if (selectedAsset == null || !selectedAsset.ContainsKey("browser_download_url"))
{
throw new InvalidOperationException("Could not find the Windows desktop installer in the latest OpenCode release.");
}
var downloadUrl = Convert.ToString(selectedAsset["browser_download_url"]);
var downloadPath = Path.Combine(Path.GetTempPath(), WindowsAssetName);
var expectedSize = selectedAsset.ContainsKey("size") ? Convert.ToInt64(selectedAsset["size"]) : -1;
if (expectedSize > 0 && File.Exists(downloadPath) && new FileInfo(downloadPath).Length == expectedSize)
{
SetStatus("Using already downloaded OpenCode Desktop " + tagName + ".", 70);
}
else
{
SetStatus("Downloading OpenCode Desktop " + tagName + "...", 30);
DownloadFileWithProgress(downloadUrl, downloadPath, expectedSize, "OpenCode Desktop " + tagName, 30, 70);
}
SetStatus("Starting the OpenCode Desktop installer...", 75);
var process = Process.Start(new ProcessStartInfo
{
FileName = downloadPath,
UseShellExecute = true
});
if (process == null)
{
throw new InvalidOperationException("Could not start the OpenCode Desktop installer.");
}
SetStatus("Waiting briefly for the OpenCode Desktop installer...", 90);
if (process.WaitForExit(DesktopInstallerWaitMilliseconds))
{
if (process.ExitCode != 0)
{
throw new InvalidOperationException("The OpenCode Desktop installer ended with exit code " + process.ExitCode + ".");
}
return;
}
SetStatus("OpenCode Desktop installer is still running. Continuing with GESIS setup.", 95);
}
private void DownloadFileWithProgress(string url, string destinationPath, long expectedSize, string label, int startProgress, int endProgress)
{
var partialPath = destinationPath + ".download";
if (File.Exists(partialPath))
{
File.Delete(partialPath);
}
var request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
request.Timeout = DownloadTimeoutMilliseconds;
request.ReadWriteTimeout = DownloadTimeoutMilliseconds;
request.UserAgent = "GESIS-OpenCode-Installer";
request.Accept = "application/octet-stream";
using (var response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode != HttpStatusCode.OK)
{
throw new InvalidOperationException("Could not download OpenCode Desktop. The server returned " + response.StatusCode + ".");
}
var totalBytes = response.ContentLength > 0 ? response.ContentLength : expectedSize;
var downloadedBytes = 0L;
var buffer = new byte[1024 * 128];
var lastUpdate = DateTime.MinValue;
using (var input = response.GetResponseStream())
using (var output = File.Create(partialPath))
{
if (input == null)
{
throw new InvalidOperationException("Could not download OpenCode Desktop. The server returned no file content.");
}
while (true)
{
var read = input.Read(buffer, 0, buffer.Length);
if (read <= 0)
{
break;
}
output.Write(buffer, 0, read);
downloadedBytes += read;
if ((DateTime.Now - lastUpdate).TotalMilliseconds >= 250)
{
lastUpdate = DateTime.Now;
UpdateDownloadStatus(label, downloadedBytes, totalBytes, startProgress, endProgress);
}
}
}
if (expectedSize > 0 && downloadedBytes != expectedSize)
{
throw new InvalidOperationException("The OpenCode Desktop download was incomplete. Please check your internet connection and try again.");
}
if (File.Exists(destinationPath))
{
File.Delete(destinationPath);
}
File.Move(partialPath, destinationPath);
UpdateDownloadStatus(label, downloadedBytes, totalBytes, startProgress, endProgress);
}
}
private void UpdateDownloadStatus(string label, long downloadedBytes, long totalBytes, int startProgress, int endProgress)
{
var downloadedMb = downloadedBytes / 1024.0 / 1024.0;
if (totalBytes > 0)
{
var totalMb = totalBytes / 1024.0 / 1024.0;
var ratio = Math.Max(0, Math.Min(1, downloadedBytes / (double)totalBytes));
var progress = startProgress + (int)((endProgress - startProgress) * ratio);
SetStatus("Downloading " + label + "... " + downloadedMb.ToString("0.0") + " MB of " + totalMb.ToString("0.0") + " MB", progress);
return;
}
SetStatus("Downloading " + label + "... " + downloadedMb.ToString("0.0") + " MB", startProgress);
}
private void WriteOpenCodeConfig(string apiKey, List<ModelOption> selectedModels)
{
SetStatus("Writing OpenCode configuration...", 85);
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var configDirectory = Path.Combine(userProfile, ".config", "opencode");
var configPath = Path.Combine(configDirectory, "opencode.json");
Directory.CreateDirectory(configDirectory);
if (File.Exists(configPath))
{
var backupPath = configPath + ".backup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss");
File.Copy(configPath, backupPath, true);
}
File.WriteAllText(configPath, BuildConfigJson(apiKey, selectedModels), new UTF8Encoding(false));
}
private static string BuildConfigJson(string apiKey, List<ModelOption> selectedModels)
{
var modelsJson = new StringBuilder();
for (var i = 0; i < selectedModels.Count; i++)
{
var model = selectedModels[i];
modelsJson.Append(" \"");
modelsJson.Append(JsonEscape(model.Id));
modelsJson.Append("\": { \"name\": \"");
modelsJson.Append(JsonEscape(model.Name));
modelsJson.Append("\" }");
if (i < selectedModels.Count - 1)
{
modelsJson.Append(",");
}
modelsJson.Append("\n");
}
return "{\n" +
" \"$schema\": \"https://opencode.ai/config.json\",\n" +
" \"enabled_providers\": [\"gesis\"],\n" +
" \"disabled_providers\": [\"zen\", \"opencode\"],\n" +
" \"provider\": {\n" +
" \"gesis\": {\n" +
" \"npm\": \"@ai-sdk/openai-compatible\",\n" +
" \"name\": \"GESIS AI Server\",\n" +
" \"options\": {\n" +
" \"baseURL\": \"" + JsonEscape(BaseUrl) + "\",\n" +
" \"apiKey\": \"" + JsonEscape(apiKey) + "\"\n" +
" },\n" +
" \"models\": {\n" +
modelsJson +
" }\n" +
" }\n" +
" }\n" +
"}\n";
}
private List<ModelOption> ProbeAvailableModels(string apiKey)
{
var endpoints = new[]
{
BaseUrl.TrimEnd('/') + "/models",
BaseUrl.TrimEnd('/') + "/v1/models"
};
Exception lastError = null;
var sawEmptyModelList = false;
foreach (var endpoint in endpoints)
{
try
{
SetStatus("Checking available models...", 20);
var json = DownloadString(endpoint, apiKey);
var models = ParseModels(json);
if (models.Count == 0)
{
sawEmptyModelList = true;
throw new InvalidOperationException("The GESIS AI Server returned no models for this API key.");
}
models.Sort((left, right) => String.Compare(left.Name, right.Name, StringComparison.CurrentCultureIgnoreCase));
return models;
}
catch (WebException ex)
{
lastError = ex;
var response = ex.Response as HttpWebResponse;
if (response != null && (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden))
{
throw new InvalidOperationException("The API key was rejected by the GESIS AI Server.");
}
}
catch (Exception ex)
{
lastError = ex;
}
}
if (lastError is WebException)
{
throw new InvalidOperationException("The installer could not reach the GESIS AI Server or could not retrieve its model list.");
}
if (sawEmptyModelList)
{
throw new InvalidOperationException("No models are available for this API key.");
}
throw new InvalidOperationException("The GESIS AI Server returned a model list in an unexpected format.");
}
private static List<ModelOption> ParseModels(string json)
{
var serializer = new JavaScriptSerializer();
var parsed = serializer.DeserializeObject(json);
var modelItems = FindModelItems(parsed);
if (modelItems == null)
{
throw new InvalidOperationException("The server returned a model list in an unexpected format.");
}
var byId = new Dictionary<string, ModelOption>(StringComparer.OrdinalIgnoreCase);
foreach (var item in modelItems)
{
var model = ToModelOption(item);
if (model != null && !byId.ContainsKey(model.Id))
{
byId.Add(model.Id, model);
}
}
return new List<ModelOption>(byId.Values);
}
private static object[] FindModelItems(object parsed)
{
var array = parsed as object[];
if (array != null)
{
return array;
}
var dictionary = parsed as Dictionary<string, object>;
if (dictionary == null)
{
return null;
}
var keys = new[] { "data", "models" };
foreach (var key in keys)
{
if (dictionary.ContainsKey(key))
{
var nestedArray = dictionary[key] as object[];
if (nestedArray != null)
{
return nestedArray;
}
}
}
return null;
}
private static ModelOption ToModelOption(object item)
{
var id = "";
var name = "";
var text = item as string;
if (text != null)
{
id = text.Trim();
name = PrettyName(id);
}
else
{
var dictionary = item as Dictionary<string, object>;
if (dictionary == null)
{
return null;
}
id = GetStringValue(dictionary, "id");
if (id.Length == 0)
{
id = GetStringValue(dictionary, "model");
}
name = GetStringValue(dictionary, "name");
if (name.Length == 0)
{
name = GetStringValue(dictionary, "title");
}
if (name.Length == 0)
{
name = PrettyName(id);
}
}
if (id.Length == 0)
{
return null;
}
return new ModelOption(id, name);
}
private static string GetStringValue(Dictionary<string, object> dictionary, string key)
{
if (!dictionary.ContainsKey(key) || dictionary[key] == null)
{
return "";
}
return Convert.ToString(dictionary[key]).Trim();
}
private static string PrettyName(string modelId)
{
if (String.IsNullOrWhiteSpace(modelId))
{
return "";
}
return modelId.Replace("-", " ").Replace("_", " ");
}
private List<ModelOption> GetSelectedModels()
{
SyncVisibleModelSelection();
var selected = new List<ModelOption>();
foreach (var model in availableModels)
{
if (model.IsSelected)
{
selected.Add(model);
}
}
return selected;
}
private void RefreshModelList()
{
var filter = modelFilterBox.Text.Trim();
modelGridPanel.SuspendLayout();
try
{
modelGridPanel.Controls.Clear();
modelGridPanel.ColumnStyles.Clear();
modelGridPanel.RowStyles.Clear();
var visibleModels = new List<ModelOption>();
foreach (var model in availableModels)
{
if (filter.Length > 0 &&
model.Id.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0 &&
model.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
visibleModels.Add(model);
}
var columns = GetModelGridColumnCount();
modelGridPanel.ColumnCount = columns;
modelGridPanel.RowCount = Math.Max(1, (int)Math.Ceiling(visibleModels.Count / (double)columns));
for (var column = 0; column < columns; column++)
{
modelGridPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F / columns));
}
for (var row = 0; row < modelGridPanel.RowCount; row++)
{
modelGridPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
}
for (var i = 0; i < visibleModels.Count; i++)
{
var model = visibleModels[i];
var checkBox = new CheckBox
{
Text = model.ToString(),
Checked = model.IsSelected,
AutoSize = true,
MaximumSize = new Size(GetModelColumnTextWidth(columns), 0),
Margin = new Padding(0, 4, 18, 4),
Tag = model
};
checkBox.CheckedChanged += ModelCheckBox_CheckedChanged;
var row = i / columns;
var column = i % columns;
modelGridPanel.Controls.Add(checkBox, column, row);
}
}
finally
{
modelGridPanel.ResumeLayout(true);
}
}
private void SyncVisibleModelSelection()
{
foreach (Control control in modelGridPanel.Controls)
{
var checkBox = control as CheckBox;
if (checkBox == null)
{
continue;
}
var model = checkBox.Tag as ModelOption;
if (model != null)
{
model.IsSelected = checkBox.Checked;
}
}
}
private void SetVisibleModelSelection(bool selected)
{
foreach (Control control in modelGridPanel.Controls)
{
var checkBox = control as CheckBox;
if (checkBox == null)
{
continue;
}
var model = checkBox.Tag as ModelOption;
if (model != null)
{
model.IsSelected = selected;
checkBox.Checked = selected;
}
}
}
private void ModelCheckBox_CheckedChanged(object sender, EventArgs e)
{
var checkBox = sender as CheckBox;
if (checkBox == null)
{
return;
}
var model = checkBox.Tag as ModelOption;
if (model != null)
{
model.IsSelected = checkBox.Checked;
}
}
private int GetModelGridColumnCount()
{
var workingArea = Screen.FromControl(this).WorkingArea;
if (workingArea.Width >= 1200 && availableModels.Count >= 9)
{
return 3;
}
if (workingArea.Width >= 800 && availableModels.Count >= 4)
{
return 2;
}
return 1;
}
private int GetModelColumnTextWidth(int columns)
{
var contentWidth = Math.Max(420, ClientSize.Width - 96);
return Math.Max(220, (contentWidth / Math.Max(1, columns)) - 28);
}
private void ResizeForModelGrid()
{
if (InvokeRequired)
{
BeginInvoke(new Action(ResizeForModelGrid));
return;
}
if (WindowState == FormWindowState.Maximized)
{
return;
}
var workingArea = Screen.FromControl(this).WorkingArea;
var maxWidth = (int)(workingArea.Width * 0.92);
var maxHeight = (int)(workingArea.Height * 0.92);
var columns = GetModelGridColumnCount();
var rows = Math.Max(1, (int)Math.Ceiling(availableModels.Count / (double)columns));
var desiredWidth = columns >= 3 ? 980 : columns == 2 ? 820 : 680;
var desiredHeight = 430 + (rows * Math.Max(34, Font.Height + 18));
Width = Math.Min(Math.Max(Width, desiredWidth), maxWidth);
Height = Math.Min(Math.Max(Height, desiredHeight), maxHeight);
CenterToScreen();
}
private static string JsonEscape(string value)
{
if (value == null)
{
return "";
}
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\r", "\\r")
.Replace("\n", "\\n")
.Replace("\t", "\\t");
}
private static string DownloadString(string url)
{
using (var client = CreateWebClient())
{
return client.DownloadString(url);
}
}
private static string DownloadString(string url, string apiKey)
{
using (var client = CreateWebClient())
{
client.Headers.Add("Authorization", "Bearer " + apiKey);
return client.DownloadString(url);
}
}
private static WebClient CreateWebClient()
{
var client = new WebClient();
client.Headers.Add("Accept", "application/vnd.github+json");
client.Headers.Add("User-Agent", "GESIS-OpenCode-Installer");
return client;
}
private void SetBusy(bool busy)
{
installButton.Enabled = !busy;
checkApiKeyButton.Enabled = !busy;
backButton.Enabled = !busy;
selectAllButton.Enabled = !busy;
selectNoneButton.Enabled = !busy;
apiKeyBox.Enabled = !busy;
if (!busy && installButton.Visible)
{
apiKeyBox.Enabled = false;
}
modelFilterBox.Enabled = !busy;
modelGridPanel.Enabled = !busy;
skipDesktopCheckBox.Enabled = !busy;
progressBar.Style = busy ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous;
if (!busy)
{
progressBar.Style = ProgressBarStyle.Continuous;
}
}
private void SetStatus(string text, int progress)
{
if (InvokeRequired)
{
BeginInvoke(new Action<string, int>(SetStatus), text, progress);
return;
}
progressBar.Style = ProgressBarStyle.Continuous;
progressBar.Value = Math.Max(0, Math.Min(100, progress));
statusLabel.Text = text;
}
}
internal sealed class ModelOption
{
public ModelOption(string id, string name)
{
Id = id;
Name = String.IsNullOrWhiteSpace(name) ? id : name;
IsSelected = true;
}
public string Id { get; private set; }
public string Name { get; private set; }
public bool IsSelected { get; set; }
public override string ToString()
{
if (String.Equals(Id, Name, StringComparison.OrdinalIgnoreCase))
{
return Id;
}
return Name + " (" + Id + ")";
}
}
}