|
| 1 | +using System; |
| 2 | +using System.Windows; |
| 3 | +using System.Windows.Controls; |
| 4 | +using System.ComponentModel; |
| 5 | +using DevExpress.Blazor.Internal; |
| 6 | +using Microsoft.AspNetCore.Components.WebView.Wpf; |
| 7 | +using DevExpress.AIIntegration.Blazor.Chat; |
| 8 | +using DevExpress.AIIntegration.Services.Assistant; |
| 9 | +using Microsoft.AspNetCore.Components; |
| 10 | +using Microsoft.Extensions.DependencyInjection; |
| 11 | +using DevExpress.AIIntegration.Blazor.Chat.WebView; |
| 12 | +using DevExpress.Utils; |
| 13 | +using System.Collections.Generic; |
| 14 | +using System.Drawing; |
| 15 | +using DevExpress.AIIntegration; |
| 16 | +using Microsoft.Extensions.AI; |
| 17 | +using System.Threading.Tasks; |
| 18 | +using DevExpress.Xpf.Printing.Native; |
| 19 | + |
| 20 | +namespace WPF_AIChatControl { |
| 21 | + public class AIChatControl : Control { |
| 22 | + |
| 23 | + public static readonly DependencyProperty UseStreamingProperty; |
| 24 | + public static readonly DependencyProperty ContentFormatProperty; |
| 25 | + public static readonly DependencyProperty EmptyStateTextProperty; |
| 26 | + public static readonly DependencyProperty TemperatureProperty; |
| 27 | + public static readonly DependencyProperty MaxTokensProperty; |
| 28 | + public static readonly DependencyProperty FrequencyPenaltyProperty; |
| 29 | + public static readonly DependencyProperty ControlBackgroundProperty; |
| 30 | + public static readonly DependencyProperty ItemBackgroundProperty; |
| 31 | + |
| 32 | + static AIChatControl() { |
| 33 | + var ownerType = typeof(AIChatControl); |
| 34 | + UseStreamingProperty = DependencyProperty.Register(nameof(UseStreaming), typeof(bool), ownerType, |
| 35 | + new PropertyMetadata(false, (d, e) => ((AIChatControl)d).OnUseStreamingChanged())); |
| 36 | + ContentFormatProperty = DependencyProperty.Register(nameof(ContentFormat), typeof(ResponseContentFormat), ownerType, |
| 37 | + new PropertyMetadata(ResponseContentFormat.PlainText, (d, e) => ((AIChatControl)d).OnContentFormatChanged())); |
| 38 | + EmptyStateTextProperty = DependencyProperty.Register(nameof(EmptyStateText), typeof(string), ownerType, |
| 39 | + new PropertyMetadata(string.Empty, (d, e) => ((AIChatControl)d).OnEmptyStateTextChanged())); |
| 40 | + TemperatureProperty = DependencyProperty.Register(nameof(Temperature), typeof(float?), ownerType, |
| 41 | + new PropertyMetadata(null, (d, e) => ((AIChatControl)d).OnTemperatureChanged())); |
| 42 | + MaxTokensProperty = DependencyProperty.Register(nameof(MaxTokens), typeof(int?), ownerType, |
| 43 | + new PropertyMetadata(null, (d, e) => ((AIChatControl)d).OnMaxTokensChanged())); |
| 44 | + FrequencyPenaltyProperty = DependencyProperty.Register(nameof(FrequencyPenalty), typeof(float?), ownerType, |
| 45 | + new PropertyMetadata(null, (d, e) => ((AIChatControl)d).OnFrequencyPenaltyChanged())); |
| 46 | + ControlBackgroundProperty = DependencyProperty.Register(nameof(ControlBackground), typeof(System.Windows.Media.Brush), ownerType, |
| 47 | + new PropertyMetadata(null, (d, e) => ((AIChatControl)d).OnControlBackgroundChanged())); |
| 48 | + ItemBackgroundProperty = DependencyProperty.Register(nameof(ItemBackground), typeof(System.Windows.Media.Brush), ownerType, |
| 49 | + new PropertyMetadata(null, (d, e) => ((AIChatControl)d).OnItemBackgroundChanged())); |
| 50 | + } |
| 51 | + |
| 52 | + DxChatIncapsulationService incapsulationService; |
| 53 | + RootComponent blazorChatComponent; |
| 54 | + ChatBlazorWebView chatWebView; |
| 55 | + |
| 56 | + public bool UseStreaming { |
| 57 | + get => (bool)GetValue(UseStreamingProperty); |
| 58 | + set => SetValue(UseStreamingProperty, value); |
| 59 | + } |
| 60 | + public ResponseContentFormat ContentFormat { |
| 61 | + get => (ResponseContentFormat)GetValue(ContentFormatProperty); |
| 62 | + set => SetValue(ContentFormatProperty, value); |
| 63 | + } |
| 64 | + public string EmptyStateText { |
| 65 | + get => (string)GetValue(EmptyStateTextProperty); |
| 66 | + set => SetValue(EmptyStateTextProperty, value); |
| 67 | + } |
| 68 | + public float? Temperature { |
| 69 | + get => (float?)GetValue(TemperatureProperty); |
| 70 | + set => SetValue(TemperatureProperty, value); |
| 71 | + } |
| 72 | + public int? MaxTokens { |
| 73 | + get => (int?)GetValue(MaxTokensProperty); |
| 74 | + set => SetValue(MaxTokensProperty, value); |
| 75 | + } |
| 76 | + public float? FrequencyPenalty { |
| 77 | + get => (float?)GetValue(FrequencyPenaltyProperty); |
| 78 | + set => SetValue(FrequencyPenaltyProperty, value); |
| 79 | + } |
| 80 | + public System.Windows.Media.Brush ControlBackground { |
| 81 | + get => (System.Windows.Media.Brush)GetValue(ControlBackgroundProperty); |
| 82 | + set => SetValue(ControlBackgroundProperty, value); |
| 83 | + } |
| 84 | + public System.Windows.Media.Brush ItemBackground { |
| 85 | + get => (System.Windows.Media.Brush)GetValue(ItemBackgroundProperty); |
| 86 | + set => SetValue(ItemBackgroundProperty, value); |
| 87 | + } |
| 88 | + |
| 89 | + IChatUIWrapper Chat => incapsulationService?.DxChatUI; |
| 90 | + |
| 91 | + EventHandler<AIChatControlMarkdownConvertEventArgs> markdownConvert; |
| 92 | + public event EventHandler<AIChatControlMarkdownConvertEventArgs> MarkdownConvert { |
| 93 | + add => markdownConvert += value; |
| 94 | + remove => markdownConvert -= value; |
| 95 | + } |
| 96 | + EventHandler<AIChatControlMessageSentEventArgs> messageSent; |
| 97 | + public event EventHandler<AIChatControlMessageSentEventArgs> MessageSent { |
| 98 | + add { |
| 99 | + if(messageSent == null && Chat != null) |
| 100 | + Chat.SetMessageSentCallback(RaiseMessageSent); |
| 101 | + messageSent += value; |
| 102 | + } |
| 103 | + remove { |
| 104 | + messageSent -= value; |
| 105 | + if(messageSent == null && Chat != null) |
| 106 | + Chat.SetMessageSentCallback(null); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + public async Task SendMessage(string text, ChatRole role) { |
| 111 | + if (Chat != null) { |
| 112 | + await Chat.SendMessage(text, role); |
| 113 | + Chat.Update(); |
| 114 | + } |
| 115 | + } |
| 116 | + public IEnumerable<BlazorChatMessage> SaveMessages() { |
| 117 | + return Chat?.SaveMessages() ?? []; |
| 118 | + } |
| 119 | + public void LoadMessages(IEnumerable<BlazorChatMessage> messages) { |
| 120 | + if (Chat != null) { |
| 121 | + Chat.LoadMessages(messages); |
| 122 | + Chat.Update(); |
| 123 | + } |
| 124 | + } |
| 125 | + public override void OnApplyTemplate() { |
| 126 | + base.OnApplyTemplate(); |
| 127 | + this.chatWebView = GetTemplateChild("PART_ChatWebView") as ChatBlazorWebView; |
| 128 | + ConfigureBlazorWebView(); |
| 129 | + } |
| 130 | + |
| 131 | + void RaiseMessageSent(MessageSentEventArgs args) { |
| 132 | + messageSent?.Invoke(this, new AIChatControlMessageSentEventArgs(Chat, args.Content)); |
| 133 | + } |
| 134 | + MarkupString RaiseMarkdownConvert(string text) { |
| 135 | + if(markdownConvert != null) { |
| 136 | + var e = new AIChatControlMarkdownConvertEventArgs(text); |
| 137 | + markdownConvert(this, e); |
| 138 | + return e.HtmlText ?? new MarkupString(); |
| 139 | + } |
| 140 | + return new MarkupString(); |
| 141 | + } |
| 142 | + void OnUseStreamingChanged() { |
| 143 | + if(Chat != null) { |
| 144 | + Chat.UseStreaming = UseStreaming; |
| 145 | + Chat.Update(); |
| 146 | + } |
| 147 | + } |
| 148 | + void OnContentFormatChanged() { |
| 149 | + if(Chat != null) { |
| 150 | + Chat.ResponseContentFormat = ContentFormat; |
| 151 | + Chat.SetMarkdownConvertCallback(ContentFormat == ResponseContentFormat.Markdown ? RaiseMarkdownConvert : null); |
| 152 | + Chat.Update(); |
| 153 | + } |
| 154 | + } |
| 155 | + void OnEmptyStateTextChanged() { |
| 156 | + if(Chat != null) { |
| 157 | + Chat.SetEmptyStateText(EmptyStateText); |
| 158 | + Chat.Update(); |
| 159 | + } |
| 160 | + } |
| 161 | + void OnTemperatureChanged() { |
| 162 | + if(Chat != null) |
| 163 | + Chat.Temperature = Temperature; |
| 164 | + } |
| 165 | + void OnMaxTokensChanged() { |
| 166 | + if(Chat != null) |
| 167 | + Chat.MaxTokens = MaxTokens; |
| 168 | + } |
| 169 | + void OnFrequencyPenaltyChanged() { |
| 170 | + if(Chat != null) |
| 171 | + Chat.FrequencyPenalty = FrequencyPenalty; |
| 172 | + } |
| 173 | + void OnItemBackgroundChanged() { |
| 174 | + if(Chat == null) |
| 175 | + return; |
| 176 | + Chat.Colors[ChatUIColor.UserMessageBackground] = ItemBackground.ToColor(); |
| 177 | + } |
| 178 | + void OnControlBackgroundChanged() { |
| 179 | + if(Chat == null) |
| 180 | + return; |
| 181 | + var controlBackground = ControlBackground.ToColor(); |
| 182 | + Chat.Colors[ChatUIColor.SubmitAreaBackground] = controlBackground; |
| 183 | + Chat.Colors[ChatUIColor.InputBackground] = controlBackground; |
| 184 | + Chat.Colors[ChatUIColor.AssistantMessageBackground] = controlBackground; |
| 185 | + Chat.Colors[ChatUIColor.ButtonNormalBackground] = controlBackground; |
| 186 | + Chat.Colors[ChatUIColor.ButtonDisabledBackground] = controlBackground; |
| 187 | + |
| 188 | + } |
| 189 | + void OnBackgroundChanged() { |
| 190 | + if(Chat == null) |
| 191 | + return; |
| 192 | + var background = Background.ToColor(); |
| 193 | + Chat.Colors[ChatUIColor.Background] = background; |
| 194 | + Chat.Colors[ChatUIColor.ButtonHoverBackground] = background; |
| 195 | + |
| 196 | + } |
| 197 | + void OnForegroundChanged() { |
| 198 | + if(Chat == null) |
| 199 | + return; |
| 200 | + var foreground = Foreground.ToColor(); |
| 201 | + Chat.Colors[ChatUIColor.EmptyForeground] = foreground; |
| 202 | + Chat.Colors[ChatUIColor.ScrollViewer] = foreground; |
| 203 | + Chat.Colors[ChatUIColor.InputForeground] = foreground; |
| 204 | + Chat.Colors[ChatUIColor.InputBorder] = Color.FromArgb((int)(255 * 0.25), foreground); |
| 205 | + Chat.Colors[ChatUIColor.InputFocusShadow] = Color.FromArgb((int)(255 * 0.1), foreground); |
| 206 | + Chat.Colors[ChatUIColor.MessageBorder] = Color.FromArgb((int)(255 * 0.25), foreground); |
| 207 | + Chat.Colors[ChatUIColor.UserMessageForeground] = foreground; |
| 208 | + Chat.Colors[ChatUIColor.AssistantMessageForeground] = foreground; |
| 209 | + |
| 210 | + } |
| 211 | + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { |
| 212 | + base.OnPropertyChanged(e); |
| 213 | + if(e.Property == BackgroundProperty) |
| 214 | + OnBackgroundChanged(); |
| 215 | + if(e.Property == ForegroundProperty) |
| 216 | + OnForegroundChanged(); |
| 217 | + } |
| 218 | + void ConfigureBlazorWebView() { |
| 219 | + incapsulationService = new DxChatIncapsulationService(); |
| 220 | + chatWebView.HostPage = StaticResourceIdentifiers.HostPageFilePath; |
| 221 | + chatWebView.Services = GetServiceProvider(incapsulationService); |
| 222 | + chatWebView.RootComponents.Add(new RootComponent() { |
| 223 | + Selector = StaticResourceIdentifiers.AppDivId, |
| 224 | + ComponentType = typeof(ChatUIWrapper), |
| 225 | + Parameters = new Dictionary<string, object>() { |
| 226 | + { nameof(DxAIChat.Initialized), new EventCallback<IAIChat>(null, OnChatInitialized)} |
| 227 | + } |
| 228 | + }); |
| 229 | + blazorChatComponent = chatWebView.RootComponents[0]; |
| 230 | + } |
| 231 | + ServiceProvider GetServiceProvider(DxChatIncapsulationService incapsulationService) { |
| 232 | + var chatClientAIService = AIExtensionsContainerDesktop.Default.GetService<IChatClient>(); |
| 233 | + if(chatClientAIService == null) |
| 234 | + throw new InvalidOperationException("There is no registered service of type Microsoft.Extensions.AI.IChatClient"); |
| 235 | + var aiAssistantFactory = AIExtensionsContainerDesktop.Default.GetService<IAIAssistantFactory>(); |
| 236 | + return ServiceProviderBuildHelper.BuildServiceProvider(incapsulationService, |
| 237 | + s => s.AddWpfBlazorWebView(), |
| 238 | + chatClientAIService, aiAssistantFactory); |
| 239 | + } |
| 240 | + void OnChatInitialized(IAIChat chat) { |
| 241 | + var chatWrapper = chat as IChatUIWrapper; |
| 242 | + if(chatWrapper == null) |
| 243 | + return; |
| 244 | + chatWrapper.UseStreaming = UseStreaming; |
| 245 | + chatWrapper.ResponseContentFormat = ContentFormat; |
| 246 | + chatWrapper.SetEmptyStateText(EmptyStateText); |
| 247 | + chatWrapper.Temperature = Temperature; |
| 248 | + chatWrapper.FrequencyPenalty = FrequencyPenalty; |
| 249 | + chatWrapper.MaxTokens = MaxTokens; |
| 250 | + if(messageSent != null) |
| 251 | + chatWrapper.SetMessageSentCallback(RaiseMessageSent); |
| 252 | + if(ContentFormat == ResponseContentFormat.Markdown) |
| 253 | + chatWrapper.SetMarkdownConvertCallback(RaiseMarkdownConvert); |
| 254 | + OnBackgroundChanged(); |
| 255 | + OnForegroundChanged(); |
| 256 | + OnControlBackgroundChanged(); |
| 257 | + OnItemBackgroundChanged(); |
| 258 | + } |
| 259 | + } |
| 260 | + static class ColorExtensions { |
| 261 | + public static Color ToColor(this System.Windows.Media.Brush brush) { |
| 262 | + return (brush as System.Windows.Media.SolidColorBrush)?.Color.ToColor() ?? Color.Black; |
| 263 | + } |
| 264 | + } |
| 265 | +} |
0 commit comments