Skip to content

Commit 0ab0e04

Browse files
authored
Merge pull request #606 from Freeesia/copilot/add-history-function-to-llm-plugin-model
2 parents 1be45c0 + f6ef443 commit 0ab0e04

10 files changed

Lines changed: 172 additions & 2 deletions

File tree

.github/copilot-instructions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,15 @@ dotnet nuget-license -t -ignore ignore-packages.json -override package-informati
120120
* `IAsyncEnumerable<TextRect>``IFilterModule` のパイプラインで使用(ストリーミング処理)
121121
* Windows固有APIは `#if WINDOWS` で条件コンパイル(`WindowTranslator.Abstractions` はクロスプラットフォームビルド対応)
122122

123+
## 指示の日本語解釈
124+
125+
### 助詞「に」と「を」で既存/新規を区別する
126+
127+
日本語の助詞が対象の存在を示す:
128+
129+
- **「〇〇****追加して」「〇〇****完結するようにして」** → 〇〇は**既存のもの**。新規作成ではなく、既存の〇〇に機能を追加・変更する
130+
- **「〇〇****追加して」「〇〇****作って」** → 〇〇はまだ存在しない。**新規作成**する
131+
132+
この区別を誤ると、既存コンポーネントへの修正指示を新規クラス作成と誤解するため、必ず助詞を確認してから変更範囲を判断する。
133+
123134

Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/GitHubCopilotOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace WindowTranslator.Plugin.GitHubCopilotPlugin;
77

88
public class GitHubCopilotOptions : IPluginParam
99
{
10+
[EditableItemsSource]
1011
[LocalizedDescription(typeof(Resources), $"{nameof(Model)}_Desc")]
1112
public string Model { get; set; } = "gpt-5-mini";
1213

Plugins/WindowTranslator.Plugin.LLMPlugin/LLMOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class LLMOptions : IPluginParam
1313

1414
public bool WaitCorrect { get; set; }
1515

16+
[EditableItemsSource]
1617
public string? Model { get; set; } = "gpt-4o-mini";
1718

1819
[DataType(DataType.Password)]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace WindowTranslator.ComponentModel;
2+
3+
/// <summary>
4+
/// プロパティを編集可能なComboBoxとして表示するためのマーカー属性です。
5+
/// 候補一覧はView側の履歴ストアから自動的に提供されます。
6+
/// </summary>
7+
[AttributeUsage(AttributeTargets.Property)]
8+
public class EditableItemsSourceAttribute : Attribute
9+
{
10+
}

WindowTranslator/Modules/Settings/AllSettingsDialog.xaml.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Windows;
33
using System.Windows.Data;
44
using System.Windows.Markup;
5+
using WindowTranslator.Stores;
56
using Wpf.Ui;
67
using Wpf.Ui.Appearance;
78
using Wpf.Ui.Controls;
@@ -13,12 +14,16 @@ namespace WindowTranslator.Modules.Settings;
1314
/// </summary>
1415
public partial class AllSettingsDialog : FluentWindow
1516
{
16-
public AllSettingsDialog(IContentDialogService contentDialogService)
17+
public AllSettingsDialog(IContentDialogService contentDialogService, IModelHistoryStore modelHistoryStore)
1718
{
1819
SystemThemeWatcher.Watch(this);
1920
InitializeComponent();
2021
this.Language = XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.IetfLanguageTag);
2122
contentDialogService.SetDialogHost(this.RootContentDialog);
23+
if (this.Resources["operator"] is SettingsPropertyGridOperator op)
24+
{
25+
op.HistoryStore = modelHistoryStore;
26+
}
2227
}
2328
}
2429

WindowTranslator/Modules/Settings/AllSettingsViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ public async Task SaveAsync(object window)
301301
this.autoTargetStore.AutoTargets.Clear();
302302
this.autoTargetStore.AutoTargets.UnionWith(this.AutoTargets);
303303
this.autoTargetStore.Save();
304+
304305
this.rootConfig?.Reload();
305306
if (this.ApplyMode)
306307
{
@@ -479,6 +480,7 @@ public partial class TargetSettingsViewModel(
479480
{
480481
configureMethod.Invoke(configure, [name, p]);
481482
}
483+
482484
return p;
483485
}).ToArray();
484486
}

WindowTranslator/Modules/Settings/SettingsPropertyGridFactory.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using PropertyTools.DataAnnotations;
22
using PropertyTools.Wpf;
3+
using System.Collections;
34
using System.ComponentModel.DataAnnotations;
45
using System.Globalization;
56
using System.Reflection;
@@ -42,6 +43,19 @@ public override FrameworkElement CreateControl(PropertyItem property, PropertyCo
4243
fe.SetBinding(TextBox.TextProperty, property.CreateBinding());
4344
}
4445

46+
// EditableItemsSourceAttributeが指定されている場合、編集可能ComboBoxを生成
47+
if (fe == null && property is IEditableItemsPropertyItem editableItem && editableItem.EditableCandidates != null)
48+
{
49+
var comboBox = new ComboBox
50+
{
51+
IsEditable = true,
52+
IsTextSearchEnabled = true,
53+
ItemsSource = editableItem.EditableCandidates,
54+
};
55+
comboBox.SetBinding(ComboBox.TextProperty, property.CreateBinding(UpdateSourceTrigger.PropertyChanged));
56+
fe = comboBox;
57+
}
58+
4559
fe ??= base.CreateControl(property, options);
4660

4761
if (property.Descriptor.Attributes.Matches(enableAttribute))

WindowTranslator/Modules/Settings/SettingsPropertyGridOperator.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
using PropertyTools.Wpf;
2+
using System.Collections;
23
using System.ComponentModel.DataAnnotations;
34
using System.ComponentModel;
45
using System.Windows.Data;
56
using System.Globalization;
67
using System.Windows.Input;
78
using PropertyTools.DataAnnotations;
9+
using WindowTranslator.ComponentModel;
10+
using WindowTranslator.Stores;
811

912
namespace WindowTranslator.Modules.Settings;
13+
14+
internal interface IEditableItemsPropertyItem
15+
{
16+
IEnumerable? EditableCandidates { get; set; }
17+
}
18+
1019
internal class SettingsPropertyGridOperator : PropertyGridOperator
1120
{
21+
public IModelHistoryStore? HistoryStore { get; set; }
22+
1223
public SettingsPropertyGridOperator()
1324
{
1425
this.ModifyCamelCaseDisplayNames = false;
@@ -86,17 +97,35 @@ protected override void SetAttribute(Attribute attribute, PropertyItem pi, objec
8697
{
8798
pi.SortIndex = order;
8899
}
100+
if (attribute is EditableItemsSourceAttribute _ && pi is IEditableItemsPropertyItem editableItem)
101+
{
102+
var key = $"{instance.GetType().Name}.{pi.Descriptor.Name}";
103+
editableItem.EditableCandidates = this.HistoryStore?.GetHistory(key) ?? [];
104+
if (this.HistoryStore is { } store)
105+
{
106+
pi.Descriptor.AddValueChanged(instance, (s, e) =>
107+
{
108+
if (pi.Descriptor.GetValue(instance) is string value && !string.IsNullOrWhiteSpace(value))
109+
{
110+
store.AddHistory(key, value);
111+
store.Save();
112+
}
113+
});
114+
}
115+
}
89116
base.SetAttribute(attribute, pi, instance);
90117
}
91118

92119
protected override PropertyItem CreateCore(PropertyDescriptor pd, PropertyDescriptorCollection propertyDescriptors)
93120
=> new ParentablePropertyItem(pd, propertyDescriptors);
94121

95122
private class ParentablePropertyItem(PropertyDescriptor propertyDescriptor, PropertyDescriptorCollection propertyDescriptors)
96-
: PropertyItem(propertyDescriptor, propertyDescriptors)
123+
: PropertyItem(propertyDescriptor, propertyDescriptors), IEditableItemsPropertyItem
97124
{
98125
private readonly Stack<string> parents = new();
99126

127+
public IEnumerable? EditableCandidates { get; set; }
128+
100129
public void AddParent(string parent)
101130
=> parents.Push(parent);
102131

WindowTranslator/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133

134134
builder.Services.AddSingleton<IMainWindowModule, MainWindowModule>();
135135
builder.Services.AddSingleton<IAutoTargetStore, AutoTargetStore>();
136+
builder.Services.AddSingleton<IModelHistoryStore, ModelHistoryStore>();
136137
builder.Services.AddHostedService<WindowMonitor>();
137138
if (builder.Configuration.GetValue<bool>("IgnoreUpdate"))
138139
{
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System.IO;
2+
using System.Text.Json;
3+
4+
namespace WindowTranslator.Stores;
5+
6+
/// <summary>
7+
/// モデル名の利用履歴を管理するストアのインターフェースです。
8+
/// </summary>
9+
public interface IModelHistoryStore
10+
{
11+
/// <summary>
12+
/// 指定キーの履歴を取得します。
13+
/// </summary>
14+
IReadOnlyList<string> GetHistory(string key);
15+
16+
/// <summary>
17+
/// 指定キーに値を追加します。既存の値がある場合は先頭に移動します。
18+
/// </summary>
19+
void AddHistory(string key, string value);
20+
21+
/// <summary>
22+
/// 履歴をファイルに保存します。
23+
/// </summary>
24+
void Save();
25+
}
26+
27+
/// <inheritdoc cref="IModelHistoryStore"/>
28+
public class ModelHistoryStore : IModelHistoryStore
29+
{
30+
private static readonly string historyPath = Path.Combine(PathUtility.UserDir, "model-history.json");
31+
private const int MaxHistoryCount = 10;
32+
33+
private readonly Dictionary<string, List<string>> history;
34+
35+
/// <summary>
36+
/// <see cref="ModelHistoryStore"/> の新しいインスタンスを初期化します。
37+
/// </summary>
38+
public ModelHistoryStore()
39+
{
40+
if (File.Exists(historyPath))
41+
{
42+
try
43+
{
44+
using var fs = File.OpenRead(historyPath);
45+
this.history = JsonSerializer.Deserialize<Dictionary<string, List<string>>>(fs) ?? [];
46+
}
47+
catch (JsonException)
48+
{
49+
this.history = [];
50+
}
51+
catch (IOException)
52+
{
53+
this.history = [];
54+
}
55+
}
56+
else
57+
{
58+
this.history = [];
59+
}
60+
}
61+
62+
/// <inheritdoc/>
63+
public IReadOnlyList<string> GetHistory(string key)
64+
=> this.history.TryGetValue(key, out var list) ? list.AsReadOnly() : [];
65+
66+
/// <inheritdoc/>
67+
public void AddHistory(string key, string value)
68+
{
69+
if (string.IsNullOrWhiteSpace(value))
70+
{
71+
return;
72+
}
73+
74+
if (!this.history.TryGetValue(key, out var list))
75+
{
76+
list = [];
77+
this.history[key] = list;
78+
}
79+
80+
list.Remove(value);
81+
list.Insert(0, value);
82+
83+
if (list.Count > MaxHistoryCount)
84+
{
85+
list.RemoveRange(MaxHistoryCount, list.Count - MaxHistoryCount);
86+
}
87+
}
88+
89+
/// <inheritdoc/>
90+
public void Save()
91+
{
92+
Directory.CreateDirectory(PathUtility.UserDir);
93+
using var fs = File.Create(historyPath);
94+
JsonSerializer.Serialize(fs, this.history);
95+
}
96+
}

0 commit comments

Comments
 (0)