一款基于Avalonia实现的跨平台ChatGpt客户端 ,通过对接ChatGpt官方提供的ChatGpt 3.5模型实现聊天对话
实现创建ChatGpt的项目名称 ,项目类型是Avalonia MVVM ,
添加项目需要使用的Nuget包
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0-preview5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview5" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview5" />
<PackageReference Include="FreeSql.Provider.Sqlite" Version="3.2.690" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
<PackageReference Include="Avalonia.Svg.Skia" Version="11.0.0-preview5" />
</ItemGroup>
ViewLocator.cs代码修改
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using ChatGPT.ViewModels;
namespace ChatGPT;
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
创建MainApp.cs文件
using Microsoft.Extensions.DependencyInjection;
namespace ChatGPT;
public static class MainApp
{
private static IServiceProvider ServiceProvider;
public static ServiceCollection CreateServiceCollection()
{
return new ServiceCollection();
}
public static IServiceProvider Build(this IServiceCollection services)
{
return ServiceProvider = services.BuildServiceProvider();
}
public static T GetService<T>()
{
if (ServiceProvider is null)
{
throw new ArgumentNullException(nameof(ServiceProvider));
}
return ServiceProvider.GetService<T>();
}
public static IEnumerable<T> GetServices<T>()
{
if (ServiceProvider is null)
{
throw new ArgumentNullException(nameof(ServiceProvider));
}
return ServiceProvider.GetServices<T>();
}
public static object? GetService(Type type)
{
if (ServiceProvider is null)
{
throw new ArgumentNullException(nameof(ServiceProvider));
}
return ServiceProvider.GetService(type);
}
}
创建GlobalUsing.cs文件 全局引用
global using System.Reactive;
global using Avalonia;
global using Avalonia.Controls;
global using ChatGPT.ViewModels;
global using Avalonia;
global using Avalonia.Controls.ApplicationLifetimes;
global using Avalonia.Markup.Xaml;
global using ChatGPT.ViewModels;
global using ChatGPT.Views;
global using System;
global using System.Collections.Generic;
global using ReactiveUI;
修改App.axaml代码文件
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ChatGPT"
xmlns:converter="clr-namespace:ChatGPT.Converter"
RequestedThemeVariant="Light"
x:Class="ChatGPT.App">
<Application.Resources>
<converter:HeightConverter x:Key="HeightConverter" />
</Application.Resources>
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme DensityStyle="Compact"/>
</Application.Styles>
</Application>
修改App.axaml.cs代码文件
using Avalonia.Platform;
using Avalonia.Svg.Skia;
using ChatGPT.Options;
using Microsoft.Extensions.DependencyInjection;
namespace ChatGPT;
public partial class App : Application
{
public override void Initialize()
{
GC.KeepAlive(typeof(SvgImageExtension).Assembly);
GC.KeepAlive(typeof(Avalonia.Svg.Skia.Svg).Assembly);
var services = MainApp.CreateServiceCollection();
services.AddHttpClient("chatGpt")
.ConfigureHttpClient(options =>
{
var chatGptOptions = MainApp.GetService<ChatGptOptions>();
if (!string.IsNullOrWhiteSpace(chatGptOptions?.Token))
{
options.DefaultRequestHeaders.Add("Authorization",
"Bearer " + chatGptOptions?.Token.TrimStart().TrimEnd());
}
});
services.AddSingleton<ChatGptOptions>(ChatGptOptions.NewChatGptOptions());
services.AddSingleton(new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.Sqlite,
"Data Source=chatGpt.db;Pooling=true;Min Pool Size=1")
.UseAutoSyncStructure(true) //自动同步实体结构到数据库
.Build());
services.Build();
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
}
var notifyIcon = new TrayIcon();
notifyIcon.Menu ??= new NativeMenu();
notifyIcon.ToolTipText = "ChatGPT";
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
notifyIcon.Icon = new WindowIcon(assets.Open(new Uri("avares://ChatGPT/Assets/chatgpt.ico")));
var exit = new NativeMenuItem()
{
Header = "退出ChatGPT"
};
exit.Click += (sender, args) => Environment.Exit(0);
notifyIcon.Menu.Add(exit);
base.OnFrameworkInitializationCompleted();
}
}
修改MainWindow.axaml文件
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:ChatGPT.ViewModels"
xmlns:pages="clr-namespace:ChatGPT.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ChatGPT.Views.MainWindow"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Height="{Binding Height}"
MinHeight="500"
MinWidth="800"
Width="1060"
Name="Main">
<Design.DataContext>
<viewModels:MainViewModel />
</Design.DataContext>
<StackPanel Name="StackPanel" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<WrapPanel Name="WrapPanel" VerticalAlignment="Stretch" Height="{Binding ElementName=StackPanel, Path=Height}">
<StackPanel MaxWidth="55" Width="55">
<DockPanel Background="#2E2E2E" Height="{Binding Height}">
<StackPanel DockPanel.Dock="Top">
<StackPanel Margin="0,32,0,0"></StackPanel>
<StackPanel Margin="8">
<Image Source="/Assets/avatar.png"></Image>
</StackPanel>
<StackPanel Name="ChatStackPanel" Margin="15">
<Image Source="/Assets/chat-1.png"></Image>
</StackPanel>
</StackPanel>
<StackPanel Margin="5" VerticalAlignment="Bottom" DockPanel.Dock="Bottom">
<StackPanel VerticalAlignment="Bottom" Name="FunctionStackPanel">
<Menu HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<MenuItem HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<MenuItem.Header>
<Image Margin="5" Height="20" Width="20" Source="/Assets/function.png"></Image>
</MenuItem.Header>
<MenuItem Click="Setting_OnClick" Name="Setting" Header="设置" />
</MenuItem>
</Menu>
</StackPanel>
</StackPanel>
</DockPanel>
</StackPanel>
<Border Width="250" MaxWidth="250" BorderBrush="#D3D3D3" BorderThickness="0,0,1,0">
<StackPanel>
<pages:ChatShowView Name="ChatShowView"/>
</StackPanel>
</Border>
<StackPanel>
<StackPanel Height="{Binding Height}" HorizontalAlignment="Center" VerticalAlignment="Center">
<pages:SendChat DataContext="{Binding SendChatViewModel}"></pages:SendChat>
</StackPanel>
</StackPanel>
</WrapPanel>
</StackPanel>
</Window>
修改MainWindow.axaml.cs文件
using Avalonia.Interactivity;
using ChatGPT.Pages;
namespace ChatGPT.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var observer = Observer.Create<Rect>(rect =>
{
if (ViewModel is null) return;
ViewModel.SendChatViewModel.Height = (int)rect.Height;
ViewModel.SendChatViewModel.Width = (int)rect.Width - 305;
ViewModel.Height = (int)rect.Height;
ViewModel.SendChatViewModel.ShowChatPanelHeight =
(int)rect.Height - ViewModel.SendChatViewModel.SendPanelHeight - 60;
});
this.GetObservable(BoundsProperty).Subscribe(observer);
ChatShowView = this.Find<ChatShowView>(nameof(ChatShowView));
ChatShowView.OnClick += view =>
{
ViewModel.SendChatViewModel.ChatShow = view;
};
}
private MainViewModel ViewModel => DataContext as MainViewModel;
private void Setting_OnClick(object? sender, RoutedEventArgs e)
{
var setting = new Setting
{
DataContext = ViewModel.SettingViewModel
};
setting.Show();
}
}
提供部分代码 所有源码都是开源,链接防止最下面
效果图
SendChat.axaml.cs中提供了请求ChatGpt 3.5的实现
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using ChatGPT.Model;
using Notification = Avalonia.Controls.Notifications.Notification;
namespace ChatGPT.Pages;
public partial class SendChat : UserControl
{
private readonly HttpClient http;
private WindowNotificationManager? _manager;
public SendChat()
{
http = MainApp.GetService<IHttpClientFactory>().CreateClient("chatGpt");
InitializeComponent();
DataContextChanged += async (sender, args) =>
{
if (DataContext is not SendChatViewModel model) return;
if (model.ChatShow != null)
{
var freeSql = MainApp.GetService<IFreeSql>();
try
{
var values = await freeSql.Select<ChatMessage>()
.Where(x => x.ChatShowKey == model.ChatShow.Key)
.OrderBy(x => x.CreatedTime)
.ToListAsync();
foreach (var value in values)
{
model.messages.Add(value);
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
else
{
model.ChatShowAction += async () =>
{
var freeSql = MainApp.GetService<IFreeSql>();
var values = await freeSql.Select<ChatMessage>()
.Where(x => x.Key == model.ChatShow.Key)
.OrderBy(x => x.CreatedTime)
.ToListAsync();
foreach (var value in values)
{
model.messages.Add(value);
}
};
}
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var topLevel = TopLevel.GetTopLevel(this);
_manager = new WindowNotificationManager(topLevel) { MaxItems = 3 };
}
private void Close_OnClick(object? sender, RoutedEventArgs e)
{
var window = TopLevel.GetTopLevel(this) as Window;
window.ShowInTaskbar = false;
window.WindowState = WindowState.Minimized;
}
private SendChatViewModel ViewModel => DataContext as SendChatViewModel;
private void Thumb_OnDragDelta(object? sender, VectorEventArgs e)
{
var thumb = (Thumb)sender;
var wrapPanel = (WrapPanel)thumb.Parent;
wrapPanel.Width += e.Vector.X;
wrapPanel.Height += e.Vector.Y;
}
private void SendBorder_OnPointerEntered(object? sender, PointerEventArgs e)
{
}
private async void SendMessage_OnClick(object? sender, RoutedEventArgs e)
{
await SendMessageAsync();
}
private void Minimize_OnClick(object? sender, RoutedEventArgs e)
{
var window = TopLevel.GetTopLevel(this) as Window;
window.WindowState = WindowState.Minimized;
}
private void Maximize_OnClick(object? sender, RoutedEventArgs e)
{
var window = TopLevel.GetTopLevel(this) as Window;
window.WindowState = window.WindowState switch
{
WindowState.Maximized => WindowState.Normal,
WindowState.Normal => WindowState.Maximized,
_ => window.WindowState
};
}
private async void SendTextBox_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
await SendMessageAsync();
}
}
private async Task SendMessageAsync()
{
try
{
if (ViewModel?.ChatShow?.Key == null)
{
_manager?.Show(new Notification("提示", "请先选择一个对话框!", NotificationType.Warning));
return;
}
// 获取当前程序集 assets图片
// var uri = new Uri("avares://ChatGPT/Assets/avatar.png");
// // 通过uri获取Stream
// var bitmap = new Bitmap(AvaloniaLocator.Current.GetService<IAssetLoader>().Open(uri));
var model = new ChatMessage
{
ChatShowKey = ViewModel.ChatShow.Key,
// Avatar = bitmap,
Title = "token",
Content = ViewModel.Message,
CreatedTime = DateTime.Now,
IsChatGPT = false
};
// 添加到消息列表
ViewModel.messages.Add(model);
// 清空输入框
ViewModel.Message = string.Empty;
// 获取消息记录用于AI联系上下文分析 来自Token的代码
var message = ViewModel.messages
.OrderByDescending(x => x.CreatedTime) // 拿到最近的5条消息
.Take(5)
.OrderBy(x => x.CreatedTime) // 按时间排序
.Select(x => x.IsChatGPT
? new
{
role = "assistant",
content = x.Content
}
: new
{
role = "user",
content = x.Content
}
)
.ToList();
// 请求ChatGpt 3.5最新模型 来自Token的代码
var responseMessage = await http.PostAsJsonAsync("https://api.openai.com/v1/chat/completions", new
{
model = "gpt-3.5-turbo",
temperature = 0,
max_tokens = 2560,
user = "token",
messages = message
});
// 获取返回的消息 来自Token的代码
var response = await responseMessage.Content.ReadFromJsonAsync<GetChatGPTDto>();
// 获取当前程序集 assets图片
// uri = new Uri("avares://ChatGPT/Assets/chatgpt.ico");
var chatGptMessage = new ChatMessage
{
ChatShowKey = ViewModel.ChatShow.Key,
// Avatar = new Bitmap(AvaloniaLocator.Current.GetService<IAssetLoader>().Open(uri)),
Title = "ChatGPT",
Content = response.choices[0].message.content,
IsChatGPT = true,
CreatedTime = DateTime.Now
};
// 添加到消息列表 来自Token的代码
ViewModel.messages.Add(chatGptMessage);
var freeSql = MainApp.GetService<IFreeSql>();
await freeSql
.Insert(model)
.ExecuteAffrowsAsync();
await freeSql
.Insert(chatGptMessage)
.ExecuteAffrowsAsync();
}
catch (Exception e)
{
// 异常处理
_manager?.Show(new Notification("提示", "在请求AI服务时出现错误!请联系管理员!", NotificationType.Error));
}
}
}
实现发送前需要将之前的最近五条数据得到跟随当前数据一块发送,为了让其ChatGpt可以联系上下文,这样回复的内容更准确,聊天的数据使用Sqlite本地存储,为了轻量使用ORM采用FreeSql,界面仿制微信的百分之八十的还原度,基本上一直,而且源码完全开源。
来自token的分享
GitHub开源地址: https://github.com/239573049/ChatGpt.Desktop