diff --git a/ShareX.HelpersLib/Helpers/ImageHelpers.cs b/ShareX.HelpersLib/Helpers/ImageHelpers.cs index 5ebf5175c..9443183b1 100644 --- a/ShareX.HelpersLib/Helpers/ImageHelpers.cs +++ b/ShareX.HelpersLib/Helpers/ImageHelpers.cs @@ -3077,5 +3077,21 @@ namespace ShareX.HelpersLib } } } + + public static string ImageToBase64(Image image, ImageFormat format) + { + using (MemoryStream ms = new MemoryStream()) + { + image.Save(ms, format); + byte[] imageBytes = ms.ToArray(); + return Convert.ToBase64String(imageBytes); + } + } + + public static string ImageFileToBase64(string path) + { + byte[] imageBytes = File.ReadAllBytes(path); + return Convert.ToBase64String(imageBytes); + } } -} \ No newline at end of file +} diff --git a/ShareX/Tools/AI/AIForm.cs b/ShareX/Tools/AI/AIForm.cs index 4e0a16978..b44075503 100644 --- a/ShareX/Tools/AI/AIForm.cs +++ b/ShareX/Tools/AI/AIForm.cs @@ -58,16 +58,32 @@ namespace ShareX private void UpdateControls() { - btnAnalyze.Enabled = !string.IsNullOrEmpty(Options.ChatGPTAPIKey) && (!string.IsNullOrEmpty(txtImage.Text) || pbImage.Image != null); + btnAnalyze.Enabled = IsAPIKeyAvailable() && (!string.IsNullOrEmpty(txtImage.Text) || pbImage.Image != null); btnResultCopy.Enabled = !string.IsNullOrEmpty(txtResult.Text); } + private bool IsAPIKeyAvailable() + { + switch (Options.Provider) + { + case AIProvider.OpenAI: + case AIProvider.Custom: + return !string.IsNullOrEmpty(Options.OpenAIAPIKey); + case AIProvider.Gemini: + return !string.IsNullOrEmpty(Options.GeminiAPIKey); + case AIProvider.OpenRouter: + return !string.IsNullOrEmpty(Options.OpenRouterAPIKey); + default: + return false; + } + } + private async Task AnalyzeImage() { txtResult.Clear(); lblTimer.ResetText(); - if (!string.IsNullOrEmpty(Options.ChatGPTAPIKey) && (!string.IsNullOrEmpty(txtImage.Text) || pbImage.Image != null)) + if (IsAPIKeyAvailable() && (!string.IsNullOrEmpty(txtImage.Text) || pbImage.Image != null)) { btnAnalyze.Enabled = false; Cursor = Cursors.WaitCursor; @@ -78,16 +94,16 @@ namespace ShareX try { - ChatGPT chatGPT = new ChatGPT(Options.ChatGPTAPIKey, Options.Model); + IAIProvider provider = AIProviderFactory.GetProvider(Options); string result = null; string imagePath = txtImage.Text; if (!string.IsNullOrEmpty(imagePath)) { - result = await chatGPT.AnalyzeImage(imagePath, Options.Input, Options.ReasoningEffort, Options.Verbosity); + result = await provider.AnalyzeImage(imagePath, Options.Input, Options.ReasoningEffort, Options.Verbosity); } else if (pbImage.Image != null) { - result = await chatGPT.AnalyzeImage(pbImage.Image, Options.Input, Options.ReasoningEffort, Options.Verbosity); + result = await provider.AnalyzeImage(pbImage.Image, Options.Input, Options.ReasoningEffort, Options.Verbosity); } if (!string.IsNullOrEmpty(result)) @@ -217,4 +233,4 @@ namespace ShareX } } } -} \ No newline at end of file +} diff --git a/ShareX/Tools/AI/AIOptions.cs b/ShareX/Tools/AI/AIOptions.cs index 5d8601c49..0dbf548ec 100644 --- a/ShareX/Tools/AI/AIOptions.cs +++ b/ShareX/Tools/AI/AIOptions.cs @@ -27,11 +27,31 @@ using ShareX.HelpersLib; namespace ShareX { + public enum AIProvider + { + OpenAI, + Gemini, + OpenRouter, + Custom + } + public class AIOptions { - public string Model { get; set; } = "gpt-5-mini"; + public AIProvider Provider { get; set; } = AIProvider.OpenAI; + [JsonEncrypt] - public string ChatGPTAPIKey { get; set; } + public string OpenAIAPIKey { get; set; } + public string OpenAIModel { get; set; } = "gpt-4o-mini"; + public string OpenAICustomURL { get; set; } + + [JsonEncrypt] + public string GeminiAPIKey { get; set; } + public string GeminiModel { get; set; } = "gemini-1.5-flash-latest"; + + [JsonEncrypt] + public string OpenRouterAPIKey { get; set; } + public string OpenRouterModel { get; set; } = "google/gemini-flash-1.5"; + public string ReasoningEffort { get; set; } = "minimal"; public string Verbosity { get; set; } = "medium"; public string Input { get; set; } = "What is in this image?"; @@ -39,4 +59,4 @@ namespace ShareX public bool AutoStartAnalyze { get; set; } = true; public bool AutoCopyResult { get; set; } = false; } -} \ No newline at end of file +} diff --git a/ShareX/Tools/AI/AIOptionsForm.Designer.cs b/ShareX/Tools/AI/AIOptionsForm.Designer.cs index d969e8e5b..f32dc0370 100644 --- a/ShareX/Tools/AI/AIOptionsForm.Designer.cs +++ b/ShareX/Tools/AI/AIOptionsForm.Designer.cs @@ -29,134 +29,412 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AIOptionsForm)); - btnAPIKeyHelp = new System.Windows.Forms.Button(); - cbReasoningEffort = new System.Windows.Forms.ComboBox(); - lblReasoningEffort = new System.Windows.Forms.Label(); - txtAPIKey = new System.Windows.Forms.TextBox(); - lblAPIKey = new System.Windows.Forms.Label(); - cbModel = new System.Windows.Forms.ComboBox(); - lblModel = new System.Windows.Forms.Label(); - cbAutoStartRegion = new System.Windows.Forms.CheckBox(); - cbAutoStartAnalyze = new System.Windows.Forms.CheckBox(); - btnOK = new System.Windows.Forms.Button(); - btnCancel = new System.Windows.Forms.Button(); - cbAutoCopyResult = new System.Windows.Forms.CheckBox(); - cbVerbosity = new System.Windows.Forms.ComboBox(); - lblVerbosity = new System.Windows.Forms.Label(); - SuspendLayout(); + this.btnAPIKeyHelp = new System.Windows.Forms.Button(); + this.cbReasoningEffort = new System.Windows.Forms.ComboBox(); + this.lblReasoningEffort = new System.Windows.Forms.Label(); + this.cbAutoStartRegion = new System.Windows.Forms.CheckBox(); + this.cbAutoStartAnalyze = new System.Windows.Forms.CheckBox(); + this.btnOK = new System.Windows.Forms.Button(); + this.btnCancel = new System.Windows.Forms.Button(); + this.cbAutoCopyResult = new System.Windows.Forms.CheckBox(); + this.cbVerbosity = new System.Windows.Forms.ComboBox(); + this.lblVerbosity = new System.Windows.Forms.Label(); + this.lblProvider = new System.Windows.Forms.Label(); + this.cbProvider = new System.Windows.Forms.ComboBox(); + this.gbOpenAI = new System.Windows.Forms.GroupBox(); + this.lblOpenAICustomURL = new System.Windows.Forms.Label(); + this.txtOpenAICustomURL = new System.Windows.Forms.TextBox(); + this.lblOpenAIModel = new System.Windows.Forms.Label(); + this.cbOpenAIModel = new System.Windows.Forms.ComboBox(); + this.lblOpenAIAPIKey = new System.Windows.Forms.Label(); + this.txtOpenAIAPIKey = new System.Windows.Forms.TextBox(); + this.gbGemini = new System.Windows.Forms.GroupBox(); + this.lblGeminiModel = new System.Windows.Forms.Label(); + this.cbGeminiModel = new System.Windows.Forms.ComboBox(); + this.lblGeminiAPIKey = new System.Windows.Forms.Label(); + this.txtGeminiAPIKey = new System.Windows.Forms.TextBox(); + this.gbOpenRouter = new System.Windows.Forms.GroupBox(); + this.lblOpenRouterModel = new System.Windows.Forms.Label(); + this.cbOpenRouterModel = new System.Windows.Forms.ComboBox(); + this.lblOpenRouterAPIKey = new System.Windows.Forms.Label(); + this.txtOpenRouterAPIKey = new System.Windows.Forms.TextBox(); + this.gbOpenAI.SuspendLayout(); + this.gbGemini.SuspendLayout(); + this.gbOpenRouter.SuspendLayout(); + this.SuspendLayout(); // // btnAPIKeyHelp // - btnAPIKeyHelp.Image = Properties.Resources.question; - resources.ApplyResources(btnAPIKeyHelp, "btnAPIKeyHelp"); - btnAPIKeyHelp.Name = "btnAPIKeyHelp"; - btnAPIKeyHelp.UseVisualStyleBackColor = true; - btnAPIKeyHelp.Click += btnAPIKeyHelp_Click; + this.btnAPIKeyHelp.Image = global::ShareX.Properties.Resources.question; + resources.ApplyResources(this.btnAPIKeyHelp, "btnAPIKeyHelp"); + this.btnAPIKeyHelp.Name = "btnAPIKeyHelp"; + this.btnAPIKeyHelp.UseVisualStyleBackColor = true; + this.btnAPIKeyHelp.Click += new System.EventHandler(this.btnAPIKeyHelp_Click); // // cbReasoningEffort // - cbReasoningEffort.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - cbReasoningEffort.FormattingEnabled = true; - cbReasoningEffort.Items.AddRange(new object[] { resources.GetString("cbReasoningEffort.Items"), resources.GetString("cbReasoningEffort.Items1"), resources.GetString("cbReasoningEffort.Items2"), resources.GetString("cbReasoningEffort.Items3") }); - resources.ApplyResources(cbReasoningEffort, "cbReasoningEffort"); - cbReasoningEffort.Name = "cbReasoningEffort"; + this.cbReasoningEffort.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbReasoningEffort.FormattingEnabled = true; + this.cbReasoningEffort.Items.AddRange(new object[] { + resources.GetString("cbReasoningEffort.Items"), + resources.GetString("cbReasoningEffort.Items1"), + resources.GetString("cbReasoningEffort.Items2"), + resources.GetString("cbReasoningEffort.Items3")}); + resources.ApplyResources(this.cbReasoningEffort, "cbReasoningEffort"); + this.cbReasoningEffort.Name = "cbReasoningEffort"; // // lblReasoningEffort // - resources.ApplyResources(lblReasoningEffort, "lblReasoningEffort"); - lblReasoningEffort.Name = "lblReasoningEffort"; - // - // txtAPIKey - // - resources.ApplyResources(txtAPIKey, "txtAPIKey"); - txtAPIKey.Name = "txtAPIKey"; - txtAPIKey.UseSystemPasswordChar = true; - // - // lblAPIKey - // - resources.ApplyResources(lblAPIKey, "lblAPIKey"); - lblAPIKey.Name = "lblAPIKey"; - // - // cbModel - // - cbModel.FormattingEnabled = true; - cbModel.Items.AddRange(new object[] { resources.GetString("cbModel.Items"), resources.GetString("cbModel.Items1"), resources.GetString("cbModel.Items2") }); - resources.ApplyResources(cbModel, "cbModel"); - cbModel.Name = "cbModel"; - // - // lblModel - // - resources.ApplyResources(lblModel, "lblModel"); - lblModel.Name = "lblModel"; + resources.ApplyResources(this.lblReasoningEffort, "lblReasoningEffort"); + this.lblReasoningEffort.Name = "lblReasoningEffort"; // // cbAutoStartRegion // - resources.ApplyResources(cbAutoStartRegion, "cbAutoStartRegion"); - cbAutoStartRegion.Name = "cbAutoStartRegion"; - cbAutoStartRegion.UseVisualStyleBackColor = true; + resources.ApplyResources(this.cbAutoStartRegion, "cbAutoStartRegion"); + this.cbAutoStartRegion.Name = "cbAutoStartRegion"; + this.cbAutoStartRegion.UseVisualStyleBackColor = true; // // cbAutoStartAnalyze // - resources.ApplyResources(cbAutoStartAnalyze, "cbAutoStartAnalyze"); - cbAutoStartAnalyze.Name = "cbAutoStartAnalyze"; - cbAutoStartAnalyze.UseVisualStyleBackColor = true; + resources.ApplyResources(this.cbAutoStartAnalyze, "cbAutoStartAnalyze"); + this.cbAutoStartAnalyze.Name = "cbAutoStartAnalyze"; + this.cbAutoStartAnalyze.UseVisualStyleBackColor = true; // // btnOK // - resources.ApplyResources(btnOK, "btnOK"); - btnOK.Name = "btnOK"; - btnOK.UseVisualStyleBackColor = true; - btnOK.Click += btnOK_Click; + resources.ApplyResources(this.btnOK, "btnOK"); + this.btnOK.Name = "btnOK"; + this.btnOK.UseVisualStyleBackColor = true; + this.btnOK.Click += new System.EventHandler(this.btnOK_Click); // // btnCancel // - resources.ApplyResources(btnCancel, "btnCancel"); - btnCancel.Name = "btnCancel"; - btnCancel.UseVisualStyleBackColor = true; - btnCancel.Click += btnCancel_Click; + resources.ApplyResources(this.btnCancel, "btnCancel"); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.UseVisualStyleBackColor = true; + this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); // // cbAutoCopyResult // - resources.ApplyResources(cbAutoCopyResult, "cbAutoCopyResult"); - cbAutoCopyResult.Name = "cbAutoCopyResult"; - cbAutoCopyResult.UseVisualStyleBackColor = true; + resources.ApplyResources(this.cbAutoCopyResult, "cbAutoCopyResult"); + this.cbAutoCopyResult.Name = "cbAutoCopyResult"; + this.cbAutoCopyResult.UseVisualStyleBackColor = true; // // cbVerbosity // - cbVerbosity.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - cbVerbosity.FormattingEnabled = true; - cbVerbosity.Items.AddRange(new object[] { resources.GetString("cbVerbosity.Items"), resources.GetString("cbVerbosity.Items1"), resources.GetString("cbVerbosity.Items2") }); - resources.ApplyResources(cbVerbosity, "cbVerbosity"); - cbVerbosity.Name = "cbVerbosity"; + this.cbVerbosity.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbVerbosity.FormattingEnabled = true; + this.cbVerbosity.Items.AddRange(new object[] { + resources.GetString("cbVerbosity.Items"), + resources.GetString("cbVerbosity.Items1"), + resources.GetString("cbVerbosity.Items2")}); + resources.ApplyResources(this.cbVerbosity, "cbVerbosity"); + this.cbVerbosity.Name = "cbVerbosity"; // // lblVerbosity // - resources.ApplyResources(lblVerbosity, "lblVerbosity"); - lblVerbosity.Name = "lblVerbosity"; + resources.ApplyResources(this.lblVerbosity, "lblVerbosity"); + this.lblVerbosity.Name = "lblVerbosity"; + // + // lblProvider + // + resources.ApplyResources(this.lblProvider, "lblProvider"); + this.lblProvider.Name = "lblProvider"; + this.lblProvider.Text = "Select Provider:"; + // + // cbProvider + // + this.cbProvider.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbProvider.FormattingEnabled = true; + resources.ApplyResources(this.cbProvider, "cbProvider"); + this.cbProvider.Name = "cbProvider"; + this.cbProvider.SelectedIndexChanged += new System.EventHandler(this.cbProvider_SelectedIndexChanged); + // + // gbOpenAI + // + this.gbOpenAI.Controls.Add(this.lblOpenAICustomURL); + this.gbOpenAI.Controls.Add(this.txtOpenAICustomURL); + this.gbOpenAI.Controls.Add(this.lblOpenAIModel); + this.gbOpenAI.Controls.Add(this.cbOpenAIModel); + this.gbOpenAI.Controls.Add(this.lblOpenAIAPIKey); + this.gbOpenAI.Controls.Add(this.txtOpenAIAPIKey); + resources.ApplyResources(this.gbOpenAI, "gbOpenAI"); + this.gbOpenAI.Name = "gbOpenAI"; + this.gbOpenAI.TabStop = false; + this.gbOpenAI.Text = "Configure Provider"; + // + // lblOpenAICustomURL + // + resources.ApplyResources(this.lblOpenAICustomURL, "lblOpenAICustomURL"); + this.lblOpenAICustomURL.Name = "lblOpenAICustomURL"; + // + // txtOpenAICustomURL + // + resources.ApplyResources(this.txtOpenAICustomURL, "txtOpenAICustomURL"); + this.txtOpenAICustomURL.Name = "txtOpenAICustomURL"; + // + // lblOpenAIModel + // + resources.ApplyResources(this.lblOpenAIModel, "lblOpenAIModel"); + this.lblOpenAIModel.Name = "lblOpenAIModel"; + // + // cbOpenAIModel + // + this.cbOpenAIModel.FormattingEnabled = true; + resources.ApplyResources(this.cbOpenAIModel, "cbOpenAIModel"); + this.cbOpenAIModel.Name = "cbOpenAIModel"; + // + // lblOpenAIAPIKey + // + resources.ApplyResources(this.lblOpenAIAPIKey, "lblOpenAIAPIKey"); + this.lblOpenAIAPIKey.Name = "lblOpenAIAPIKey"; + // + // txtOpenAIAPIKey + // + resources.ApplyResources(this.txtOpenAIAPIKey, "txtOpenAIAPIKey"); + this.txtOpenAIAPIKey.Name = "txtOpenAIAPIKey"; + this.txtOpenAIAPIKey.UseSystemPasswordChar = true; + // + // gbGemini + // + this.gbGemini.Controls.Add(this.lblGeminiModel); + this.gbGemini.Controls.Add(this.cbGeminiModel); + this.gbGemini.Controls.Add(this.lblGeminiAPIKey); + this.gbGemini.Controls.Add(this.txtGeminiAPIKey); + resources.ApplyResources(this.gbGemini, "gbGemini"); + this.gbGemini.Name = "gbGemini"; + this.gbGemini.TabStop = false; + this.gbGemini.Text = "Configure Provider"; + // + // lblGeminiModel + // + resources.ApplyResources(this.lblGeminiModel, "lblGeminiModel"); + this.lblGeminiModel.Name = "lblGeminiModel"; + // + // cbGeminiModel + // + this.cbGeminiModel.FormattingEnabled = true; + resources.ApplyResources(this.cbGeminiModel, "cbGeminiModel"); + this.cbGeminiModel.Name = "cbGeminiModel"; + // + // lblGeminiAPIKey + // + resources.ApplyResources(this.lblGeminiAPIKey, "lblGeminiAPIKey"); + this.lblGeminiAPIKey.Name = "lblGeminiAPIKey"; + // + // txtGeminiAPIKey + // + resources.ApplyResources(this.txtGeminiAPIKey, "txtGeminiAPIKey"); + this.txtGeminiAPIKey.Name = "txtGeminiAPIKey"; + this.txtGeminiAPIKey.UseSystemPasswordChar = true; + // + // gbOpenRouter + // + this.gbOpenRouter.Controls.Add(this.lblOpenRouterModel); + this.gbOpenRouter.Controls.Add(this.cbOpenRouterModel); + this.gbOpenRouter.Controls.Add(this.lblOpenRouterAPIKey); + this.gbOpenRouter.Controls.Add(this.txtOpenRouterAPIKey); + resources.ApplyResources(this.gbOpenRouter, "gbOpenRouter"); + this.gbOpenRouter.Name = "gbOpenRouter"; + this.gbOpenRouter.TabStop = false; + this.gbOpenRouter.Text = "Configure Provider"; + // + // lblOpenRouterModel + // + resources.ApplyResources(this.lblOpenRouterModel, "lblOpenRouterModel"); + this.lblOpenRouterModel.Name = "lblOpenRouterModel"; + // + // cbOpenRouterModel + // + this.cbOpenRouterModel.FormattingEnabled = true; + resources.ApplyResources(this.cbOpenRouterModel, "cbOpenRouterModel"); + this.cbOpenRouterModel.Name = "cbOpenRouterModel"; + // + // lblOpenRouterAPIKey + // + resources.ApplyResources(this.lblOpenRouterAPIKey, "lblOpenRouterAPIKey"); + this.lblOpenRouterAPIKey.Name = "lblOpenRouterAPIKey"; + // + // txtOpenRouterAPIKey + // + resources.ApplyResources(this.txtOpenRouterAPIKey, "txtOpenRouterAPIKey"); + this.txtOpenRouterAPIKey.Name = "txtOpenRouterAPIKey"; + this.txtOpenRouterAPIKey.UseSystemPasswordChar = true; // // AIOptionsForm // resources.ApplyResources(this, "$this"); - AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - Controls.Add(lblVerbosity); - Controls.Add(cbVerbosity); - Controls.Add(cbAutoCopyResult); - Controls.Add(btnCancel); - Controls.Add(btnOK); - Controls.Add(cbAutoStartAnalyze); - Controls.Add(cbAutoStartRegion); - Controls.Add(btnAPIKeyHelp); - Controls.Add(cbReasoningEffort); - Controls.Add(lblReasoningEffort); - Controls.Add(txtAPIKey); - Controls.Add(lblAPIKey); - Controls.Add(cbModel); - Controls.Add(lblModel); - FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; - MaximizeBox = false; - Name = "AIOptionsForm"; - ResumeLayout(false); - PerformLayout(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.lblProvider); + this.Controls.Add(this.cbProvider); + this.Controls.Add(this.gbOpenAI); + this.Controls.Add(this.gbGemini); + this.Controls.Add(this.gbOpenRouter); + this.Controls.Add(this.lblVerbosity); + this.Controls.Add(this.cbVerbosity); + this.Controls.Add(this.cbAutoCopyResult); + this.Controls.Add(this.btnCancel); + this.Controls.Add(this.btnOK); + this.Controls.Add(this.cbAutoStartAnalyze); + this.Controls.Add(this.cbAutoStartRegion); + this.Controls.Add(this.btnAPIKeyHelp); + this.Controls.Add(this.cbReasoningEffort); + this.Controls.Add(this.lblReasoningEffort); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Name = "AIOptionsForm"; + this.gbOpenAI.ResumeLayout(false); + this.gbOpenAI.PerformLayout(); + this.gbGemini.ResumeLayout(false); + this.gbGemini.PerformLayout(); + this.gbOpenRouter.ResumeLayout(false); + this.gbOpenRouter.PerformLayout(); + + // Explicit manual layout overrides to ensure consistent UI regardless of stale .resx + // Increase form size to accommodate provider configuration group and other options + this.ClientSize = new System.Drawing.Size(420, 520); + + // Provider selection at the top + this.lblProvider.AutoSize = true; + this.lblProvider.Location = new System.Drawing.Point(16, 16); + + this.cbProvider.Location = new System.Drawing.Point(16, 36); + this.cbProvider.Size = new System.Drawing.Size(this.ClientSize.Width - 56, this.cbProvider.Height); + this.cbProvider.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + // Help button next to provider selector + this.btnAPIKeyHelp.Location = new System.Drawing.Point(this.ClientSize.Width - 16 - this.btnAPIKeyHelp.Width, 32); + this.btnAPIKeyHelp.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + // Recompute provider ComboBox width to avoid overlap with help button (leave 8px gap) + this.cbProvider.Size = new System.Drawing.Size(this.btnAPIKeyHelp.Left - 8 - this.cbProvider.Left, this.cbProvider.Height); + + // Group boxes share the same location/size; visibility toggled by provider selection + int groupLeft = 16; + int groupTop = 72; + int groupWidth = this.ClientSize.Width - 32; + int groupHeight = 210; + + this.gbOpenAI.Location = new System.Drawing.Point(groupLeft, groupTop); + this.gbOpenAI.Size = new System.Drawing.Size(groupWidth, groupHeight); + this.gbOpenAI.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + this.gbGemini.Location = new System.Drawing.Point(groupLeft, groupTop); + this.gbGemini.Size = new System.Drawing.Size(groupWidth, groupHeight); + this.gbGemini.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + this.gbOpenRouter.Location = new System.Drawing.Point(groupLeft, groupTop); + this.gbOpenRouter.Size = new System.Drawing.Size(groupWidth, groupHeight); + this.gbOpenRouter.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + // OpenAI group contents + this.lblOpenAIAPIKey.Location = new System.Drawing.Point(12, 24); + this.txtOpenAIAPIKey.Location = new System.Drawing.Point(12, 44); + this.txtOpenAIAPIKey.Size = new System.Drawing.Size(groupWidth - 24, this.txtOpenAIAPIKey.Height); + + this.lblOpenAIModel.Location = new System.Drawing.Point(12, 80); + this.cbOpenAIModel.Location = new System.Drawing.Point(12, 100); + this.cbOpenAIModel.Size = new System.Drawing.Size(groupWidth - 24, this.cbOpenAIModel.Height); + + this.lblOpenAICustomURL.Location = new System.Drawing.Point(12, 136); + this.txtOpenAICustomURL.Location = new System.Drawing.Point(12, 156); + this.txtOpenAICustomURL.Size = new System.Drawing.Size(groupWidth - 24, this.txtOpenAICustomURL.Height); + + // Ensure labels have text and autosize; inputs expand horizontally + this.lblOpenAIAPIKey.AutoSize = true; + this.lblOpenAIAPIKey.Text = "API key:"; + this.txtOpenAIAPIKey.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + this.lblOpenAIModel.AutoSize = true; + this.lblOpenAIModel.Text = "Model:"; + this.cbOpenAIModel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.cbOpenAIModel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDown; + + this.lblOpenAICustomURL.AutoSize = true; + this.lblOpenAICustomURL.Text = "Custom base URL:"; + this.txtOpenAICustomURL.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + // Gemini group contents + this.lblGeminiAPIKey.Location = new System.Drawing.Point(12, 24); + this.txtGeminiAPIKey.Location = new System.Drawing.Point(12, 44); + this.txtGeminiAPIKey.Size = new System.Drawing.Size(groupWidth - 24, this.txtGeminiAPIKey.Height); + + this.lblGeminiModel.Location = new System.Drawing.Point(12, 80); + this.cbGeminiModel.Location = new System.Drawing.Point(12, 100); + this.cbGeminiModel.Size = new System.Drawing.Size(groupWidth - 24, this.cbGeminiModel.Height); + + // Ensure labels have text and autosize; inputs expand horizontally (Gemini) + this.lblGeminiAPIKey.AutoSize = true; + this.lblGeminiAPIKey.Text = "API key:"; + this.txtGeminiAPIKey.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + this.lblGeminiModel.AutoSize = true; + this.lblGeminiModel.Text = "Model:"; + this.cbGeminiModel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.cbGeminiModel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDown; + + // OpenRouter group contents + this.lblOpenRouterAPIKey.Location = new System.Drawing.Point(12, 24); + this.txtOpenRouterAPIKey.Location = new System.Drawing.Point(12, 44); + this.txtOpenRouterAPIKey.Size = new System.Drawing.Size(groupWidth - 24, this.txtOpenRouterAPIKey.Height); + + this.lblOpenRouterModel.Location = new System.Drawing.Point(12, 80); + this.cbOpenRouterModel.Location = new System.Drawing.Point(12, 100); + this.cbOpenRouterModel.Size = new System.Drawing.Size(groupWidth - 24, this.cbOpenRouterModel.Height); + + // Ensure labels have text and autosize; inputs expand horizontally (OpenRouter) + this.lblOpenRouterAPIKey.AutoSize = true; + this.lblOpenRouterAPIKey.Text = "API key:"; + this.txtOpenRouterAPIKey.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + + this.lblOpenRouterModel.AutoSize = true; + this.lblOpenRouterModel.Text = "Model:"; + this.cbOpenRouterModel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.cbOpenRouterModel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDown; + + // Remaining options stacked under the provider config group + // Test connection button and status + this.btnTestConnection = new System.Windows.Forms.Button(); + this.lblTestStatus = new System.Windows.Forms.Label(); + + this.btnTestConnection.Text = "Test"; + this.btnTestConnection.Size = new System.Drawing.Size(80, 28); + this.btnTestConnection.Location = new System.Drawing.Point(this.ClientSize.Width - 16 - this.btnTestConnection.Width, groupTop + groupHeight + 8); + this.btnTestConnection.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnTestConnection.Click += new System.EventHandler(this.btnTestConnection_Click); + + this.lblTestStatus.AutoSize = true; + this.lblTestStatus.Location = new System.Drawing.Point(16, groupTop + groupHeight + 12); + this.lblTestStatus.Text = ""; + this.Controls.Add(this.lblTestStatus); + this.Controls.Add(this.btnTestConnection); + + int y = groupTop + groupHeight + 12 + 36; + + this.lblReasoningEffort.Location = new System.Drawing.Point(16, y); + this.cbReasoningEffort.Location = new System.Drawing.Point(16, y + 24); + this.cbReasoningEffort.Size = new System.Drawing.Size(this.ClientSize.Width - 32, this.cbReasoningEffort.Height); + this.cbReasoningEffort.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + y += 24 + 24 + 12; + + this.lblVerbosity.Location = new System.Drawing.Point(16, y); + this.cbVerbosity.Location = new System.Drawing.Point(16, y + 24); + this.cbVerbosity.Size = new System.Drawing.Size(this.ClientSize.Width - 32, this.cbVerbosity.Height); + this.cbVerbosity.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + y += 24 + 24 + 12; + + this.cbAutoStartRegion.Location = new System.Drawing.Point(16, y); + this.cbAutoStartAnalyze.Location = new System.Drawing.Point(16, y + 24); + this.cbAutoCopyResult.Location = new System.Drawing.Point(16, y + 48); + + // OK/Cancel at bottom-right + this.btnOK.Location = new System.Drawing.Point(this.ClientSize.Width - 224, this.ClientSize.Height - 48); + this.btnCancel.Location = new System.Drawing.Point(this.ClientSize.Width - 112, this.ClientSize.Height - 48); + this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + + this.ResumeLayout(false); + this.PerformLayout(); + } #endregion @@ -164,10 +442,6 @@ private System.Windows.Forms.Button btnAPIKeyHelp; private System.Windows.Forms.ComboBox cbReasoningEffort; private System.Windows.Forms.Label lblReasoningEffort; - private System.Windows.Forms.TextBox txtAPIKey; - private System.Windows.Forms.Label lblAPIKey; - private System.Windows.Forms.ComboBox cbModel; - private System.Windows.Forms.Label lblModel; private System.Windows.Forms.CheckBox cbAutoStartRegion; private System.Windows.Forms.CheckBox cbAutoStartAnalyze; private System.Windows.Forms.Button btnOK; @@ -175,5 +449,26 @@ private System.Windows.Forms.CheckBox cbAutoCopyResult; private System.Windows.Forms.ComboBox cbVerbosity; private System.Windows.Forms.Label lblVerbosity; + private System.Windows.Forms.Label lblProvider; + private System.Windows.Forms.ComboBox cbProvider; + private System.Windows.Forms.GroupBox gbOpenAI; + private System.Windows.Forms.Label lblOpenAIModel; + private System.Windows.Forms.ComboBox cbOpenAIModel; + private System.Windows.Forms.Label lblOpenAIAPIKey; + private System.Windows.Forms.TextBox txtOpenAIAPIKey; + private System.Windows.Forms.GroupBox gbGemini; + private System.Windows.Forms.Label lblGeminiModel; + private System.Windows.Forms.ComboBox cbGeminiModel; + private System.Windows.Forms.Label lblGeminiAPIKey; + private System.Windows.Forms.TextBox txtGeminiAPIKey; + private System.Windows.Forms.GroupBox gbOpenRouter; + private System.Windows.Forms.Label lblOpenRouterModel; + private System.Windows.Forms.ComboBox cbOpenRouterModel; + private System.Windows.Forms.Label lblOpenRouterAPIKey; + private System.Windows.Forms.TextBox txtOpenRouterAPIKey; + private System.Windows.Forms.Label lblOpenAICustomURL; + private System.Windows.Forms.TextBox txtOpenAICustomURL; + private System.Windows.Forms.Button btnTestConnection; + private System.Windows.Forms.Label lblTestStatus; } -} \ No newline at end of file +} diff --git a/ShareX/Tools/AI/AIOptionsForm.cs b/ShareX/Tools/AI/AIOptionsForm.cs index 99dab4a89..529652824 100644 --- a/ShareX/Tools/AI/AIOptionsForm.cs +++ b/ShareX/Tools/AI/AIOptionsForm.cs @@ -25,6 +25,9 @@ using ShareX.HelpersLib; using System; +using System.Drawing; +using System.Net.Http; +using System.Net.Http.Headers; using System.Windows.Forms; namespace ShareX @@ -39,13 +42,28 @@ namespace ShareX InitializeComponent(); ShareXResources.ApplyTheme(this, true); + foreach (AIProvider provider in Enum.GetValues(typeof(AIProvider))) + { + cbProvider.Items.Add(provider.ToString()); + } + LoadOptions(); } private void LoadOptions() { - cbModel.Text = Options.Model; - txtAPIKey.Text = Options.ChatGPTAPIKey; + cbProvider.SelectedIndex = (int)Options.Provider; + + txtOpenAIAPIKey.Text = Options.OpenAIAPIKey; + cbOpenAIModel.Text = Options.OpenAIModel; + txtOpenAICustomURL.Text = Options.OpenAICustomURL; + + txtGeminiAPIKey.Text = Options.GeminiAPIKey; + cbGeminiModel.Text = Options.GeminiModel; + + txtOpenRouterAPIKey.Text = Options.OpenRouterAPIKey; + cbOpenRouterModel.Text = Options.OpenRouterModel; + int index = cbReasoningEffort.FindStringExact(Options.ReasoningEffort); if (index >= 0) { @@ -71,8 +89,18 @@ namespace ShareX private void SaveOptions() { - Options.Model = cbModel.Text; - Options.ChatGPTAPIKey = txtAPIKey.Text; + Options.Provider = (AIProvider)cbProvider.SelectedIndex; + + Options.OpenAIAPIKey = txtOpenAIAPIKey.Text; + Options.OpenAIModel = cbOpenAIModel.Text; + Options.OpenAICustomURL = txtOpenAICustomURL.Text; + + Options.GeminiAPIKey = txtGeminiAPIKey.Text; + Options.GeminiModel = cbGeminiModel.Text; + + Options.OpenRouterAPIKey = txtOpenRouterAPIKey.Text; + Options.OpenRouterModel = cbOpenRouterModel.Text; + Options.ReasoningEffort = cbReasoningEffort.Text; Options.Verbosity = cbVerbosity.Text; Options.AutoStartRegion = cbAutoStartRegion.Checked; @@ -80,9 +108,159 @@ namespace ShareX Options.AutoCopyResult = cbAutoCopyResult.Checked; } + private void cbProvider_SelectedIndexChanged(object sender, EventArgs e) + { + gbOpenAI.Visible = false; + gbGemini.Visible = false; + gbOpenRouter.Visible = false; + lblOpenAICustomURL.Visible = false; + txtOpenAICustomURL.Visible = false; + + switch ((AIProvider)cbProvider.SelectedIndex) + { + case AIProvider.OpenAI: + gbOpenAI.Visible = true; + break; + case AIProvider.Custom: + gbOpenAI.Visible = true; + lblOpenAICustomURL.Visible = true; + txtOpenAICustomURL.Visible = true; + break; + case AIProvider.Gemini: + gbGemini.Visible = true; + break; + case AIProvider.OpenRouter: + gbOpenRouter.Visible = true; + break; + } + } + + private async void btnTestConnection_Click(object sender, EventArgs e) + { + try + { + btnTestConnection.Enabled = false; + lblTestStatus.ForeColor = Color.Gold; + lblTestStatus.Text = "Testing..."; + + var client = HttpClientFactory.Create(); + HttpRequestMessage req = null; + var provider = (AIProvider)cbProvider.SelectedIndex; + + switch (provider) + { + case AIProvider.OpenAI: + case AIProvider.Custom: + { + var key = txtOpenAIAPIKey.Text?.Trim(); + if (string.IsNullOrEmpty(key)) + { + lblTestStatus.ForeColor = Color.IndianRed; + lblTestStatus.Text = "Missing OpenAI API key."; + return; + } + + var baseUrl = txtOpenAICustomURL.Text; + if (string.IsNullOrWhiteSpace(baseUrl)) + baseUrl = "https://api.openai.com/v1"; + baseUrl = baseUrl.Trim().TrimEnd('/'); + + var url = baseUrl + "/models"; + req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key); + break; + } + + case AIProvider.Gemini: + { + var key = txtGeminiAPIKey.Text?.Trim(); + if (string.IsNullOrEmpty(key)) + { + lblTestStatus.ForeColor = Color.IndianRed; + lblTestStatus.Text = "Missing Gemini API key."; + return; + } + + var url = "https://generativelanguage.googleapis.com/v1beta/models?key=" + Uri.EscapeDataString(key); + req = new HttpRequestMessage(HttpMethod.Get, url); + break; + } + + case AIProvider.OpenRouter: + { + var key = txtOpenRouterAPIKey.Text?.Trim(); + if (string.IsNullOrEmpty(key)) + { + lblTestStatus.ForeColor = Color.IndianRed; + lblTestStatus.Text = "Missing OpenRouter API key."; + return; + } + + var url = "https://openrouter.ai/api/v1/models"; + req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key); + break; + } + + default: + lblTestStatus.ForeColor = Color.IndianRed; + lblTestStatus.Text = "Select a provider first."; + return; + } + + using (req) + using (var resp = await client.SendAsync(req)) + { + var ok = (int)resp.StatusCode >= 200 && (int)resp.StatusCode < 300; + var text = await resp.Content.ReadAsStringAsync(); + + if (ok) + { + lblTestStatus.ForeColor = Color.LimeGreen; + lblTestStatus.Text = "Connection OK"; + } + else + { + lblTestStatus.ForeColor = Color.IndianRed; + string summary = resp.ReasonPhrase; + if (string.IsNullOrWhiteSpace(summary)) summary = resp.StatusCode.ToString(); + if (!string.IsNullOrWhiteSpace(text)) + { + if (text.Length > 200) text = text.Substring(0, 200) + "..."; + summary += ": " + text; + } + lblTestStatus.Text = summary; + } + } + } + catch (Exception ex) + { + lblTestStatus.ForeColor = Color.IndianRed; + lblTestStatus.Text = ex.Message; + } + finally + { + btnTestConnection.Enabled = true; + } + } + private void btnAPIKeyHelp_Click(object sender, EventArgs e) { - URLHelpers.OpenURL("https://platform.openai.com/api-keys"); + string url = ""; + switch ((AIProvider)cbProvider.SelectedIndex) + { + case AIProvider.OpenAI: + case AIProvider.Custom: + url = "https://platform.openai.com/api-keys"; + break; + case AIProvider.Gemini: + url = "https://aistudio.google.com/app/apikey"; + break; + case AIProvider.OpenRouter: + url = "https://openrouter.ai/keys"; + break; + } + URLHelpers.OpenURL(url); } private void btnOK_Click(object sender, EventArgs e) @@ -99,4 +277,4 @@ namespace ShareX Close(); } } -} \ No newline at end of file +} diff --git a/ShareX/Tools/AI/AIProviderFactory.cs b/ShareX/Tools/AI/AIProviderFactory.cs new file mode 100644 index 000000000..97b693352 --- /dev/null +++ b/ShareX/Tools/AI/AIProviderFactory.cs @@ -0,0 +1,49 @@ +#region License Information (GPL v3) + +/* + ShareX - A program that allows you to take screenshots and share any file type + Copyright (c) 2007-2025 ShareX Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + Optionally you can also view the license at . +*/ + +#endregion License Information (GPL v3) + +using System; + +namespace ShareX +{ + public static class AIProviderFactory + { + public static IAIProvider GetProvider(AIOptions options) + { + switch (options.Provider) + { + case AIProvider.OpenAI: + return new OpenAIProvider(options.OpenAIAPIKey, options.OpenAIModel); + case AIProvider.Custom: + return new OpenAIProvider(options.OpenAIAPIKey, options.OpenAIModel, options.OpenAICustomURL); + case AIProvider.Gemini: + return new GeminiProvider(options.GeminiAPIKey, options.GeminiModel); + case AIProvider.OpenRouter: + return new OpenRouterProvider(options.OpenRouterAPIKey, options.OpenRouterModel); + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/ShareX/Tools/AI/GeminiProvider.cs b/ShareX/Tools/AI/GeminiProvider.cs new file mode 100644 index 000000000..70cfa0b0e --- /dev/null +++ b/ShareX/Tools/AI/GeminiProvider.cs @@ -0,0 +1,98 @@ +#region License Information (GPL v3) + +/* + ShareX - A program that allows you to take screenshots and share any file type + Copyright (c) 2007-2025 ShareX Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + Optionally you can also view the license at . +*/ + +#endregion License Information (GPL v3) + +using Newtonsoft.Json; +using ShareX.HelpersLib; +using System; +using System.Drawing; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace ShareX +{ + public class GeminiProvider : IAIProvider + { + private readonly string apiKey; + private readonly string model; + + public GeminiProvider(string apiKey, string model) + { + this.apiKey = apiKey; + this.model = model; + } + + public async Task AnalyzeImage(Image image, string prompt, string reasoningEffort, string verbosity) + { + string base64Image = ImageHelpers.ImageToBase64(image, System.Drawing.Imaging.ImageFormat.Png); + return await AnalyzeImageInternal(base64Image, prompt, reasoningEffort, verbosity); + } + + public async Task AnalyzeImage(string imagePath, string prompt, string reasoningEffort, string verbosity) + { + string base64Image = ImageHelpers.ImageFileToBase64(imagePath); + return await AnalyzeImageInternal(base64Image, prompt, reasoningEffort, verbosity); + } + + private async Task AnalyzeImageInternal(string base64Image, string prompt, string reasoningEffort, string verbosity) + { + using (HttpClient client = new HttpClient()) + { + var payload = new + { + contents = new[] + { + new + { + parts = new object[] + { + new { text = prompt }, + new { inline_data = new { mime_type = "image/png", data = base64Image } } + } + } + } + }; + + string jsonPayload = JsonConvert.SerializeObject(payload); + StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + string url = $"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}"; + + HttpResponseMessage response = await client.PostAsync(url, content); + string responseString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + dynamic responseObject = JsonConvert.DeserializeObject(responseString); + return responseObject.candidates[0].content.parts[0].text; + } + else + { + throw new Exception($"Error from Gemini API: {responseString}"); + } + } + } + } +} diff --git a/ShareX/Tools/AI/IAIProvider.cs b/ShareX/Tools/AI/IAIProvider.cs new file mode 100644 index 000000000..ee843d22d --- /dev/null +++ b/ShareX/Tools/AI/IAIProvider.cs @@ -0,0 +1,36 @@ +#region License Information (GPL v3) + +/* + ShareX - A program that allows you to take screenshots and share any file type + Copyright (c) 2007-2025 ShareX Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + Optionally you can also view the license at . +*/ + +#endregion License Information (GPL v3) + +using System.Drawing; +using System.Threading.Tasks; + +namespace ShareX +{ + public interface IAIProvider + { + Task AnalyzeImage(Image image, string prompt, string reasoningEffort, string verbosity); + Task AnalyzeImage(string imagePath, string prompt, string reasoningEffort, string verbosity); + } +} diff --git a/ShareX/Tools/AI/OpenAIProvider.cs b/ShareX/Tools/AI/OpenAIProvider.cs new file mode 100644 index 000000000..41114e391 --- /dev/null +++ b/ShareX/Tools/AI/OpenAIProvider.cs @@ -0,0 +1,107 @@ +#region License Information (GPL v3) + +/* + ShareX - A program that allows you to take screenshots and share any file type + Copyright (c) 2007-2025 ShareX Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + Optionally you can also view the license at . +*/ + +#endregion License Information (GPL v3) + +using Newtonsoft.Json; +using ShareX.HelpersLib; +using System; +using System.Drawing; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace ShareX +{ + public class OpenAIProvider : IAIProvider + { + private readonly string apiKey; + private readonly string model; + private readonly string customUrl; + + public OpenAIProvider(string apiKey, string model, string customUrl = null) + { + this.apiKey = apiKey; + this.model = model; + this.customUrl = customUrl; + } + + public async Task AnalyzeImage(Image image, string prompt, string reasoningEffort, string verbosity) + { + string base64Image = ImageHelpers.ImageToBase64(image, System.Drawing.Imaging.ImageFormat.Png); + return await AnalyzeImageInternal(base64Image, prompt, reasoningEffort, verbosity); + } + + public async Task AnalyzeImage(string imagePath, string prompt, string reasoningEffort, string verbosity) + { + string base64Image = ImageHelpers.ImageFileToBase64(imagePath); + return await AnalyzeImageInternal(base64Image, prompt, reasoningEffort, verbosity); + } + + private async Task AnalyzeImageInternal(string base64Image, string prompt, string reasoningEffort, string verbosity) + { + using (HttpClient client = new HttpClient()) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + var payload = new + { + model = this.model, + messages = new[] + { + new + { + role = "user", + content = new object[] + { + new { type = "text", text = prompt }, + new { type = "image_url", image_url = new { url = $"data:image/png;base64,{base64Image}" } } + } + } + }, + max_tokens = 1024 + }; + + string jsonPayload = JsonConvert.SerializeObject(payload); + StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + string url = string.IsNullOrEmpty(customUrl) ? "https://api.openai.com/v1/chat/completions" : customUrl; + + HttpResponseMessage response = await client.PostAsync(url, content); + string responseString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + dynamic responseObject = JsonConvert.DeserializeObject(responseString); + return responseObject.choices[0].message.content; + } + else + { + throw new Exception($"Error from OpenAI API: {responseString}"); + } + } + } + } +} diff --git a/ShareX/Tools/AI/OpenRouterProvider.cs b/ShareX/Tools/AI/OpenRouterProvider.cs new file mode 100644 index 000000000..8f7fbb93b --- /dev/null +++ b/ShareX/Tools/AI/OpenRouterProvider.cs @@ -0,0 +1,104 @@ +#region License Information (GPL v3) + +/* + ShareX - A program that allows you to take screenshots and share any file type + Copyright (c) 2007-2025 ShareX Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + Optionally you can also view the license at . +*/ + +#endregion License Information (GPL v3) + +using Newtonsoft.Json; +using ShareX.HelpersLib; +using System; +using System.Drawing; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace ShareX +{ + public class OpenRouterProvider : IAIProvider + { + private readonly string apiKey; + private readonly string model; + + public OpenRouterProvider(string apiKey, string model) + { + this.apiKey = apiKey; + this.model = model; + } + + public async Task AnalyzeImage(Image image, string prompt, string reasoningEffort, string verbosity) + { + string base64Image = ImageHelpers.ImageToBase64(image, System.Drawing.Imaging.ImageFormat.Png); + return await AnalyzeImageInternal(base64Image, prompt, reasoningEffort, verbosity); + } + + public async Task AnalyzeImage(string imagePath, string prompt, string reasoningEffort, string verbosity) + { + string base64Image = ImageHelpers.ImageFileToBase64(imagePath); + return await AnalyzeImageInternal(base64Image, prompt, reasoningEffort, verbosity); + } + + private async Task AnalyzeImageInternal(string base64Image, string prompt, string reasoningEffort, string verbosity) + { + using (HttpClient client = new HttpClient()) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + var payload = new + { + model = this.model, + messages = new[] + { + new + { + role = "user", + content = new object[] + { + new { type = "text", text = prompt }, + new { type = "image_url", image_url = new { url = $"data:image/png;base64,{base64Image}" } } + } + } + }, + max_tokens = 1024 + }; + + string jsonPayload = JsonConvert.SerializeObject(payload); + StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + const string url = "https://openrouter.ai/api/v1/chat/completions"; + + HttpResponseMessage response = await client.PostAsync(url, content); + string responseString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + dynamic responseObject = JsonConvert.DeserializeObject(responseString); + return responseObject.choices[0].message.content; + } + else + { + throw new Exception($"Error from OpenRouter API: {responseString}"); + } + } + } + } +} diff --git a/ShareX/Tools/OCR/OCRForm.cs b/ShareX/Tools/OCR/OCRForm.cs index bfe3334a9..5ef498b8e 100644 --- a/ShareX/Tools/OCR/OCRForm.cs +++ b/ShareX/Tools/OCR/OCRForm.cs @@ -50,6 +50,7 @@ namespace ShareX InitializeComponent(); ShareXResources.ApplyTheme(this, true); + OCRLanguage[] languages = OCRHelper.AvailableLanguages.OrderBy(x => x.DisplayName).ToArray(); if (languages.Length > 0) @@ -133,6 +134,7 @@ namespace ShareX cbSingleLine.Enabled = !busy; } + private async Task OCR(Bitmap bmp) { if (bmp != null && !string.IsNullOrEmpty(Options.Language)) @@ -271,4 +273,4 @@ namespace ShareX btnOpenServiceLink.Enabled = btnCopyAll.Enabled = !string.IsNullOrEmpty(Result); } } -} \ No newline at end of file +}