WPF创建DeepSeek本地自己的客户端-进阶版

ops/2025/3/4 15:29:11/

本次文章接上次写的“基础版”继续 WPF快速创建DeepSeek本地自己的客户端-基础思路版本

1 开发环境与工具

开发工具:VS 2015
开发环境:.Net 4.0
使用技术:WPF

本章内容:WPF实现一个进阶版的DeepSeek客户端。
效果图如下:
在这里插入图片描述
实现的功能:

1、实时接收DeepSeek回复的数据。
2、用户输入识别和AI回复识别使用不同的头像。
3、能够复制文字。

2 搭建本地DeepSeek环境

我参考的是一下几个教程:
1、DeepSeek本地搭建部署+搭建知识库+智能体详细图文教程
2、【问题记录】DeepSeek本地部署遇到问题
3、公司数据不泄露,DeepSeek R1本地化部署+web端访问+个人知识库搭建与使用,喂饭级实操教程,老旧笔记本竟跑出企业级AI
4、【大语言模型】本地快速部署Ollama运行大语言模型详细流程

3 vs2015 创建WPF项目

Message.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;namespace WpfApplication2
{public class Message : INotifyPropertyChanged{private string _content;public string Content{get { return _content; }set{if (_content != value){_content = value;OnPropertyChanged(nameof(Content));  // 通知UI更新}}}public bool IsAI { get; set; } // 标记消息是否来自AIpublic bool IsUser { get; set; } // 标记消息是否来自用户public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged(string propertyName){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}

MainWindow.xaml

<Window x:Class="WpfApplication2.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"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:local="clr-namespace:WpfApplication2"mc:Ignorable="d"Title="DeepSeek客户端" Height="680" Width="850"><Window.Resources><!-- Boolean to Visibility Converter --><local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /></Window.Resources><Window.DataContext><local:ChatViewModel/></Window.DataContext><Grid><Grid.RowDefinitions><RowDefinition Height="8.5*"/><RowDefinition Height="1.5*"/></Grid.RowDefinitions><!--第一个格子,AI对话格子--><Grid Grid.Row="0" Grid.Column="0" Margin="0,15,0,0"><ListBox Name="listbox" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Messages}" Margin="0,-20,0,-14"><ListBox.ItemTemplate><DataTemplate><StackPanel Orientation="Vertical" HorizontalAlignment="Stretch"><!-- AI消息 --><StackPanel Orientation="Horizontal" Visibility="{Binding IsAI, Converter={StaticResource BooleanToVisibilityConverter}}"><Image Source="/Resources/Deepseek.png"   Width="40" Height="40" Margin="5"  VerticalAlignment="Top" /><!-- 使用TextBox代替TextBlock,并设置为只读 --><TextBox Text="{Binding Content}" FontFamily="Segoe UI" FontSize="16" Padding="5,10" TextWrapping="Wrap" MaxWidth="750" VerticalAlignment="Top" IsReadOnly="True" BorderBrush="Transparent" Background="Transparent" /></StackPanel><!-- 用户消息 --><StackPanel Orientation="Horizontal" Visibility="{Binding IsUser, Converter={StaticResource BooleanToVisibilityConverter}}"><Image Source="/Resources/User.png"  Width="40" Height="40" Margin="5" VerticalAlignment="Top" /><!-- 使用TextBox代替TextBlock,并设置为只读 --><TextBox Text="{Binding Content}" FontFamily="Segoe UI" FontSize="16" Padding="5,10" TextWrapping="Wrap" MaxWidth="750" VerticalAlignment="Top" IsReadOnly="True" BorderBrush="Transparent" Background="Transparent" /></StackPanel></StackPanel></DataTemplate></ListBox.ItemTemplate></ListBox></Grid><!--第二个格子,用户输入框--><Grid Grid.Row="1" Grid.Column="0"><Grid.ColumnDefinitions><ColumnDefinition Width="3*" /><!-- 调整比例为3:1,更符合输入框和按钮的实际需求 --><ColumnDefinition Width="1*"/></Grid.ColumnDefinitions><!-- 输入信息框 --><Grid Grid.Column="0" Margin="0,0,0,0"><!-- 统一化Margin值 --><TextBox x:Name="InputTextBox"MaxWidth="540"Height="50"  VerticalAlignment="Bottom"KeyDown="InputTextBox_KeyDown"Margin="107,0,2.4,19.6"/><!-- 移除内层Margin,使用Grid的Margin控制 --></Grid><!-- 发送按钮区域 --><Grid Grid.Column="1" Margin="0,0,0,0"><!-- 添加右下Margin保持整体平衡 --><!-- 发送按钮 --><Button x:Name="SendButton"Content="Send"Width="70"Height="40"  HorizontalAlignment="Left"VerticalAlignment="Bottom"Background="#147bc6"Foreground="White"Click="SendButton_Click"FontFamily="Arial Black"FontSize="13"Margin="6,0,0,23.6"/><Button x:Name="SendButton1"Content="new"Width="30"Height="30"HorizontalAlignment="Left"VerticalAlignment="Bottom"Background="#FFB6F5C2"Foreground="#FF424234"Click="SendButton_Click1"FontFamily="Cambria"Margin="93,0,0,49"/></Grid></Grid></Grid>
</Window>

MainWindow.xaml.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Net;
using System.Threading;namespace WpfApplication2
{/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>public partial class MainWindow : Window{// 创建一个ChatViewModel对象来保存聊天历史private ChatViewModel _viewModel;// 用于存储对话的历史记录static StringBuilder conversationHistory = new StringBuilder();public MainWindow(){InitializeComponent();_viewModel = new ChatViewModel();DataContext = _viewModel;}/// <summary>/// 输入按钮框/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void InputTextBox_KeyDown(object sender, KeyEventArgs e){// 用户输入string userInput = InputTextBox.Text;if (e.Key == Key.Enter){// 异步方法需在同步上下文中调用(需手动处理)Task.Factory.StartNew(() =>{// 调用同步的AIMain方法获取响应RunAI(userInput);});clearText();}}/// <summary>/// 将最新的消息显示到最上面/// </summary>private void clearText(){// 设置最后一个消息为选中的项listbox.SelectedItem = _viewModel.Messages.LastOrDefault();// 滚动到选中的项(即最后一项)listbox.ScrollIntoView(listbox.SelectedItem);}/// <summary>/// 确认发送按钮/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void SendButton_Click(object sender, RoutedEventArgs e){// 用户输入string userInput = InputTextBox.Text;// 异步方法需在同步上下文中调用(需手动处理)Task.Factory.StartNew(() =>{// 调用同步的AIMain方法获取响应RunAI(userInput);});clearText();}private CancellationTokenSource cancellationTokenSource;private CancellationToken cancellationToken;public void RunAI(string userInput){// 如果输入不正确,不输出if (string.IsNullOrWhiteSpace(userInput))return;// 创建取消源cancellationTokenSource = new CancellationTokenSource();cancellationToken = cancellationTokenSource.Token;// 用户输入添加到历史对话记录conversationHistory.AppendLine($"用户: {userInput}");// 添加用户消息Dispatcher.Invoke((Action)(() =>{// 添加AI消息_viewModel.AddUserMessage(userInput);}));// 用户输入添加到历史对话记录var requestData = new{model = "deepseek-r1:1.5b",prompt = conversationHistory.ToString(),stream = true};string jsonContent = Newtonsoft.Json.JsonConvert.SerializeObject(requestData);byte[] byteArray = Encoding.UTF8.GetBytes(jsonContent);HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://localhost:11434/api/generate");request.Method = "POST";request.ContentType = "application/json";request.ContentLength = byteArray.Length;try{using (Stream dataStream = request.GetRequestStream()){dataStream.Write(byteArray, 0, byteArray.Length);}}catch{MessageBox.Show("请本地配置DeepSeek,或者启动相关服务");return;}try{using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())using (Stream responseStream = response.GetResponseStream())using (StreamReader reader = new StreamReader(responseStream)){string line;string line2 = "";while ((line = reader.ReadLine()) != null){// 检查取消标志if (cancellationToken.IsCancellationRequested){break; // 如果取消请求,退出读取流}if (!string.IsNullOrEmpty(line)){dynamic result = Newtonsoft.Json.JsonConvert.DeserializeObject(line);if (result != null && result.response != null){// 每次读取一行后,立即通过Dispatcher更新UIstring responseText = result.response;// 去除所有多余的换行符(例如将每个换行符替换为空格)responseText = responseText.Replace(Environment.NewLine, " ");string surrt = RegexLine(responseText);line2 += surrt;Dispatcher.Invoke((Action)(() =>{// 添加AI消息_viewModel.AddAIMessage(surrt);}));}}}//添加历史对话conversationHistory.AppendLine($"DeepSeek: {line2}");line2 = "";}}catch (WebException ex){MessageBox.Show("请求异常: " + ex.Message);}Dispatcher.Invoke((Action)(() =>{// 清空输入框InputTextBox.Text = "";}));}/// <summary>/// 处理DeepSeek返回的字符串/// </summary>/// <param name="line2"></param>/// <returns></returns>private string RegexLine(string line2){// 使用正则表达式去掉 <think> 和 </think> 标签line2 = Regex.Replace(line2, @"<\/?think>", "\n");// 去掉开头的换行符line2 = line2.TrimStart('\r', '\n');return line2;}/// <summary>/// 开启新的对话/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void SendButton_Click1(object sender, RoutedEventArgs e){// 取消流接收cancellationTokenSource?.Cancel();// 1清空 _viewModel 中的消息记录_viewModel.Messages.Clear();// 2清空输入框InputTextBox.Text = "";// 3清空历史记录conversationHistory.Clear();}}
}

ChatViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.Text;
using System.ComponentModel;namespace WpfApplication2
{public class ChatViewModel : INotifyPropertyChanged{private ObservableCollection<Message> _messages;public ObservableCollection<Message> Messages{get { return _messages; }set{_messages = value;OnPropertyChanged(nameof(Messages));}}public ChatViewModel(){Messages = new ObservableCollection<Message>();}// 添加用户消息public void AddUserMessage(string userInput){Messages.Add(new Message { Content = userInput, IsUser = true, IsAI = false });}// 添加AI消息public void AddAIMessage(string newText){// 检查是否已有消息,且最后一条消息是AI消息if (Messages.Any() && !Messages.Last().IsUser){Messages.Last().Content += newText;  // 追加流数据到最后一条消息OnPropertyChanged(nameof(Messages));  // 通知UI更新}else{// 如果没有消息或最后一条消息是用户消息,则创建新消息Messages.Add(new Message { Content = newText, IsUser = false, IsAI = true });}}// 实现INotifyPropertyChanged接口public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged(string propertyName){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}

BooleanToVisibilityConverter.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;namespace WpfApplication2
{public class BooleanToVisibilityConverter : IValueConverter{public object Convert(object value, Type targetType, object parameter, CultureInfo culture){return value is bool && (bool)value ? Visibility.Visible : Visibility.Collapsed;}public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){return value;}}
}

5 需要安装System.Net.Http库

Install-package System.Net.Http

6 相关图片如下

Resources/Deepseek.png
在这里插入图片描述
Resources/User.png
在这里插入图片描述

通过以上步骤,我们成功创建了一个进阶版的DeepSeek本地客户端,具备了实时对话、消息区分和文本复制等功能。随着对WPF和DeepSeek的深入了解,您可以进一步扩展功能,比如增加更多的用户交互方式和优化UI设计。希望本文对您在WPF开发和DeepSeek应用方面有所帮助!


http://www.ppmy.cn/ops/163071.html

相关文章

OpenCV计算摄影学(10)将一组不同曝光的图像合并成一张高动态范围(HDR)图像的实现类cv::MergeDebevec

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 resulting HDR 图像被计算为考虑了曝光值和相机响应的各次曝光的加权平均值。 cv::MergeDebevec 是 OpenCV 中用于将一组不同曝光的图像合并成一…

十一、Spring Boot:使用JWT实现用户认证深度解析

Spring Boot JWT&#xff08;JSON Web Token&#xff09;&#xff1a;无状态认证 在现代 Web 开发中&#xff0c;无状态认证是一种重要的安全机制&#xff0c;它允许服务器在不存储会话信息的情况下验证用户身份。JSON Web Token&#xff08;JWT&#xff09;是一种常用的无状态…

RocketMQ定时/延时消息实现机制

RocketMQ 的延迟消息是其核心特性之一&#xff0c;允许消息在指定延迟时间后才被消费者消费。 定时消息生命周期 一、延迟消息的核心机制 RocketMQ&#xff08;5.0之前&#xff09; 不支持任意时间精度的延迟&#xff0c;而是通过预定义的 延迟级别&#xff08;Delay Level&a…

本地部署 DeepSeek-R1大模型详细教程(桌面客户端美观UI)

大家好&#xff01;今天我来分享一篇超级详细的教程&#xff0c;教你如何在本地部署 DeepSeek-R1 大模型&#xff0c;让你的电脑也能成为一个强大的 AI 工作站&#xff01;这篇文章会从零开始&#xff0c;手把手带你完成所有步骤&#xff0c;适合小白操作。废话不多说&#xff…

什么是kube-proxy?

kube-proxy是Kubernetes集群中一个关键的组件&#xff0c;主要负责实现Kubernetes服务&#xff08;Service&#xff09;的网络代理和负载均衡功能。 基本概念 kube-proxy是一个运行在每个Kubernetes节点上的守护进程。它监听KubernetesAPI服务器中关于服务&#xff08;Service…

SEKI —— 基于大型语言模型的自进化与知识启发式神经架构搜索

01、项目概述 我们引入了一种基于新型大型语言模型&#xff08; LLM &#xff09;的神经架构搜索&#xff08; NAS &#xff09;方法&#xff0c;名为 SEKI 。SEKI 受到现代 LLM 中思维链&#xff08; CoT &#xff09;范式的启发&#xff0c;分为两个关键阶段运行&#xff1a…

5分钟看懂Deepseek开源周之六:Deepseek-V3/R1推理系统设计----揭开深度求索模型系统设计和运营成本之谜

前言 众所周知&#xff0c;四大天王一般有五个人。所以开源周五连发有第六天也很正常。贴上了开源周活动的github主贴&#xff0c;大家可以不上推特就能了解详情。 deepseek-ai/open-infra-index: Production-tested AI infrastructure tools for efficient AGI development a…

力扣:1.两数之和(O(n)复杂度)

1. 两数之和 - 力扣&#xff08;LeetCode&#xff09;1. 两数之和 - 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。你可以假设每种输入只会对应一个答案&#xff0c;并且…