跨平台`ChatGpt` 客户端

开发 前端
实现发送前需要将之前的最近五条数据得到跟随当前数据一块发送,为了让其ChatGpt可以联系上下文,这样回复的内容更准确,聊天的数据使用Sqlite本地存储,为了轻量使用ORM采用FreeSql,界面仿制微信的百分之八十的还原度,基本上一直,而且源码完全开源。

一款基于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

责任编辑:武晓燕 来源: token的技术分享
相关推荐

2016-11-29 13:03:46

微信客户端跨平台组件

2009-12-25 15:12:01

WPF平台

2009-04-22 18:42:13

Vmware虚拟化英特尔

2021-09-22 15:46:29

虚拟桌面瘦客户端胖客户端

2011-08-17 10:10:59

2010-08-01 16:20:29

Android

2011-07-07 13:21:56

UI设计

2009-06-08 15:18:34

EJB远程客户端JVM

2010-05-31 10:11:32

瘦客户端

2011-03-24 13:00:31

配置nagios客户端

2011-03-02 14:36:24

Filezilla客户端

2010-12-21 11:03:15

获取客户端证书

2011-10-26 13:17:05

2009-03-04 10:27:50

客户端组件桌面虚拟化Xendesktop

2011-03-21 14:53:36

Nagios监控Linux

2011-04-06 14:24:20

Nagios监控Linux

2013-05-09 09:33:59

2011-05-12 11:26:00

客户端虚拟化平台

2010-02-22 09:03:22

零客户端瘦客户端VDI终端

2009-11-17 15:02:27

Oracle客户端
点赞
收藏

51CTO技术栈公众号