refactor: OpenAI integration

* supports configure multiple services
* supports select service when generate commit message by OpenAI

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo 2024-10-28 11:00:11 +08:00
parent 8280287362
commit 1044915be1
No known key found for this signature in database
13 changed files with 283 additions and 229 deletions

View file

@ -20,8 +20,9 @@ namespace SourceGit.Commands
} }
} }
public GenerateCommitMessage(string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress) public GenerateCommitMessage(Models.OpenAIService service, string repo, List<Models.Change> changes, CancellationToken cancelToken, Action<string> onProgress)
{ {
_service = service;
_repo = repo; _repo = repo;
_changes = changes; _changes = changes;
_cancelToken = cancelToken; _cancelToken = cancelToken;
@ -75,7 +76,7 @@ namespace SourceGit.Commands
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd(); var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
var diff = rs.IsSuccess ? rs.StdOut : "unknown change"; var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
var rsp = Models.OpenAI.Chat(Models.OpenAI.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken); var rsp = _service.Chat(_service.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0) if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content; return rsp.Choices[0].Message.Content;
@ -84,13 +85,14 @@ namespace SourceGit.Commands
private string GenerateSubject(string summary) private string GenerateSubject(string summary)
{ {
var rsp = Models.OpenAI.Chat(Models.OpenAI.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken); var rsp = _service.Chat(_service.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0) if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content; return rsp.Choices[0].Message.Content;
return string.Empty; return string.Empty;
} }
private Models.OpenAIService _service;
private string _repo; private string _repo;
private List<Models.Change> _changes; private List<Models.Change> _changes;
private CancellationToken _cancelToken; private CancellationToken _cancelToken;

View file

@ -6,6 +6,8 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models namespace SourceGit.Models
{ {
public class OpenAIChatMessage public class OpenAIChatMessage
@ -74,44 +76,78 @@ namespace SourceGit.Models
} }
} }
public static class OpenAI public class OpenAIService : ObservableObject
{ {
public static string Server public string Name
{ {
get; get => _name;
set; set => SetProperty(ref _name, value);
} }
public static string ApiKey public string Server
{ {
get; get => _server;
set; set => SetProperty(ref _server, value);
} }
public static string Model public string ApiKey
{ {
get; get => _apiKey;
set; set => SetProperty(ref _apiKey, value);
} }
public static string AnalyzeDiffPrompt public string Model
{ {
get; get => _model;
set; set => SetProperty(ref _model, value);
} }
public static string GenerateSubjectPrompt public string AnalyzeDiffPrompt
{ {
get; get => _analyzeDiffPrompt;
set; set => SetProperty(ref _analyzeDiffPrompt, value);
} }
public static bool IsValid public string GenerateSubjectPrompt
{ {
get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model); get => _generateSubjectPrompt;
set => SetProperty(ref _generateSubjectPrompt, value);
} }
public static OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation) public OpenAIService()
{
AnalyzeDiffPrompt = """
You are an expert developer specialist in creating commits.
Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:
- Do not use any code snippets, imports, file routes or bullets points.
- Do not mention the route of file that has been change.
- Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes.
- Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.".
- Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature".
- Avoid using general terms like "update" or "change", be specific about what was updated or changed.
- Avoid using terms like "The main goal of", just output directly the summary in plain text
""";
GenerateSubjectPrompt = """
You are an expert developer specialist in creating commits messages.
Your only goal is to retrieve a single commit message.
Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:
- Assign the commit {type} according to the next conditions:
feat: Only when adding a new feature.
fix: When fixing a bug.
docs: When updating documentation.
style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.
test: When adding or updating tests.
chore: When making changes to the build process or auxiliary tools and libraries.
revert: When undoing a previous commit.
refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.
- Do not add any issues numeration, explain your output nor introduce your answer.
- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.
- Be as concise as possible, keep the message under 50 characters.
""";
}
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
{ {
var chat = new OpenAIChatRequest() { Model = Model }; var chat = new OpenAIChatRequest() { Model = Model };
chat.AddMessage("system", prompt); chat.AddMessage("system", prompt);
@ -144,5 +180,12 @@ namespace SourceGit.Models
throw; throw;
} }
} }
private string _name;
private string _server;
private string _apiKey;
private string _model;
private string _analyzeDiffPrompt;
private string _generateSubjectPrompt;
} }
} }

View file

@ -401,12 +401,12 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">Last year</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">Last year</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} years ago</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} years ago</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String> <x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String>
<x:String x:Key="Text.Preference.Advanced" xml:space="preserve">Advanced Options</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String> <x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Key</x:String> <x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Key</x:String>
<x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String> <x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">Model</x:String> <x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">Model</x:String>
<x:String x:Key="Text.Preference.AI.Name" xml:space="preserve">Name</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Server</x:String> <x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Server</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">APPEARANCE</x:String> <x:String x:Key="Text.Preference.Appearance" xml:space="preserve">APPEARANCE</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Default Font</x:String> <x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Default Font</x:String>

View file

@ -405,7 +405,6 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">В пролому году</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">В пролому году</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} лет назад</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} лет назад</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Параметры</x:String> <x:String x:Key="Text.Preference" xml:space="preserve">Параметры</x:String>
<x:String x:Key="Text.Preference.Advanced" xml:space="preserve">Расширенные опции</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">ОТКРЫТЬ ИИ</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">ОТКРЫТЬ ИИ</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">Ключ API</x:String> <x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">Ключ API</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Запрос на анализ различий</x:String> <x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Запрос на анализ различий</x:String>

View file

@ -404,7 +404,13 @@
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0}年前</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0}年前</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好设置</x:String> <x:String x:Key="Text.Preference" xml:space="preserve">偏好设置</x:String>
<x:String x:Key="Text.Preference.Advanced" xml:space="preserve">高级设置</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API密钥</x:String>
<x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">模型</x:String>
<x:String x:Key="Text.Preference.AI.Name" xml:space="preserve">配置名称</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">服务地址</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外观配置</x:String> <x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外观配置</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">缺省字体</x:String> <x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">缺省字体</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">默认字体大小</x:String> <x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">默认字体大小</x:String>

View file

@ -403,12 +403,12 @@
<x:String x:Key="Text.Period.MonthsAgo" xml:space="preserve">{0} 個月前</x:String> <x:String x:Key="Text.Period.MonthsAgo" xml:space="preserve">{0} 個月前</x:String>
<x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String> <x:String x:Key="Text.Period.LastYear" xml:space="preserve">一年前</x:String>
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} 年前</x:String> <x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} 年前</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">偏好設定</x:String>
<x:String x:Key="Text.Preference.Advanced" xml:space="preserve">進階設定</x:String> <x:String x:Key="Text.Preference.Advanced" xml:space="preserve">進階設定</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OpenAI</x:String> <x:String x:Key="Text.Preference.AI" xml:space="preserve">OpenAI</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">伺服器</x:String> <x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">伺服器</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API 金鑰</x:String> <x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API 金鑰</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">模型</x:String> <x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">模型</x:String>
<x:String x:Key="Text.Preference.AI.Name" xml:space="preserve">名稱</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">分析變更差異提示詞</x:String> <x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">分析變更差異提示詞</x:String>
<x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">產生提交訊息提示詞</x:String> <x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">產生提交訊息提示詞</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外觀設定</x:String> <x:String x:Key="Text.Preference.Appearance" xml:space="preserve">外觀設定</x:String>

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
@ -25,7 +25,6 @@ namespace SourceGit.ViewModels
_instance.PrepareGit(); _instance.PrepareGit();
_instance.PrepareShellOrTerminal(); _instance.PrepareShellOrTerminal();
_instance.PrepareWorkspaces(); _instance.PrepareWorkspaces();
_instance.PrepareOpenAIPrompt();
return _instance; return _instance;
} }
@ -277,71 +276,6 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _externalMergeToolPath, value); set => SetProperty(ref _externalMergeToolPath, value);
} }
public string OpenAIServer
{
get => Models.OpenAI.Server;
set
{
if (value != Models.OpenAI.Server)
{
Models.OpenAI.Server = value;
OnPropertyChanged();
}
}
}
public string OpenAIApiKey
{
get => Models.OpenAI.ApiKey;
set
{
if (value != Models.OpenAI.ApiKey)
{
Models.OpenAI.ApiKey = value;
OnPropertyChanged();
}
}
}
public string OpenAIModel
{
get => Models.OpenAI.Model;
set
{
if (value != Models.OpenAI.Model)
{
Models.OpenAI.Model = value;
OnPropertyChanged();
}
}
}
public string OpenAIAnalyzeDiffPrompt
{
get => Models.OpenAI.AnalyzeDiffPrompt;
set
{
if (value != Models.OpenAI.AnalyzeDiffPrompt)
{
Models.OpenAI.AnalyzeDiffPrompt = value;
OnPropertyChanged();
}
}
}
public string OpenAIGenerateSubjectPrompt
{
get => Models.OpenAI.GenerateSubjectPrompt;
set
{
if (value != Models.OpenAI.GenerateSubjectPrompt)
{
Models.OpenAI.GenerateSubjectPrompt = value;
OnPropertyChanged();
}
}
}
public uint StatisticsSampleColor public uint StatisticsSampleColor
{ {
get => _statisticsSampleColor; get => _statisticsSampleColor;
@ -360,6 +294,12 @@ namespace SourceGit.ViewModels
set; set;
} = []; } = [];
public AvaloniaList<Models.OpenAIService> OpenAIServices
{
get;
set;
} = [];
public double LastCheckUpdateTime public double LastCheckUpdateTime
{ {
get => _lastCheckUpdateTime; get => _lastCheckUpdateTime;
@ -554,45 +494,6 @@ namespace SourceGit.ViewModels
} }
} }
private void PrepareOpenAIPrompt()
{
if (string.IsNullOrEmpty(Models.OpenAI.AnalyzeDiffPrompt))
{
Models.OpenAI.AnalyzeDiffPrompt = """
You are an expert developer specialist in creating commits.
Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:
- Do not use any code snippets, imports, file routes or bullets points.
- Do not mention the route of file that has been change.
- Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes.
- Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.".
- Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature".
- Avoid using general terms like "update" or "change", be specific about what was updated or changed.
- Avoid using terms like "The main goal of", just output directly the summary in plain text
""";
}
if (string.IsNullOrEmpty(Models.OpenAI.GenerateSubjectPrompt))
{
Models.OpenAI.GenerateSubjectPrompt = """
You are an expert developer specialist in creating commits messages.
Your only goal is to retrieve a single commit message.
Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:
- Assign the commit {type} according to the next conditions:
feat: Only when adding a new feature.
fix: When fixing a bug.
docs: When updating documentation.
style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.
test: When adding or updating tests.
chore: When making changes to the build process or auxiliary tools and libraries.
revert: When undoing a previous commit.
refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.
- Do not add any issues numeration, explain your output nor introduce your answer.
- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.
- Be as concise as possible, keep the message under 50 characters.
""";
}
}
private RepositoryNode FindNodeRecursive(string id, List<RepositoryNode> collection) private RepositoryNode FindNodeRecursive(string id, List<RepositoryNode> collection)
{ {
foreach (var node in collection) foreach (var node in collection)

View file

@ -403,25 +403,6 @@ namespace SourceGit.ViewModels
} }
} }
public void GenerateCommitMessageByAI()
{
if (!Models.OpenAI.IsValid)
{
App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI");
return;
}
if (_staged is { Count: > 0 })
{
var dialog = new Views.AIAssistant(_repo.FullPath, _staged, generated => CommitMessage = generated);
App.OpenDialog(dialog);
}
else
{
App.RaiseException(_repo.FullPath, "No files added to commit!");
}
}
public void Commit() public void Commit()
{ {
DoCommit(false, false, false); DoCommit(false, false, false);
@ -1211,6 +1192,51 @@ namespace SourceGit.ViewModels
return menu; return menu;
} }
public ContextMenu CreateContextForOpenAI()
{
if (_staged == null || _staged.Count == 0)
{
App.RaiseException(_repo.FullPath, "No files added to commit!");
return null;
}
var services = Preference.Instance.OpenAIServices;
if (services.Count == 0)
{
App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI");
return null;
}
if (services.Count == 1)
{
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, _staged, generated => CommitMessage = generated);
App.OpenDialog(dialog);
return null;
}
else
{
var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft };
foreach (var service in services)
{
var dup = service;
var item = new MenuItem();
item.Header = service.Name;
item.Click += (_, e) =>
{
var dialog = new Views.AIAssistant(dup, _repo.FullPath, _staged, generated => CommitMessage = generated);
App.OpenDialog(dialog);
e.Handled = true;
};
menu.Items.Add(item);
}
return menu;
}
}
private List<Models.Change> GetStagedChanges() private List<Models.Change> GetStagedChanges()
{ {
if (_useAmend) if (_useAmend)

View file

@ -17,12 +17,14 @@ namespace SourceGit.Views
InitializeComponent(); InitializeComponent();
} }
public AIAssistant(string repo, List<Models.Change> changes, Action<string> onDone) public AIAssistant(Models.OpenAIService service, string repo, List<Models.Change> changes, Action<string> onDone)
{ {
_service = service;
_repo = repo; _repo = repo;
_changes = changes; _changes = changes;
_onDone = onDone; _onDone = onDone;
_cancel = new CancellationTokenSource(); _cancel = new CancellationTokenSource();
InitializeComponent(); InitializeComponent();
} }
@ -35,7 +37,7 @@ namespace SourceGit.Views
Task.Run(() => Task.Run(() =>
{ {
var message = new Commands.GenerateCommitMessage(_repo, _changes, _cancel.Token, SetDescription).Result(); var message = new Commands.GenerateCommitMessage(_service, _repo, _changes, _cancel.Token, SetDescription).Result();
if (_cancel.IsCancellationRequested) if (_cancel.IsCancellationRequested)
return; return;
@ -63,6 +65,7 @@ namespace SourceGit.Views
Dispatcher.UIThread.Invoke(() => ProgressMessage.Text = message); Dispatcher.UIThread.Invoke(() => ProgressMessage.Text = message);
} }
private Models.OpenAIService _service;
private string _repo; private string _repo;
private List<Models.Change> _changes; private List<Models.Change> _changes;
private Action<string> _onDone; private Action<string> _onDone;

View file

@ -412,7 +412,7 @@
<TextBlock Classes="bold" Margin="4,0,0,0" Text="{DynamicResource Text.Preference.DiffMerge}"/> <TextBlock Classes="bold" Margin="4,0,0,0" Text="{DynamicResource Text.Preference.DiffMerge}"/>
</StackPanel> </StackPanel>
<Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/> <Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/>
<Grid Margin="8,0,0,0" RowDefinitions="32,Auto"> <Grid Margin="8,0,0,8" RowDefinitions="32,Auto">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/> <ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@ -459,82 +459,117 @@
</TextBox.InnerRightContent> </TextBox.InnerRightContent>
</TextBox> </TextBox>
</Grid> </Grid>
<StackPanel Orientation="Horizontal" Margin="0,24,0,0">
<Path Width="12" Height="12" Data="{StaticResource Icons.AIAssist}"/>
<TextBlock Classes="bold" Margin="4,0,0,0" Text="{DynamicResource Text.Preference.AI}"/>
</StackPanel>
<Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/>
<Grid Margin="8,0,0,0" RowDefinitions="32,32,32,32,Auto,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.Server}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="0" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding OpenAIServer, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.Model}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="1" Grid.Column="1"
Height="28"
CornerRadius="3"
Text="{Binding OpenAIModel, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.ApiKey}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="2" Grid.Column="1"
Height="28"
CornerRadius="3"
PasswordChar="*"
Text="{Binding OpenAIApiKey, Mode=TwoWay}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" Classes="group_expander" x:Name="OpenAIAdvancedOptions" HorizontalAlignment="Right">
<TextBlock Margin="0" Text="{DynamicResource Text.Preference.Advanced}"/>
</ToggleButton>
<TextBlock Grid.Row="4" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.AnalyzeDiffPrompt}"
HorizontalAlignment="Right"
Margin="0,0,16,0"
IsVisible="{Binding #OpenAIAdvancedOptions.IsChecked}"/>
<TextBox Grid.Row="4" Grid.Column="1"
Height="120"
Margin="0,2"
CornerRadius="3"
VerticalContentAlignment="Top"
Text="{Binding OpenAIAnalyzeDiffPrompt, Mode=TwoWay}"
AcceptsReturn="true"
TextWrapping="Wrap"
IsVisible="{Binding #OpenAIAdvancedOptions.IsChecked}"/>
<TextBlock Grid.Row="5" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.GenerateSubjectPrompt}"
HorizontalAlignment="Right"
Margin="0,0,16,0"
IsVisible="{Binding #OpenAIAdvancedOptions.IsChecked}"/>
<TextBox Grid.Row="5" Grid.Column="1"
Height="120"
Margin="0,2"
CornerRadius="3"
VerticalContentAlignment="Top"
Text="{Binding OpenAIGenerateSubjectPrompt, Mode=TwoWay}"
AcceptsReturn="true"
TextWrapping="Wrap"
IsVisible="{Binding #OpenAIAdvancedOptions.IsChecked}"/>
</Grid>
</StackPanel> </StackPanel>
</TabItem> </TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.AI}"/>
</TabItem.Header>
<Grid ColumnDefinitions="200,*" Margin="0,8,0,16" MinHeight="400">
<Border Grid.Column="0"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="{DynamicResource Brush.Contents}">
<Grid RowDefinitions="*,1,Auto">
<ListBox Grid.Row="0"
Background="Transparent"
ItemsSource="{Binding OpenAIServices}"
SelectedItem="{Binding #ThisControl.SelectedOpenAIService, Mode=TwoWay}"
SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Height" Value="26"/>
<Setter Property="Padding" Value="4,2"/>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:OpenAIService">
<Grid ColumnDefinitions="Auto,*">
<Path Grid.Column="0" Width="14" Height="14" Data="{StaticResource Icons.AIAssist}"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Margin="8,0" TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Rectangle Grid.Row="1" Height="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Background="{DynamicResource Brush.ToolBar}">
<Button Classes="icon_button" Click="OnAddOpenAIService">
<Path Width="14" Height="14" Data="{StaticResource Icons.Plus}"/>
</Button>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
<Button Classes="icon_button" Click="OnRemoveSelectedOpenAIService">
<Path Width="14" Height="14" Data="{StaticResource Icons.Window.Minimize}"/>
</Button>
<Rectangle Width="1" Fill="{DynamicResource Brush.Border2}" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Column="1" Margin="16,0,0,0">
<ContentControl.Content>
<Binding Path="#ThisControl.SelectedOpenAIService">
<Binding.TargetNullValue>
<Path Width="64" Height="64"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Empty}"/>
</Binding.TargetNullValue>
</Binding>
</ContentControl.Content>
<ContentControl.DataTemplates>
<DataTemplate DataType="m:OpenAIService">
<StackPanel Orientation="Vertical" MaxWidth="680">
<TextBlock Text="{DynamicResource Text.Preference.AI.Name}"/>
<TextBox Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Name, Mode=TwoWay}"/>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Preference.AI.Server}"/>
<TextBox Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Server, Mode=TwoWay}"/>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Preference.AI.Model}"/>
<TextBox Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding Model, Mode=TwoWay}"/>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Preference.AI.ApiKey}"/>
<TextBox Margin="0,4,0,0" CornerRadius="3" Height="28" Text="{Binding ApiKey, Mode=TwoWay}" PasswordChar="*"/>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Preference.AI.AnalyzeDiffPrompt}"/>
<TextBox Height="120"
Margin="0,4,0,0"
CornerRadius="3"
VerticalContentAlignment="Top"
Text="{Binding AnalyzeDiffPrompt, Mode=TwoWay}"
AcceptsReturn="true"
TextWrapping="Wrap"/>
<TextBlock Margin="0,12,0,0" Text="{DynamicResource Text.Preference.AI.GenerateSubjectPrompt}"/>
<TextBox Height="120"
Margin="0,4,0,0"
CornerRadius="3"
VerticalContentAlignment="Top"
Text="{Binding GenerateSubjectPrompt, Mode=TwoWay}"
AcceptsReturn="true"
TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</TabItem>
</TabControl> </TabControl>
</Border> </Border>
</Grid> </Grid>

View file

@ -74,6 +74,15 @@ namespace SourceGit.Views
set; set;
} }
public static readonly StyledProperty<Models.OpenAIService> SelectedOpenAIServiceProperty =
AvaloniaProperty.Register<Preference, Models.OpenAIService>(nameof(SelectedOpenAIService));
public Models.OpenAIService SelectedOpenAIService
{
get => GetValue(SelectedOpenAIServiceProperty);
set => SetValue(SelectedOpenAIServiceProperty, value);
}
public Preference() public Preference()
{ {
var pref = ViewModels.Preference.Instance; var pref = ViewModels.Preference.Instance;
@ -312,5 +321,24 @@ namespace SourceGit.Views
e.Handled = true; e.Handled = true;
} }
private void OnAddOpenAIService(object sender, RoutedEventArgs e)
{
var service = new Models.OpenAIService() { Name = "Unnamed Service" };
ViewModels.Preference.Instance.OpenAIServices.Add(service);
SelectedOpenAIService = service;
e.Handled = true;
}
private void OnRemoveSelectedOpenAIService(object sender, RoutedEventArgs e)
{
if (SelectedOpenAIService == null)
return;
ViewModels.Preference.Instance.OpenAIServices.Remove(SelectedOpenAIService);
SelectedOpenAIService = null;
e.Handled = true;
}
} }
} }

View file

@ -199,7 +199,7 @@
<Button Grid.Column="1" <Button Grid.Column="1"
Classes="icon_button" Classes="icon_button"
Margin="4,2,0,0" Margin="4,2,0,0"
Command="{Binding GenerateCommitMessageByAI}" Click="OnOpenOpenAIHelper"
ToolTip.Tip="{DynamicResource Text.AIAssistant.Tip}" ToolTip.Tip="{DynamicResource Text.AIAssistant.Tip}"
ToolTip.Placement="Top" ToolTip.Placement="Top"
ToolTip.VerticalOffset="0"> ToolTip.VerticalOffset="0">

View file

@ -120,6 +120,17 @@ namespace SourceGit.Views
e.Handled = true; e.Handled = true;
} }
private void OnOpenOpenAIHelper(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.WorkingCopy vm)
{
var menu = vm.CreateContextForOpenAI();
(sender as Button)?.OpenContextMenu(menu);
}
e.Handled = true;
}
private void OnOpenConventionalCommitHelper(object _, RoutedEventArgs e) private void OnOpenConventionalCommitHelper(object _, RoutedEventArgs e)
{ {
if (DataContext is ViewModels.WorkingCopy vm) if (DataContext is ViewModels.WorkingCopy vm)