From b38aebfc8efbe680c5a6a0f7a56a1f215ac1e06b Mon Sep 17 00:00:00 2001 From: HeyEchoo Date: Sun, 5 Jul 2026 01:25:49 +0800 Subject: [PATCH] feat(ai): overhaul AI service and model management with per-repository model selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor `AI.Service`: replace `AutoFetchAvailableModels` flag with explicit `AvailableModels` list; add `AddModel`, `RemoveModel`, `FetchModelsFromServer`, and `Clone` methods - Add `PreferredAIModel` to `RepositorySettings` for per-repository model override - Refactor `Repository.cs`: replace `GetPreferredOpenAIServices()` with `GetAllOpenAIServices()`, `GetOpenAIConfig()`, and `ResolveAIService()` for cleaner service/model resolution - Update `RepositoryConfigure`: add model selector dropdown alongside service selector with dynamic model list based on selected service - Update AI invocation in `CommitMessageToolBox` and `WorkingCopy` to show nested service→model context menus when no preferred service is configured - Revamp Preferences AI panel: replace text input + auto-fetch checkbox with list-based model management (add, remove, edit, fetch from server) - Add `FetchAIModels` dialog window for fetching and selecting models from AI server with loading indicator and checkbox list - Add reusable `TextInput` dialog window for simple text input prompts - Remove auto-fetch of available models on application startup (`UpdateAvailableAIModels` now no-op) - Remove `JsonSerialization.JsonIgnore` from `AvailableModels` and update `Service` model binding - Rename locale keys from `Text.Configure.OpenAI*` to `Text.Configure.AI*` with new keys for default service/model selection and model management UI - Update all 16 locale files (en_US, zh_CN, zh_TW, de_DE, el_GR, es_ES, fr_FR, he_IL, id_ID, it_IT, ja_JP, ko_KR, pt_BR, ru_RU, ta_IN, uk_UA) with new localization strings - Auto-close AI assistant window after applying a commit message --- src/AI/Service.cs | 62 ++++++++------ src/App.axaml.cs | 1 - src/Models/RepositorySettings.cs | 8 +- src/Resources/Locales/de_DE.axaml | 6 +- src/Resources/Locales/el_GR.axaml | 7 +- src/Resources/Locales/en_US.axaml | 17 +++- src/Resources/Locales/es_ES.axaml | 11 ++- src/Resources/Locales/fr_FR.axaml | 7 +- src/Resources/Locales/he_IL.axaml | 7 +- src/Resources/Locales/id_ID.axaml | 6 +- src/Resources/Locales/it_IT.axaml | 6 +- src/Resources/Locales/ja_JP.axaml | 7 +- src/Resources/Locales/ko_KR.axaml | 7 +- src/Resources/Locales/pt_BR.axaml | 6 +- src/Resources/Locales/ru_RU.axaml | 11 ++- src/Resources/Locales/ta_IN.axaml | 6 +- src/Resources/Locales/uk_UA.axaml | 6 +- src/Resources/Locales/zh_CN.axaml | 17 +++- src/Resources/Locales/zh_TW.axaml | 15 +++- src/ViewModels/AIAssistant.cs | 2 +- src/ViewModels/Preferences.cs | 15 ---- src/ViewModels/Repository.cs | 39 +++++---- src/ViewModels/RepositoryConfigure.cs | 104 ++++++++++++++++++++++-- src/Views/AIAssistant.axaml.cs | 5 ++ src/Views/CommitMessageToolBox.axaml | 2 +- src/Views/CommitMessageToolBox.axaml.cs | 65 +++++++++------ src/Views/FetchAIModels.axaml | 92 +++++++++++++++++++++ src/Views/FetchAIModels.axaml.cs | 92 +++++++++++++++++++++ src/Views/Preferences.axaml | 96 ++++++++++++++++++---- src/Views/Preferences.axaml.cs | 103 ++++++++++++++++++++++- src/Views/RepositoryConfigure.axaml | 21 +++-- src/Views/TextInput.axaml | 55 +++++++++++++ src/Views/TextInput.axaml.cs | 39 +++++++++ src/Views/WorkingCopy.axaml.cs | 40 +++++---- 34 files changed, 797 insertions(+), 186 deletions(-) create mode 100644 src/Views/FetchAIModels.axaml create mode 100644 src/Views/FetchAIModels.axaml.cs create mode 100644 src/Views/TextInput.axaml create mode 100644 src/Views/TextInput.axaml.cs diff --git a/src/AI/Service.cs b/src/AI/Service.cs index 7d311c6c0..e0ef8ff5b 100644 --- a/src/AI/Service.cs +++ b/src/AI/Service.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.ClientModel; using System.Collections.Generic; -using System.Text.Json.Serialization; using Azure.AI.OpenAI; using CommunityToolkit.Mvvm.ComponentModel; using OpenAI; @@ -35,12 +34,11 @@ public bool ReadApiKeyFromEnv set; } = false; - [JsonIgnore] public List AvailableModels { - get; - private set; - } = []; + get => _availableModels; + set => SetProperty(ref _availableModels, value); + } public string Model { @@ -48,34 +46,38 @@ public string Model set => SetProperty(ref _model, value); } - public bool AutoFetchAvailableModels - { - get => _autoFetchAvailableModels; - set => SetProperty(ref _autoFetchAvailableModels, value); - } - public string AdditionalPrompt { get; set; } = string.Empty; - public void FetchAvailableModels() + public void AddModel(string model) { - if (!_autoFetchAvailableModels) + if (!_availableModels.Contains(model)) { - if (!string.IsNullOrEmpty(Model)) - AvailableModels = [Model]; - return; + var newList = new List(_availableModels) { model }; + AvailableModels = newList; } + } + public void RemoveModel(string model) + { + if (_availableModels.Contains(model)) + { + var newList = new List(_availableModels); + newList.Remove(model); + AvailableModels = newList; + } + } + + public List FetchModelsFromServer() + { var allModels = GetOpenAIClient().GetOpenAIModelClient().GetModels(); - AvailableModels = new List(); + var result = new List(); foreach (var model in allModels.Value) - AvailableModels.Add(model.Id); - - if (AvailableModels.Count > 0 && (string.IsNullOrEmpty(Model) || !AvailableModels.Contains(Model))) - Model = AvailableModels[0]; + result.Add(model.Id); + return result; } public ChatClient GetChatClient() @@ -83,6 +85,20 @@ public ChatClient GetChatClient() return !string.IsNullOrEmpty(Model) ? GetOpenAIClient().GetChatClient(Model) : null; } + public Service Clone() + { + return new Service + { + Name = Name, + Server = Server, + ApiKey = ApiKey, + ReadApiKeyFromEnv = ReadApiKeyFromEnv, + Model = Model, + AdditionalPrompt = AdditionalPrompt, + AvailableModels = new List(AvailableModels), + }; + } + private OpenAIClient GetOpenAIClient() { var credential = new ApiKeyCredential(ReadApiKeyFromEnv ? Environment.GetEnvironmentVariable(ApiKey) : ApiKey); @@ -93,6 +109,6 @@ private OpenAIClient GetOpenAIClient() private string _name = string.Empty; private string _model = string.Empty; - private bool _autoFetchAvailableModels = true; + private List _availableModels = []; } } diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 3a7470dc1..36cf73452 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -482,7 +482,6 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) var pref = ViewModels.Preferences.Instance; pref.SetCanModify(); - pref.UpdateAvailableAIModels(); _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 506e6ded7..7ea6b6ffe 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -46,7 +46,13 @@ public string PreferredOpenAIService { get; set; - } = "---"; + } = ""; + + public string PreferredAIModel + { + get; + set; + } = string.Empty; public AvaloniaList CommitTemplates { diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 6da606aee..60be1c952 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -242,9 +242,9 @@ $1, $2, … Werte der Eingabe-Steuerelemente Diese Regel per .issuetracker-Datei teilen Ergebnis-URL: Verwende bitte $1, $2, um auf Regex-Gruppenwerte zuzugreifen. - OPEN AI - Bevorzugter Dienst: - Der ausgewählte ‚Bevorzugte Dienst‘ wird nur in diesem Repository gesetzt und verwendet. Wenn keiner gesetzt ist und mehrere Dienste verfügbar sind, wird ein Kontextmenü zur Auswahl angezeigt. + OPEN AI + Bevorzugter Dienst: + Der ausgewählte ‚Bevorzugte Dienst‘ wird nur in diesem Repository gesetzt und verwendet. Wenn keiner gesetzt ist und mehrere Dienste verfügbar sind, wird ein Kontextmenü zur Auswahl angezeigt. HTTP-Proxy HTTP-Proxy für dieses Repository Benutzername diff --git a/src/Resources/Locales/el_GR.axaml b/src/Resources/Locales/el_GR.axaml index 7f1da3c39..b621be368 100644 --- a/src/Resources/Locales/el_GR.axaml +++ b/src/Resources/Locales/el_GR.axaml @@ -264,9 +264,9 @@ Κοινή χρήση αυτού του κανόνα στο αρχείο .issuetracker URL αποτελέσματος: Χρησιμοποιήστε $1, $2 για πρόσβαση στις τιμές των ομάδων της κανονικής έκφρασης. - AI - Προτιμώμενη υπηρεσία: - Αν οριστεί η «Προτιμώμενη υπηρεσία», το SourceGit θα τη χρησιμοποιεί μόνο σε αυτό το αποθετήριο. Διαφορετικά, αν υπάρχουν περισσότερες από μία διαθέσιμες υπηρεσίες, θα εμφανίζεται μενού για να επιλέξετε μία. + AI + Προτιμώμενη υπηρεσία: + Αν οριστεί η «Προτιμώμενη υπηρεσία», το SourceGit θα τη χρησιμοποιεί μόνο σε αυτό το αποθετήριο. Διαφορετικά, αν υπάρχουν περισσότερες από μία διαθέσιμες υπηρεσίες, θα εμφανίζεται μενού για να επιλέξετε μία. HTTP Proxy HTTP proxy που χρησιμοποιεί αυτό το αποθετήριο Όνομα χρήστη @@ -633,7 +633,6 @@ Επιπλέον prompt (χρησιμοποιήστε `-` για να απαριθμήσετε τις απαιτήσεις σας) Κλειδί API Μοντέλο - Αυτόματη ανάκτηση διαθέσιμων model-ids Όνομα Η τιμή που εισήχθη είναι το όνομα για φόρτωση του κλειδιού API από το ENV Διακομιστής diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index f23542dd2..4da6454be 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -3,6 +3,7 @@ About SourceGit Release Date: {0} Release Notes + Add Opensource & Free Git GUI Client Add File(s) To Ignore Pattern: @@ -269,9 +270,12 @@ Share this rule in .issuetracker file Result URL: Please use $1, $2 to access regex groups values. - AI - Preferred Service: - If the 'Preferred Service' is set, SourceGit will only use it in this repository. Otherwise, if there is more than one service available, a context menu to choose one of them will be shown. + AI + Default Service: + Default Model: + If set, SourceGit will use this service and model directly for AI commit message generation in this repository. Otherwise, a selection menu will be shown. + (None) + (None) HTTP Proxy HTTP proxy used by this repository User Name @@ -655,10 +659,15 @@ Additional Prompt (Use `-` to list your requirements) API Key Model - Fetch available model-ids automatically + Add Model + Fetch Models + Fetching models... + Remove Model + Select Models Name Entered value is the name to load API key from ENV Server + AI Services APPEARANCE Default Font Editor Tab Width diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index e59e25e6a..fb57863fc 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -21,10 +21,10 @@ Qué Checkout: Crear Nueva Rama Rama Existente - Asistente OpenAI + Asistente AI Modelo RE-GENERAR - Usar OpenAI para generar mensaje de commit + Usar AI para generar mensaje de commit Usar Ocultar SourceGit Ocultar Otros @@ -273,9 +273,9 @@ Compartir esta regla en el archivo .issuetracker URL Resultante: Por favor, use $1, $2 para acceder a los valores de los grupos regex. - OPEN AI - Servicio Preferido: - Si el 'Servicio Preferido' está establecido, SourceGit sólo lo usará en este repositorio. De lo contrario, si hay más de un servicio disponible, se mostrará un menú de contexto para elegir uno. + OPEN AI + Servicio Preferido: + Si el 'Servicio Preferido' está establecido, SourceGit sólo lo usará en este repositorio. De lo contrario, si hay más de un servicio disponible, se mostrará un menú de contexto para elegir uno. Proxy HTTP Proxy HTTP utilizado por este repositorio Nombre del Usuario @@ -648,7 +648,6 @@ Prompt adicional (Usa `-` para listar tus requerimientos) Clave API Modelo - Traer automáticamente los model-ids disponibles Nombre El valor ingresado es el nombre de la clave API a cargar desde ENV Servidor diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index e68c72954..dc091834a 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -262,9 +262,9 @@ Partager cette règle dans le fichier .issuetracker URL résultant: Veuillez utiliser $1, $2 pour accéder aux valeurs des groupes regex. - IA - Service préféré: - Si le 'Service préféré' est défini, SourceGit l'utilisera seulement dans ce repository. Sinon, si plus d'un service est disponible, un menu contextuel permettant de choisir l'un d'eux sera affiché. + IA + Service préféré: + Si le 'Service préféré' est défini, SourceGit l'utilisera seulement dans ce repository. Sinon, si plus d'un service est disponible, un menu contextuel permettant de choisir l'un d'eux sera affiché. Proxy HTTP Proxy HTTP utilisé par ce dépôt Nom d'utilisateur @@ -627,7 +627,6 @@ Prompt supplémentaire (utilisez `-` pour lister vos exigences) Clé d'API Modèle - Récupérer automatiquement les modèles disponibles Nom La valeur saisie est le nom pour charger la clé API depuis l'ENV Serveur diff --git a/src/Resources/Locales/he_IL.axaml b/src/Resources/Locales/he_IL.axaml index 66430604c..2339abceb 100644 --- a/src/Resources/Locales/he_IL.axaml +++ b/src/Resources/Locales/he_IL.axaml @@ -262,9 +262,9 @@ שיתוף כלל זה בקובץ .issuetracker URL התוצאה: השתמש ב־$1, $2 לגישה לערכי קבוצות ה־regex. - AI - שירות מועדף: - אם מוגדר 'שירות מועדף', SourceGit ישתמש רק בו במאגר זה. אחרת, אם זמינים מספר שירותים, יוצג תפריט הקשר לבחירת אחד מהם. + AI + שירות מועדף: + אם מוגדר 'שירות מועדף', SourceGit ישתמש רק בו במאגר זה. אחרת, אם זמינים מספר שירותים, יוצג תפריט הקשר לבחירת אחד מהם. HTTP Proxy HTTP proxy לשימוש המאגר הזה שם משתמש @@ -627,7 +627,6 @@ Prompt נוסף (השתמש ב־`-` לרשימת דרישות) API Key מודל - Fetch אוטומטי של model-ids זמינים שם הערך שהוזן הוא שם משתנה סביבה לטעינת ה־API key שרת diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index 75bd5ccf7..96bafc355 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -217,9 +217,9 @@ Bagikan aturan ini di berkas .issuetracker URL Hasil: Gunakan $1, $2 untuk mengakses nilai grup regex. - AI - Layanan Pilihan: - Jika 'Layanan Pilihan' diatur, SourceGit hanya akan menggunakannya di repositori ini. Jika tidak, jika ada lebih dari satu layanan yang tersedia, menu konteks untuk memilih salah satunya akan ditampilkan. + AI + Layanan Pilihan: + Jika 'Layanan Pilihan' diatur, SourceGit hanya akan menggunakannya di repositori ini. Jika tidak, jika ada lebih dari satu layanan yang tersedia, menu konteks untuk memilih salah satunya akan ditampilkan. Proksi HTTP Proksi HTTP yang digunakan oleh repositori ini Nama Pengguna diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index b18b821e6..136414edf 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -241,9 +241,9 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Condividi questa regola nel file .issuetracker URL Risultato: Utilizza $1, $2 per accedere ai valori dei gruppi regex. - AI - Servizio preferito: - Se il 'Servizio Preferito' é impostato, SourceGit utilizzerà solo quello per questo repository. Altrimenti, se ci sono più servizi disponibili, verrà mostrato un menu contestuale per sceglierne uno. + AI + Servizio preferito: + Se il 'Servizio Preferito' é impostato, SourceGit utilizzerà solo quello per questo repository. Altrimenti, se ci sono più servizi disponibili, verrà mostrato un menu contestuale per sceglierne uno. Proxy HTTP Proxy HTTP usato da questo repository Nome Utente diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 7f2341eb6..22ba24843 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -273,9 +273,9 @@ このルールを .issuetracker ファイルで共有 最終的な URL: 正規表現のグループ値は $1, $2 で取得してください。 - AI - 優先するサービス: - '優先するサービス' を設定すると、このリポジトリではそのサービスのみを使用するようになります。そうでなければ、複数のサービスが存在する場合に限り、その中からひとつを選択できるコンテキストメニューが表示されます。 + AI + 優先するサービス: + '優先するサービス' を設定すると、このリポジトリではそのサービスのみを使用するようになります。そうでなければ、複数のサービスが存在する場合に限り、その中からひとつを選択できるコンテキストメニューが表示されます。 HTTP プロキシ このリポジトリで使用する HTTP プロキシ ユーザー名 @@ -659,7 +659,6 @@ 追加のプロンプト (`-` で必要事項を羅列してください) API キー モデル - 利用できるモデル ID を自動的に取得 名前 この値を環境変数の名前とし、そこから API キーを読み込む サーバー diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index 4698b0993..cab34e3b1 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -260,9 +260,9 @@ .issuetracker 파일에 이 규칙 공유 결과 URL: 정규식 그룹 값에 접근하려면 $1, $2를 사용하세요. - AI - 선호하는 서비스: - '선호하는 서비스'가 설정되면, SourceGit은 이 저장소에서 해당 서비스만 사용합니다. 그렇지 않고 사용 가능한 서비스가 두 개 이상인 경우, 하나를 선택할 수 있는 컨텍스트 메뉴가 표시됩니다. + AI + 선호하는 서비스: + '선호하는 서비스'가 설정되면, SourceGit은 이 저장소에서 해당 서비스만 사용합니다. 그렇지 않고 사용 가능한 서비스가 두 개 이상인 경우, 하나를 선택할 수 있는 컨텍스트 메뉴가 표시됩니다. HTTP 프록시 이 저장소에서 사용하는 HTTP 프록시 사용자 이름 @@ -630,7 +630,6 @@ 추가 프롬프트(`-`로 요구 사항 나열) API 키 모델 - 사용 가능한 모델 ID 자동 가져오기 이름 입력된 값은 환경변수(ENV)에서 API 키를 불러올 이름입니다 서버 diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 8155dc647..1d15d3f5b 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -167,9 +167,9 @@ Nome da Regra: URL de Resultado: Por favor, use $1, $2 para acessar os valores de grupos do regex. - IA - Serviço desejado: - Se o 'Serviço desejado' for definido, SourceGit usará ele neste Repositório. Senão, caso haja mais de um serviço disponível, será exibido um menu para seleção. + IA + Serviço desejado: + Se o 'Serviço desejado' for definido, SourceGit usará ele neste Repositório. Senão, caso haja mais de um serviço disponível, será exibido um menu para seleção. Proxy HTTP Proxy HTTP usado por este repositório Nome de Usuário diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 9bf74e96e..5c68d96bd 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -21,10 +21,10 @@ Переключиться на: Создать новую ветку Ветку из списка - Помощник OpenAI + Помощник AI Модель ПЕРЕСОЗДАТЬ - Использовать OpenAI для создания сообщения о ревизии + Использовать AI для создания сообщения о ревизии Использовать Скрыть SourceGit Скрыть остальные @@ -273,9 +273,9 @@ Опубликовать это правило в файле .issuetracker Адрес результата: Пожалуйста, используйте $1, $2 для доступа к значениям групп регулярных выражений. - ОТКРЫТЬ ИИ - Предпочтительный сервис: - Если «Предпочтительный сервис» установлен, SourceGit будет использовать только этот репозиторий. В противном случае, если доступно более одной услуги, будет отображено контекстное меню для выбора одной из них. + ОТКРЫТЬ ИИ + Предпочтительный сервис: + Если «Предпочтительный сервис» установлен, SourceGit будет использовать только этот репозиторий. В противном случае, если доступно более одной услуги, будет отображено контекстное меню для выбора одной из них. HTTP-прокси HTTP-прокси для репозитория Имя пользователя @@ -648,7 +648,6 @@ Дополнительная подсказка (Для перечисления ваших требований используйте `-`) Ключ API Модель - Автоматическое получение доступных идентификаторов моделей Имя: Введённое значение — это имя для загрузки API-ключа из ENV Сервер diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index e7b89e254..95f3dcaee 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -146,9 +146,9 @@ விதியின் பெயர்: முடிவு முகவரி: வழக்கவெளி குழுக்கள் மதிப்புகளை அணுக $1, $2 ஐப் பயன்படுத்து - செநு - விருப்பமான சேவை: - 'விருப்பமான சேவை' அமைக்கப்பட்டிருந்தால், மூலஅறிவிலி இந்த களஞ்சியத்தில் மட்டுமே அதைப் பயன்படுத்தும். இல்லையெனில், ஒன்றுக்கு மேற்பட்ட சேவைகள் இருந்தால், அவற்றில் ஒன்றைத் தேர்ந்தெடுப்பதற்கான சூழல் பட்டயல் காண்பிக்கப்படும். + செநு + விருப்பமான சேவை: + 'விருப்பமான சேவை' அமைக்கப்பட்டிருந்தால், மூலஅறிவிலி இந்த களஞ்சியத்தில் மட்டுமே அதைப் பயன்படுத்தும். இல்லையெனில், ஒன்றுக்கு மேற்பட்ட சேவைகள் இருந்தால், அவற்றில் ஒன்றைத் தேர்ந்தெடுப்பதற்கான சூழல் பட்டயல் காண்பிக்கப்படும். உஉபநெ பதிலாள் இந்த களஞ்சியத்தால் பயன்படுத்தப்படும் உஉபநெ பதிலாள் பயனர் பெயர் diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index f8a730a99..442b88dc9 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -147,9 +147,9 @@ Назва правила: URL результату: Використовуйте $1, $2 для доступу до значень груп регулярного виразу. - AI - Бажаний сервіс: - Якщо 'Бажаний сервіс' встановлено, SourceGit буде використовувати лише його у цьому сховищі. Інакше, якщо доступно більше одного сервісу, буде показано контекстне меню для вибору. + AI + Бажаний сервіс: + Якщо 'Бажаний сервіс' встановлено, SourceGit буде використовувати лише його у цьому сховищі. Інакше, якщо доступно більше одного сервісу, буде показано контекстне меню для вибору. HTTP Проксі HTTP проксі, що використовується цим сховищем Ім'я користувача diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 36b5f21ec..4ebd713d0 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -7,6 +7,7 @@ 关于本软件 发布日期:{0} 浏览版本更新说明 + 添加 开源免费的Git客户端 新增忽略文件 匹配模式 : @@ -273,9 +274,12 @@ 写入 .issuetracker 文件共享此规则 为ISSUE生成的URL链接 : 可在URL中使用$1,$2等变量填入正则表达式匹配的内容 - AI - 启用特定服务 : - 当【启用特定服务】被设置时,SourceGit将在本仓库中仅使用该服务。否则将弹出可用的AI服务列表供用户选择。 + AI + 默认模型服务: + 默认模型 : + 设置后,SourceGit将在本仓库中直接使用该服务和模型生成提交信息,否则将弹出选择菜单。 + (无) + (无) HTTP代理 HTTP网络代理 用户名 @@ -658,10 +662,15 @@ 附加提示词 (请使用 `-` 列出您的要求) API密钥 模型 - 自动拉取可用模型列表 + 添加模型 + 获取模型列表 + 正在获取模型列表... + 删除模型 + 选择模型 配置名称 从环境变量(填写环境变量名)中读取API密钥 服务地址 + 模型服务 外观配置 缺省字体 编辑器制表符宽度 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 6a238a5ea..58af08d24 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -7,6 +7,7 @@ 關於 SourceGit 發行日期: {0} 版本說明 + 新增 開源免費的 Git 客戶端 新增忽略檔案 比對模式: @@ -273,9 +274,10 @@ 寫入 .issuetracker 檔案以共用此規則 為 Issue 產生的網址連結: 可在網址中使用 $1、$2 等變數填入正規表達式相符的內容 - AI - 偏好服務: - 設定 [偏好服務] 後,SourceGit 將於此存放庫中使用該服務,否則會顯示 AI 服務列表供使用者選擇。 + AI + 預設模型服務: + 預設模型: + 設定後,SourceGit將於此存放庫中直接使用該服務和模型生成提交資訊,否則會顯示選擇選單。 HTTP 代理 HTTP 網路代理 使用者名稱 @@ -658,10 +660,15 @@ 附加提示詞 (請使用 '-' 列出您的要求) API 金鑰 模型 - 自動獲取可用的模型列表 + 新增模型 + 取得模型列表 + 正在取得模型列表... + 刪除模型 + 選擇模型 名稱 從環境變數中 (輸入環境變數名稱) 讀取 API 金鑰 伺服器 + 模型服務 外觀設定 預設字型 編輯器 Tab 寬度 diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index 4f54678c2..d94cf3938 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Threading; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index ce0942d4c..9f2a32561 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -3,7 +3,6 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; @@ -624,20 +623,6 @@ public void AutoRemoveInvalidNode() public void UpdateAvailableAIModels() { - Task.Run(() => - { - foreach (var service in OpenAIServices) - { - try - { - service.FetchAvailableModels(); - } - catch - { - // Ignore errors. - } - } - }); } public void Save() diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 8c08dbe7f..33b1d7c5d 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -1583,26 +1583,33 @@ public async Task UnlockWorktreeAsync(Worktree worktree) log.Complete(); } - public List GetPreferredOpenAIServices() + public List GetAllOpenAIServices() { - var services = Preferences.Instance.OpenAIServices; - if (services == null || services.Count == 0) - return []; + return new List(Preferences.Instance.OpenAIServices ?? []); + } - if (services.Count == 1) - return [services[0]]; + public (string Service, string Model) GetOpenAIConfig() + { + return (_settings.PreferredOpenAIService, _settings.PreferredAIModel); + } - var preferred = _settings.PreferredOpenAIService; - var all = new List(); - foreach (var service in services) - { - if (service.Name.Equals(preferred, StringComparison.Ordinal)) - return [service]; + public AI.Service ResolveAIService() + { + var allServices = GetAllOpenAIServices(); + var (preferredServiceName, preferredModelName) = GetOpenAIConfig(); - all.Add(service); - } + if (string.IsNullOrEmpty(preferredServiceName) || allServices.Count == 0) + return null; + + var service = allServices.Find(s => s.Name == preferredServiceName); + if (service == null) + return null; + + var resolved = service.Clone(); + if (!string.IsNullOrEmpty(preferredModelName)) + resolved.Model = preferredModelName; - return all; + return resolved; } public void DiscardAllChanges() diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index a76194716..23b97dece 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -137,8 +137,72 @@ public List AvailableOpenAIServices public string PreferredOpenAIService { - get => _repo.Settings.PreferredOpenAIService; - set => _repo.Settings.PreferredOpenAIService = value; + get + { + var value = _repo.Settings.PreferredOpenAIService; + if (string.IsNullOrEmpty(value)) + return App.Text("Configure.AI.DefaultService.None"); + return value; + } + set + { + var noneText = App.Text("Configure.AI.DefaultService.None"); + var storedValue = (value == noneText) ? "" : value; + + if (_repo.Settings.PreferredOpenAIService != storedValue) + { + _repo.Settings.PreferredOpenAIService = storedValue; + + var modelNoneText = App.Text("Configure.AI.PreferredModel.None"); + var models = GetModelsForService(storedValue); + if (models.Count > 0) + models.Insert(0, modelNoneText); + + AvailableAIModels = models; + IsModelSelectorEnabled = models.Count > 0; + + if (!AvailableAIModels.Contains(PreferredAIModel)) + PreferredAIModel = modelNoneText; + + OnPropertyChanged(); + OnPropertyChanged(nameof(AvailableAIModels)); + OnPropertyChanged(nameof(IsModelSelectorEnabled)); + } + } + } + + public bool IsModelSelectorEnabled + { + get => _isModelSelectorEnabled; + private set => SetProperty(ref _isModelSelectorEnabled, value); + } + + public List AvailableAIModels + { + get => _availableAIModels; + private set => SetProperty(ref _availableAIModels, value); + } + + public string PreferredAIModel + { + get + { + var value = _repo.Settings.PreferredAIModel; + if (string.IsNullOrEmpty(value)) + return App.Text("Configure.AI.PreferredModel.None"); + return value; + } + set + { + var noneText = App.Text("Configure.AI.PreferredModel.None"); + var storedValue = (value == noneText) ? "" : value; + + if (_repo.Settings.PreferredAIModel != storedValue) + { + _repo.Settings.PreferredAIModel = storedValue; + OnPropertyChanged(); + } + } } public AvaloniaList CustomActions @@ -160,12 +224,27 @@ public RepositoryConfigure(Repository repo) foreach (var remote in _repo.Remotes) Remotes.Add(remote.Name); - AvailableOpenAIServices = new List() { "---" }; + var serviceNoneText = App.Text("Configure.AI.DefaultService.None"); + var modelNoneText = App.Text("Configure.AI.PreferredModel.None"); + + AvailableOpenAIServices = new List() { serviceNoneText }; foreach (var service in Preferences.Instance.OpenAIServices) AvailableOpenAIServices.Add(service.Name); if (!AvailableOpenAIServices.Contains(PreferredOpenAIService)) - PreferredOpenAIService = "---"; + PreferredOpenAIService = serviceNoneText; + + // 使用存储值判断,而不是 getter 返回的本地化文本 + var storedService = _repo.Settings.PreferredOpenAIService; + var models = GetModelsForService(storedService); + if (models.Count > 0) + models.Insert(0, modelNoneText); + + AvailableAIModels = models; + IsModelSelectorEnabled = models.Count > 0; + + if (!AvailableAIModels.Contains(PreferredAIModel)) + PreferredAIModel = modelNoneText; _cached = new Commands.Config(repo.FullPath).ReadAll(); if (_cached.TryGetValue("user.name", out var name)) @@ -347,9 +426,24 @@ private async Task ApplyIssueTrackerChangesAsync() } } + private List GetModelsForService(string serviceName) + { + if (string.IsNullOrEmpty(serviceName)) + return []; + + foreach (var service in Preferences.Instance.OpenAIServices) + { + if (service.Name == serviceName) + return new List(service.AvailableModels); + } + return []; + } + private readonly Repository _repo; private readonly Dictionary _cached; private string _httpProxy; + private bool _isModelSelectorEnabled; + private List _availableAIModels = []; private Models.CommitTemplate _selectedCommitTemplate = null; private Models.IssueTracker _selectedIssueTracker = null; private Models.CustomAction _selectedCustomAction = null; diff --git a/src/Views/AIAssistant.axaml.cs b/src/Views/AIAssistant.axaml.cs index ddd601be2..44570e93d 100644 --- a/src/Views/AIAssistant.axaml.cs +++ b/src/Views/AIAssistant.axaml.cs @@ -110,6 +110,8 @@ private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs apply.Click += (_, ev) => { vm.Use(selected); + if (TopLevel.GetTopLevel(this) is Window window) + window.Close(); ev.Handled = true; }; @@ -166,7 +168,10 @@ private async void OnModelChanged(object sender, SelectionChangedEventArgs e) private void OnUseClicked(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.AIAssistant vm && !string.IsNullOrEmpty(vm.Response)) + { vm.Use(vm.Response); + Close(); + } e.Handled = true; } diff --git a/src/Views/CommitMessageToolBox.axaml b/src/Views/CommitMessageToolBox.axaml index dbdadafc0..7cf56aae7 100644 --- a/src/Views/CommitMessageToolBox.axaml +++ b/src/Views/CommitMessageToolBox.axaml @@ -106,7 +106,7 @@ Classes="icon_button" Width="28" Margin="0,0,4,0" Padding="0" - Click="OnOpenOpenAIHelper" + Click="OnOpenAIHelper" IsVisible="{Binding #ThisControl.ShowAdvancedOptions}" ToolTip.Tip="{DynamicResource Text.AIAssistant.Tip}"> diff --git a/src/Views/CommitMessageToolBox.axaml.cs b/src/Views/CommitMessageToolBox.axaml.cs index 238419d70..41d0c5909 100644 --- a/src/Views/CommitMessageToolBox.axaml.cs +++ b/src/Views/CommitMessageToolBox.axaml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using Avalonia; @@ -649,7 +650,7 @@ private async void OnOpenCommitMessagePicker(object sender, RoutedEventArgs e) e.Handled = true; } - private void OnOpenOpenAIHelper(object sender, RoutedEventArgs e) + private void OnOpenAIHelper(object sender, RoutedEventArgs e) { if (DataContext is ViewModels.WorkingCopy vm && sender is Button button && _showAdvancedOptions) { @@ -662,43 +663,59 @@ private void OnOpenOpenAIHelper(object sender, RoutedEventArgs e) return; } - var services = repo.GetPreferredOpenAIServices(); - if (services.Count == 0) + var resolved = repo.ResolveAIService(); + if (resolved != null) { - repo.SendNotification("Bad configuration for OpenAI", true); - e.Handled = true; - return; + DoOpenAIAssistant(repo, resolved, vm.Staged); } - - if (services.Count == 1) + else { - DoOpenAIAssistant(repo, services[0], vm.Staged); - e.Handled = true; - return; + var allServices = repo.GetAllOpenAIServices(); + if (allServices.Count == 0) + { + repo.SendNotification("Bad configuration for AI", true); + } + else + { + ShowOpenAIServicesMenu(repo, allServices, vm.Staged, button); + } } + } - var menu = new ContextMenu(); - foreach (var service in services) + e.Handled = true; + } + + private void ShowOpenAIServicesMenu(ViewModels.Repository repo, List services, List changes, Button button) + { + var menu = new ContextMenu(); + foreach (var service in services) + { + var item = new MenuItem(); + item.Header = service.Name; + + foreach (var model in service.AvailableModels) { var dup = service; - var item = new MenuItem(); - item.Header = service.Name; - item.Click += (_, ev) => + var dupModel = model; + var modelItem = new MenuItem(); + modelItem.Header = dupModel; + modelItem.Click += (_, ev) => { - DoOpenAIAssistant(repo, dup, vm.Staged); + var cloned = dup.Clone(); + cloned.Model = dupModel; + DoOpenAIAssistant(repo, cloned, changes); ev.Handled = true; }; - - menu.Items.Add(item); + item.Items.Add(modelItem); } - button.IsEnabled = false; - menu.Placement = PlacementMode.TopEdgeAlignedLeft; - menu.Closed += (_, _) => button.IsEnabled = true; - menu.Open(button); + menu.Items.Add(item); } - e.Handled = true; + button.IsEnabled = false; + menu.Placement = PlacementMode.TopEdgeAlignedLeft; + menu.Closed += (_, _) => button.IsEnabled = true; + menu.Open(button); } private void OnOpenConventionalCommitHelper(object _, RoutedEventArgs e) diff --git a/src/Views/FetchAIModels.axaml b/src/Views/FetchAIModels.axaml new file mode 100644 index 000000000..df9e6b0e1 --- /dev/null +++ b/src/Views/FetchAIModels.axaml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -897,7 +901,8 @@ - + + @@ -912,25 +917,82 @@ IsChecked="{Binding ReadApiKeyFromEnv, Mode=TwoWay}"/> - - + + + + + + + + + + + + + + + + + + + + + + + - - + TextWrapping="Wrap"/> + + diff --git a/src/Views/Preferences.axaml.cs b/src/Views/Preferences.axaml.cs index 99ec73a5f..76fbf0cda 100644 --- a/src/Views/Preferences.axaml.cs +++ b/src/Views/Preferences.axaml.cs @@ -222,7 +222,6 @@ protected override async void OnClosing(WindowClosingEventArgs e) } var preferences = ViewModels.Preferences.Instance; - preferences.UpdateAvailableAIModels(); preferences.Save(); } @@ -438,6 +437,108 @@ private void OnRemoveSelectedOpenAIService(object sender, RoutedEventArgs e) e.Handled = true; } + private async void OnFetchAIModels(object sender, RoutedEventArgs e) + { + if (SelectedOpenAIService == null) + return; + + var dialog = new FetchAIModels(); + dialog.LoadModels(SelectedOpenAIService); + await this.ShowDialogAsync(dialog); + + var serverSet = new HashSet(dialog.ServerModels); + var customModels = new List(); + foreach (var model in SelectedOpenAIService.AvailableModels) + { + if (!serverSet.Contains(model)) + customModels.Add(model); + } + + var newList = new List(dialog.SelectedModels); + foreach (var model in customModels) + { + if (!newList.Contains(model)) + newList.Add(model); + } + + SelectedOpenAIService.AvailableModels = newList; + + if (string.IsNullOrEmpty(SelectedOpenAIService.Model) && SelectedOpenAIService.AvailableModels.Count > 0) + SelectedOpenAIService.Model = SelectedOpenAIService.AvailableModels[0]; + + e.Handled = true; + } + + private async void OnAddAIModel(object sender, RoutedEventArgs e) + { + if (SelectedOpenAIService == null) + return; + + var dialog = new TextInput(); + dialog.SetData(App.Text("Preferences.AI.Model.AddModel")); + await this.ShowDialogAsync(dialog); + + if (dialog.Value != null && !string.IsNullOrWhiteSpace(dialog.Value)) + { + var model = dialog.Value.Trim(); + SelectedOpenAIService.AddModel(model); + + if (string.IsNullOrEmpty(SelectedOpenAIService.Model)) + SelectedOpenAIService.Model = model; + } + + e.Handled = true; + } + + private void OnRemoveAIModelInList(object sender, RoutedEventArgs e) + { + if (SelectedOpenAIService == null) + return; + + if (sender is Button btn && btn.Tag is string model) + { + SelectedOpenAIService.RemoveModel(model); + + if (SelectedOpenAIService.Model == model) + { + if (SelectedOpenAIService.AvailableModels.Count > 0) + SelectedOpenAIService.Model = SelectedOpenAIService.AvailableModels[0]; + else + SelectedOpenAIService.Model = string.Empty; + } + } + + e.Handled = true; + } + + private async void OnEditAIModelInList(object sender, RoutedEventArgs e) + { + if (SelectedOpenAIService == null) + return; + + if (sender is Button btn && btn.Tag is string oldModel) + { + var dialog = new TextInput(); + dialog.SetData(App.Text("Preferences.AI.Model.AddModel"), oldModel); + await this.ShowDialogAsync(dialog); + + if (dialog.Value != null && !string.IsNullOrWhiteSpace(dialog.Value)) + { + var newModel = dialog.Value.Trim(); + if (newModel != oldModel) + { + SelectedOpenAIService.RemoveModel(oldModel); + SelectedOpenAIService.AddModel(newModel); + + if (SelectedOpenAIService.Model == oldModel) + SelectedOpenAIService.Model = newModel; + } + } + } + + e.Handled = true; + } + private void OnAddCustomAction(object sender, RoutedEventArgs e) { var action = new Models.CustomAction() { Name = "Unnamed Action (Global)" }; diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 8509df574..c2cef1864 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -519,14 +519,14 @@ - + - + + Text="{DynamicResource Text.Configure.AI.DefaultService}"/> - + + + diff --git a/src/Views/TextInput.axaml b/src/Views/TextInput.axaml new file mode 100644 index 000000000..c75a9c9b0 --- /dev/null +++ b/src/Views/TextInput.axaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + +