网站内链技巧,天猫网站建设分析,域名查询ip,网站建设工作经历前言
1、C#实现本地AI聊天功能 WPFOllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。 2、此程序默认你已经安装好了Ollama。
在运行前需要线安装好Ollama,如何安装请自行搜索
Ollama下载地址#xff1a; https://ollama.org.cn
Ollama模型下载地址#xf…前言
1、C#实现本地AI聊天功能 WPFOllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。 2、此程序默认你已经安装好了Ollama。
在运行前需要线安装好Ollama,如何安装请自行搜索
Ollama下载地址 https://ollama.org.cn
Ollama模型下载地址 https://ollama.org.cn/library
基本运行环境 根据自己使用的AI搜索对应模型基本配置有需要使用GPU运行的模型。 此程序除了安装Ollama外无需安装其他配置。 . 3、相关依赖 OllamaSharpe启用本地Ollama服务 Markdig.wpf : Markdown格式化输出功能。 Microsoft.Xaml.Behaviors.Wpf 解决部分不能进行命令绑定的控件实现命令绑定功能。 运行 项目
项目结构 项目结构包含如下目录: . Commands: 用于命令绑定 Models : 视图对应的模型 Services 一些操作服务 ViewModels视图模型主要的业务处理 Views :视图以及一些视图控件的样式资源 具体如下图: 项目代码
Commands
EventsCommand
using System.Windows.Input;
/// summary
/// 事件命令
/// 有些控件的无法绑定命令但是想要实现命令绑定功能可通过创建该命令实现。
/// 需要引用Microsoft.Xaml.Behaviors.Wpf组合实现。
/// /summary
public class EventsCommandT : ICommand
{private readonly ActionT _execute;private readonly FuncT, bool _canExecute;public EventsCommand(ActionT execute, FuncT, bool canExecute null){_execute execute ?? throw new ArgumentNullException(nameof(execute));_canExecute canExecute;}public bool CanExecute(object parameter){return _canExecute?.Invoke((T)parameter) ?? true;}public void Execute(object parameter){_execute((T)parameter);}public event EventHandler CanExecuteChanged{add { CommandManager.RequerySuggested value; }remove { CommandManager.RequerySuggested - value; }}
}ParameterCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{/// summary/// 参数命令/// 可以带参数的命令/// /summarypublic class ParameterCommand : ICommand{public Actionobject execute;public ParameterCommand(Actionobject execute){this.execute execute;}public event EventHandler? CanExecuteChanged;public bool CanExecute(object? parameter){return CanExecuteChanged ! null;}public void Execute(object? parameter){execute?.Invoke(parameter);}}
}
ParameterlessCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{/// summary/// 无参数命令/// 无参数的命令/// /summarypublic class ParameterlessCommand : ICommand{private Action _execute;public ParameterlessCommand(Action execute){_execute execute;}public event EventHandler? CanExecuteChanged;public bool CanExecute(object? parameter){return CanExecuteChanged ! null;}public void Execute(object? parameter){_execute.Invoke();}}
}Models
ChatRecordModel
namespace OfflineAI.Models
{/// summary/// 聊天记录模型/// /summarypublic class ChatRecordModel{public ChatRecordModel(int id, string dateTime, string name,string fullName, string data){Id id;DateTime dateTime;Name name;FullName fullName;Data data;}/// summary/// ID/// /summarypublic int Id { get; set; }/// summary/// 日期/// /summarypublic string DateTime { get; set; }/// summary/// 名称/// /summarypublic string Name { get; set; }/// summary/// 完整名称/// /summarypublic string FullName { get; set; }/// summary/// 数据/// /summarypublic string Data { get; set; }}
}FileOperationModel
namespace OfflineAI.Models
{public class FileOperationModel{/// summary/// 是否生成目录/// /summarypublic bool IsGenerateDirectory { get; set; }/// summary/// 文件目录/// /summarypublic string Directory { get; set; }/// summary/// 日期目录生成的目录/// /summarypublic string DirectoryDateTime { get; set; }/// summary/// 文件名称全路径/// /summarypublic string FileName { get; set; }/// summary/// 文件名称生成文件全路径/// /summarypublic string FileNameDateTime { get; set; }}
}Services
FileOperation
using OfflineAI.Models;
using System.IO;
namespace OfflineAI.Services
{/// summary/// 文件操作类/// 1、2025-02-24添加创建日期目录方法。输入文件名添加时间目录。/// 2、2025-02-24添加写入数据到文件方法.txt格式/// /summarypublic class FileOperation{private FileOperationModel _fileOperation;#region 构造函数public FileOperation(string fileName){_fileOperation new FileOperationModel();_fileOperation.IsGenerateDirectory true;UpdataFileName(fileName);}#endregion#region 公共方法/// summary/// 更新文件名/// /summarypublic void UpdataFileName(string fileName){if (Path.GetExtension(fileName).ToLower().Equals(txt))_fileOperation.FileName fileName;else_fileOperation.FileName fileName .txt;_fileOperation.Directory Path.GetDirectoryName(fileName);CreateDateTime();_fileOperation.FileNameDateTime ${_fileOperation.DirectoryDateTime}\\{Path.GetFileName(_fileOperation.FileName)};}/// summary/// 写入文本/// /summarypublic void WriteTxt(string data){SaveDataAsTxt(data);}/// summary/// 写入文本指定文件名/// /summarypublic void WriteTxt(string fileName, string data){UpdataFileName(fileName);SaveDataAsTxt(data);}public string ReadTxt(string fileName){// 使用 using 语句确保资源被正确释放using (FileStream fs new FileStream(fileName, FileMode.Open, FileAccess.Read))using (StreamReader sr new StreamReader(fs)){return sr.ReadToEnd();}}/// summary/// 获取指定目录下的所有文件*.txt/// /summarypublic string[] GetFiles(){string[] files Directory.GetFiles(_fileOperation.Directory, *.txt, SearchOption.AllDirectories);return files;}/// summary/// 获取指定目录下的所有文件*.txt/// /summarypublic static string[] GetFiles(string directory){string[] files Directory.GetFiles(directory, *.txt, SearchOption.AllDirectories);return files;}#endregion#region 私有方法/// summary/// 保存数据为Txt类型的文本/// /summaryprivate void SaveDataAsTxt(string data){if (_fileOperation.IsGenerateDirectory){try{string fileName _fileOperation.FileName;if (_fileOperation.IsGenerateDirectory){fileName _fileOperation.FileNameDateTime;}using (FileStream fileStream new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)){using (StreamWriter writer new StreamWriter(fileStream)){writer.Write(data);}}Console.WriteLine(数据已成功写入文件。);}catch (Exception ex){Console.WriteLine(写入文件时发生错误: ex.Message);}}}/// summary/// 创建日期目录/// /summaryprivate void CreateDateTime(){if (_fileOperation.IsGenerateDirectory){string path ${_fileOperation.Directory}\\{DateTime.Now.ToString(yyyy)};Directory.CreateDirectory(${path});path ${path}\\{DateTime.Now.ToString(yyyyMMdd)}\\;Directory.CreateDirectory(${path});_fileOperation.DirectoryDateTime path;}}#endregion}
}ProcessService
using System.ComponentModel;
using System.Diagnostics;
namespace OfflineAI.Services
{public class ProcessService{/// summary/// 执行CMD指令/// /summarypublic static bool ExecuteCommand(string command){// 创建一个新的进程启动信息ProcessStartInfo processStartInfo new ProcessStartInfo{FileName cmd.exe, // 设置要启动的程序为cmd.exeArguments $/C {command}, // 设置要执行的命令UseShellExecute true, // 使用操作系统shell启动进程CreateNoWindow false, //不创建窗体};try{Process process Process.Start(processStartInfo);// 启动进程process.WaitForExit(); // 等待进程退出process.Close(); // 返回是否成功执行return process.ExitCode 0;}catch (Exception ex){Debug.WriteLine($发生错误: {ex.Message});// 其他异常处理return false;}}}
}ShareOllamaObject
using OfflineAI.Services;
using OllamaSharp;
using System.Collections.ObjectModel;namespace OfflineAI.Sevices
{/// summary/// 共享Ollama对象类保持Ollama对象一致才能使用当前对象实现对话/// 作 者吾与谁归/// 时 间2025年02月18日/// 功 能/// 1 2025-02-18使用cmd命令启动Ollama服务,目前使用ollama list();/// 2 2025-02-18初始化模型参数在初始化时启用GPU、连接ollama、初始化模型。/// /summarypublic class ShareOllamaObject{#region 字段|属性|集合#region 字段private bool _connected false; //连接状态private Chat chat; //构建交互式聊天模型对象。private OllamaApiClient _ollama; //OllamaAPI对象private string _selectModel; //选择的模型名称#endregion#region 属性/// summary/// 连接状态/// /summarypublic bool Connected{get { return _connected; }set { _connected value; }}public string SelectModel { get _selectModel; set _selectModel value; }/// summary/// 构建交互式聊天模型对象。/// /summarypublic Chat Chat{get { return chat; }set { chat value; }}/// summary/// OllamaAPI对象/// /summarypublic OllamaApiClient Ollama{get { return _ollama; }set { _ollama value; }}#endregion#region 集合/// summary/// 模型列表/// /summarypublic ObservableCollectionstring ModelList { get; set; }#endregion#endregion#region 构造函数public ShareOllamaObject(){ProcessService.ExecuteCommand(ollama list);Initialize(llama3.2:3b);ProcessService.GetProcessId(ollama);}#endregion#region 其他方法/// summary/// 初始化方法/// /summaryprivate void Initialize( string modelName){try{// 设置默认设备为GPUEnvironment.SetEnvironmentVariable(OLLAMA_DEFAULT_DEVICE, gpu);//连接Ollama并设置初始模型Ollama new OllamaApiClient(new Uri(http://localhost:11434));//获取本地可用的模型列表ModelList (ObservableCollectionstring)GetModelList();//遍历查找是否包含llama3.2:3b模型var tmepModelName ModelList.FirstOrDefault(name name.ToLower().Contains(llama3.2:3b));//设置的模型不为空if (tmepModelName ! null){Ollama.SelectedModel tmepModelName;}//模型列表不为空else if (ModelList.Count 0){_ollama.SelectedModel ModelList[ModelList.Count - 1];}//Ollama服务启用成功SelectModel _ollama.SelectedModel;_connected true;chat new Chat(_ollama);}catch (Exception){_connected false; //Ollama服务启用失败}}/// summary/// 获取模型里列表/// /summarypublic Collectionstring GetModelList(){var models _ollama.ListLocalModelsAsync();var modelList new ObservableCollectionstring();foreach (var model in models.Result){modelList.Add(model.Name);}return modelList;}public void ReCreateChat(){chat new Chat(_ollama);}#endregion}
}ViewModels
MainViewModel
using OfflineAI.Sevices;
using OfflineAI.Commands;
using OfflineAI.Views;
using System.Windows;
using System.Diagnostics;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.IO;
using OfflineAI.Services;
using OfflineAI.Models;
namespace OfflineAI.ViewModels
{/// summary/// 主窗体视图模型/// 作者吾与谁归/// 时间2025年02月17日首次创建时间/// 更新: /// 1、2025-02-17添加折叠栏展开|折叠功能。/// 2、2025-02-17视图切换功能 1系统设置 2) 聊天/// 3、2025-02-18关闭窗体时提示是否关闭释放相关资源。/// 4、2025-02-19添加首页功能和修改新聊天功能。点击新聊天会创建新的会话Chat。/// 5、2025-02-20窗体加载时传递Ollama对象。/// 6、2025-02-24添加了窗体加载时加载聊天记录的功能。/// /summarypublic class MainViewModel : PropertyChangedBase{#region 字段、属性、集合、命令#region 字段private UserControl _currentView; //当前视图private ShareOllamaObject _ollamaService; //共享Ollama服务对象private string _selectedModel; //选择的模型private ObservableCollectionstring _modelListCollection; //模型列表private int _expandedBarWidth 50; //折叠栏宽度private string _directory; //目录private string _fileName; //文件private ObservableCollectionChatRecordModel _chatRecordCollection;public event Actionstring LoadChatRecordEventHandler;#endregion#region 属性/// summary/// 当前显示视图/// /summarypublic UserControl CurrentView { get _currentView;set{if (_currentView ! value){_currentView value;OnPropertyChanged();}}}public ShareOllamaObject OllamaService{get _ollamaService;set{if (_ollamaService ! value){_ollamaService value;OnPropertyChanged();}}}public string SelectedModel { get _selectedModel;set{if (_selectedModel ! value){_selectedModel value;OllamaService.Ollama.SelectedModel value;OllamaService.Chat.Model value;OnPropertyChanged();}}}public int ExpandedBarWidth{get _expandedBarWidth;set{if (_expandedBarWidth ! value){_expandedBarWidth value;OnPropertyChanged();}}}#endregion#region 集合/// summary/// 视图集合保存视图/// /summarypublic ObservableCollectionUserControl ViewCollection { get; set; }public ObservableCollectionstring ModelListCollection{get _modelListCollection;set{if (_modelListCollection ! value){_modelListCollection value;OnPropertyChanged();}}}public ObservableCollectionChatRecordModel ChatRecordCollection{get _chatRecordCollection;set{if (_chatRecordCollection ! value){_chatRecordCollection value;OnPropertyChanged();}}}#endregion#region 命令/// summary/// 展开功能菜单命令/// /summarypublic ICommand ExpandedMenuCommand { get; set; }/// summary/// 折叠功能菜单命令/// /summarypublic ICommand CollapsedMenuCommand { get; set; }/// summary/// 切换视图命令/// /summarypublic ICommand SwitchViewCommand { get; set; }/// summary/// 窗体关闭命令/// /summarypublic ICommand ClosingWindowCommand { get; set; }/// summary/// 窗体加载命令/// /summarypublic ICommand LoadedWindowCommand { get; set; }/// summary/// 聊天记录鼠标按下命令/// /summarypublic ICommand ChatRecordMouseDownCommand { get; set; }#endregion#endregion#region 构造函数public MainViewModel(){Initialize();}/// summary/// 初始化方法/// /summarypublic void Initialize(){//初始化Ollama_ollamaService new ShareOllamaObject();ModelListCollection _ollamaService.ModelList;SelectedModel _ollamaService.SelectModel;//创建命令SwitchViewCommand new ParameterCommand(SwitchViewTrigger);LoadedWindowCommand new EventsCommandobject(LoadedWindowTrigger);CollapsedMenuCommand new EventsCommandobject(CollapsedMenuTrigger);ExpandedMenuCommand new EventsCommandobject(ExpandedMenuTrigger);ClosingWindowCommand new EventsCommandobject(ClosingWindowTrigger);ChatRecordMouseDownCommand new EventsCommandChatRecordModel(ChatRecordMouseDownTrigger);ViewCollection new ObservableCollectionUserControl();//添加视图到集合ViewCollection.Add(new SystemSettingView());ViewCollection.Add(new UserChatView());//默认显示窗体CurrentView ViewCollection[1];//折叠栏折叠状态ExpandedBarWidth 25;//加载聊天记录LoadChatRecord();}#endregion#region 命令方法/// summary/// 聊天记录鼠标按下/// /summaryprivate void ChatRecordMouseDownTrigger(ChatRecordModel obj){Debug.Print(obj.ToString());OnLoadChatRecordCallBack(obj.FullName.ToString());}/// summary/// 触发主视图窗体加载方法/// /summaryprivate void LoadedWindowTrigger(object sender){Debug.Print(sender?.ToString());var userView ViewCollection.FirstOrDefault(obj obj is UserChatView) as UserChatView;userView.UserWindow.Ollama _ollamaService;LoadChatRecordEventHandler userView.UserWindow.LoadChatRecordCallback;}/// summary/// 触发关闭窗体方法/// /summaryprivate void ClosingWindowTrigger(object obj){if (obj is CancelEventArgs cancelEventArgs){if (MessageBox.Show(确定要关闭程序吗, 确认关闭, MessageBoxButton.YesNo) MessageBoxResult.No){cancelEventArgs.Cancel true; // 取消关闭}else{ClearingResources();}}}/// summary/// 视图切换命令触发的方法/// /summaryprivate void SwitchViewTrigger(object obj){Debug.WriteLine(obj.ToString());switch (obj.ToString()){case SystemSettingView:CurrentView ViewCollection[0];break;case UserChatView:CurrentView ViewCollection[1];break;case NewUserChatView:UserChatView newChatView new UserChatView();OllamaService.ReCreateChat();newChatView.UserWindow.Ollama OllamaService;ViewCollection[1] newChatView;CurrentView newChatView;break;}}/// summary/// 折叠菜单触发方法/// /summaryprivate void CollapsedMenuTrigger(object e){ExpandedBarWidth 25;Debug.WriteLine(折叠);}/// summary/// 展开菜单触发方法/// /summaryprivate void ExpandedMenuTrigger(object e){ExpandedBarWidth 250;Debug.WriteLine(展开);}#endregion#region 其他方法/// summary/// 加载聊天记录/// /summaryprivate void LoadChatRecord(){_directory ${Environment.CurrentDirectory}\\Record;string[] files FileOperation.GetFiles(_directory);ObservableCollectionChatRecordModel records new ObservableCollectionChatRecordModel();string name string.Empty;string data string.Empty;foreach (var item in files){name Path.GetFileNameWithoutExtension(item);data File.ReadAllLines(item)[3];if (data.Trim().Length 1 ){records.Add(new ChatRecordModel(records.Count, name, name, item, data.Substring(1)));}}ChatRecordCollection records;}/// summary/// 触发事件加载聊天记录回调/// /summaryprivate void OnLoadChatRecordCallBack(object sender){LoadChatRecordEventHandler.Invoke(sender.ToString());}/// summary/// 释放资源窗体关闭时触发/// /summaryprivate void ClearingResources(){//ProcessService.GetPIDAndCloseByPort(11434);}#endregion}
}PropertyChangedBase
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace OfflineAI.ViewModels
{/// summary/// 属性变更基类/// /summarypublic class PropertyChangedBase : INotifyPropertyChanged{public event PropertyChangedEventHandler? PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}
UserChatViewModel
using Markdig.Wpf;
using OfflineAI.Commands;
using OfflineAI.Services;
using OfflineAI.Sevices;
using System.Diagnostics;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
namespace OfflineAI.ViewModels
{/// summary/// 描述用户聊天视图模型/// 作者吾与谁归/// 时间: 2025年2月19日/// 更新/// 1、 2025-02-19添加AI聊天功能输出问题及结果到UI,并使用Markdown相关的库做简单渲染。/// 2、 2025-02-20优化了构造函数使用无参构造方便在设计器中直接绑定数据上下文感觉。/// 3、 2025-02-20滚轮滑动显示内容,提交问题后滚动显示内容鼠标右键点击内容停止继续滚动回答结束停止滚动。/// 4、 2025-02-24添加聊天记录保存功能。/// 5、 2025-02-24添加聊天记录加载功能通过点击记录列表显示。/// /summarypublic class UserChatViewModel:PropertyChangedBase{#region 字段、属性、集合、命令#region 字段private bool _isAutoScrolling false; //是否自动滚动private string _currentInputText; //当前输入文本private string _messageContent; //消息内容private string _directory; //目录private string _fileName; //文件名private MarkdownViewer _markdownViewer; //MarkdownViewer控件private ScrollViewer _scrollViewer; //ScrollViewer滑动控件private StringBuilder _message new StringBuilder(); //消息字符串拼接private CancellationToken cancellationToken; //异步线程取消标记private FileOperation _fileIO; //文件IOprivate ShareOllamaObject _ollama; //Ollama 对象实例private string _submitButtonName;#endregion#region 属性/// summary/// 提交按钮名称/// /summarypublic string SubmitButtonName{get _submitButtonName;set{if (_submitButtonName ! value){_submitButtonName value;OnPropertyChanged();}}}/// summary/// 消息内容/// /summarypublic string? MessageContent{get _messageContent;set{_messageContent value;OnPropertyChanged();}}/// summary/// 当前输入文本/// /summarypublic string CurrentInputText{get _currentInputText;set{if (_currentInputText ! value){_currentInputText value;OnPropertyChanged();}}}/// summary/// 共享Ollama对象 /// /summarypublic ShareOllamaObject Ollama {get _ollama;set{if (_ollama ! value){_ollama value;OnPropertyChanged();}}}/// summary/// 自动滚动消息/// /summarypublic bool IsAutoScrolling{get _isAutoScrolling;set{if (_isAutoScrolling ! value){_isAutoScrolling value;OnPropertyChanged();}}}#endregion#region 集合#endregion#region 命令/// summary/// 展开功能菜单命令/// /summarypublic ICommand LoadFileCommand { get; set; }/// summary/// 提交命令/// /summarypublic ICommand SubmiQuestionCommand { get; set; }/// summary/// 鼠标滚动/// /summarypublic ICommand MouseWheelCommand { get; set; }/// summary/// 鼠标按下/// /summarypublic ICommand MouseDownCommand { get; set; }/// summary/// Markdown对象命令/// /summarypublic ICommand MarkdownOBJCommand { get; set; }/// summary/// 滑动条加载/// /summarypublic ICommand ScrollLoadedCommand { get; set; }#endregion#endregion#region 构造函数public UserChatViewModel(){Initialize();}#endregion#region 初始化方法/// summary/// 初始化方法/// /summarypublic void Initialize(){//文件加载LoadFileCommand new ParameterCommand(LoadFileTrigger);MouseWheelCommand new EventsCommandMouseWheelEventArgs(MouseWheelTrigger);MouseDownCommand new EventsCommandMouseButtonEventArgs(MouseDownTrigger);MarkdownOBJCommand new EventsCommandobject(MarkdownOBJTrigger);SubmiQuestionCommand new ParameterlessCommand(SubmitQuestionTrigger);ScrollLoadedCommand new EventsCommandRoutedEventArgs(ScrollLoadedTrigger);//SubmitButtonName 提交;//日志记录_directory ${Environment.CurrentDirectory}\\Record\\;_fileName ${_directory}\\{DateTime.Now.ToString(yyyyMMddHHmmss)};_fileIO new FileOperation(${_fileName});//}#endregion#region 命令方法/// summary/// 加载文件/// /summaryprivate void LoadFileTrigger(object obj){OpenFileDialog openFile new OpenFileDialog();openFile.Multiselect true;if (openFile.ShowDialog() DialogResult.OK){string[] files openFile.FileNames;if (files.Count() 1){foreach (var item in files){Debug.WriteLine(item);}}else{Debug.WriteLine(openFile.FileName);}}}/// summary/// 提交: 提交问题到AI并获取返回结果/// /summaryprivate async void SubmitQuestionTrigger(){_ Task.Delay(1);string input CurrentInputText;try{if (!SubmintChecked(input)) return; SubmitButtonName 停止;_message.Clear();_isAutoScrolling true;AppendText($##{Environment.NewLine});AppendText($[{DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss:fff)}]{Environment.NewLine});AppendText($## 【User】{Environment.NewLine});AppendText(${input}{Environment.NewLine});AppendText(${Environment.NewLine});AppendText($## 【AI】{Environment.NewLine});await foreach (var answerToken in Ollama.Chat.SendAsync(input)){AppendText(answerToken);await Task.Delay(20);if (_isAutoScrolling) _scrollViewer.ScrollToEnd();//是否自动滚动}AppendText(${Environment.NewLine}{Environment.NewLine});}catch (Exception ex){AppendText($Error: {ex.Message});AppendText(${Environment.NewLine}{Environment.NewLine});}//回答完成_fileIO.WriteTxt(${_fileName}, _message.ToString());CurrentInputText string.Empty;_isAutoScrolling false;SubmitButtonName 提交;}/// summary/// 鼠标滚动上下滑动/// /summaryprivate void MouseWheelTrigger(MouseWheelEventArgs e){try{// 获取 ScrollViewer 对象if (e.Source is FrameworkElement element element.Parent is ScrollViewer scrollViewer){// 获取当前的垂直偏移量double currentOffset scrollViewer.VerticalOffset;if (e.Delta 0){scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);}else{scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);}// 标记事件已处理防止默认滚动行为e.Handled true;}}catch (Exception ex){Debug.Print(ex.Message);}}/// summary/// Markdown中鼠标按下/// /summaryprivate void MouseDownTrigger(MouseButtonEventArgs args){if (args.LeftButton MouseButtonState.Pressed){IsAutoScrolling false;Debug.Print(Mouse Down...);}}/// summary/// 滚动栏触发/// /summaryprivate void ScrollLoadedTrigger(RoutedEventArgs args){if (args.Source is ScrollViewer scrollView ){_scrollViewer scrollView;Debug.Print(Scroll loaded...);}}/// summary/// Markdown控件对象更新触发/// /summaryprivate void MarkdownOBJTrigger(object obj){if (_markdownViewer ! null) return;if (obj is MarkdownViewer markdownViewer){_markdownViewer markdownViewer;_markdownViewer.Markdown ;}}#endregion#region 其他方法/// summary/// 输出文本/// /summarypublic void AppendText(string newText){Debug.Print(newText);_markdownViewer.Markdown newText;_message.Append(newText);}/// summary/// 提交校验/// /summaryprivate bool SubmintChecked(string input){if (string.IsNullOrEmpty(input)) return false;if (input.Length2) return false;if (input.Equals(停止)) return false;return true;}#endregion#region 回调方法/// summary/// 加载聊天记录回调/// /summarypublic void LoadChatRecordCallback(string path){Debug.Print(path);_scrollViewer.ScrollToTop();_markdownViewer.Markdown _fileIO. ReadTxt(path);}#endregion}
}Views
UserChatView
UserControl x:ClassOfflineAI.Views.UserChatViewxmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:xhttp://schemas.microsoft.com/winfx/2006/xamlxmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:behaviorhttp://schemas.microsoft.com/xaml/behaviorsxmlns:localclr-namespace:OfflineAI.Viewsxmlns:markdig clr-namespace:Markdig.Wpf;assemblyMarkdig.Wpfxmlns:viewmodelsclr-namespace:OfflineAI.ViewModelsmc:Ignorabled d:DesignHeight450 d:DesignWidth800HorizontalAlignmentStretch VerticalAlignmentStretch!--绑定数据上下文--UserControl.DataContextviewmodels:UserChatViewModel x:NameUserWindow//UserControl.DataContextGrid!--命令绑定事件窗体加载时传参数Markdown控件对象。在Grid中创建否则会出现null异常--behavior:Interaction.Triggersbehavior:EventTrigger EventNameLoadedbehavior:InvokeCommandAction Command{Binding MarkdownOBJCommand}CommandParameter{Binding ElementNameMarkdownContent}//behavior:EventTrigger/behavior:Interaction.Triggers!--定义行--Grid.RowDefinitionsRowDefinition Height*/RowDefinition Height300//Grid.RowDefinitions!--行背景色--Border Grid.Row0 Background#FFFFFF/Border Grid.Row1 Background#5E5E5E/Grid!--markdown 滑动条--ScrollViewer Background#AEAEAEx:NameMarkDownScrollViewerbehavior:Interaction.Triggersbehavior:EventTrigger EventNameLoadedbehavior:InvokeCommandAction Command{Binding ScrollLoadedCommand}PassEventArgsToCommandTrue//behavior:EventTrigger/behavior:Interaction.Triggers!--markdown--markdig:MarkdownViewerNameMarkdownContent!--命令绑定事件鼠标滚动显示内容--behavior:Interaction.Triggers!--鼠标滚动命令事件--behavior:EventTrigger EventNamePreviewMouseWheelbehavior:InvokeCommandAction Command{Binding MouseWheelCommand}PassEventArgsToCommandTrue//behavior:EventTrigger!--鼠标点击命令事件--behavior:EventTrigger EventNamePreviewMouseDownbehavior:InvokeCommandAction Command{Binding MouseDownCommand}PassEventArgsToCommandTrue//behavior:EventTrigger/behavior:Interaction.Triggers/markdig:MarkdownViewer/ScrollViewer/Grid!--第三行内容显示回话内容--Grid Grid.Row1 Margin2!--定义三行--Grid.RowDefinitionsRowDefinition Height25/RowDefinition Height*/RowDefinition Height30//Grid.RowDefinitions!--设置Border样式--Border Grid.Row0 Margin150,0,150,0 Background#5E5E5EBorder.BorderThickness2,2,2,0/Border.BorderThicknessBorder.BorderBrushSolidColorBrush Color#000000//Border.BorderBrush/BorderBorder Grid.Row1 Margin150,0,150,0 Background#5E5E5EBorder.BorderThickness2,0,2,0/Border.BorderThicknessBorder.BorderBrushSolidColorBrush Color#000000//Border.BorderBrush/BorderBorder Grid.Row2 Margin150,0,150,0 Background#5E5E5EBorder.BorderThickness2,0,2,2/Border.BorderThicknessBorder.BorderBrushSolidColorBrush Color#000000//Border.BorderBrush/Border!--第2行内容区域--Grid Grid.Row1 Margin150,0,150,0TextBox x:NameInputBox Background#5E5E5EText{Binding CurrentInputText , ModeTwoWay, UpdateSourceTriggerPropertyChanged} Grid.Row1 Margin5 AcceptsReturnTrue VerticalScrollBarVisibilityAuto!--回车发送--TextBox.InputBindingsKeyBinding Command{Binding SubmiQuestionCommand} KeyEnter//TextBox.InputBindings/TextBox/Grid!--第3行内容区域--Grid Grid.Row2 Margin150,0,150,0WrapPanel HorizontalAlignmentRight VerticalAlignmentCenter Margin0,0,5,0Button Width50 Command{Binding LoadFileCommand}Image Width24 Height24Source/Views/Resources/append24-black.png HorizontalAlignmentRight VerticalAlignmentCenter//ButtonButton Width50 Command{Binding SubmiQuestionCommand} Content{Binding SubmitButtonName}/Button/WrapPanel/Grid/Grid/Grid
/UserControlSystemSettingView
UserControl x:ClassOfflineAI.Views.SystemSettingViewxmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:xhttp://schemas.microsoft.com/winfx/2006/xamlxmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:localclr-namespace:OfflineAI.Viewsxmlns:viewModelsclr-namespace:OfflineAI.ViewModelsmc:Ignorabled d:DesignHeight450 d:DesignWidth800HorizontalAlignmentStretch VerticalAlignmentStretchGridStackPanel Background#FFFFFF Margin5TextBox FontSize36 IsReadOnlyTrueHorizontalContentAlignmentCenter VerticalContentAlignmentCenter系统设置/TextBoxCheckBox Width200 Margin5 HorizontalAlignmentLeft IsCheckedTrue是否滚动显示/CheckBoxComboBox Width200 Margin5 HorizontalAlignmentLeft/ComboBox/StackPanel/Grid
/UserControl
Styles \ ButtonStyle.xaml
ResourceDictionary xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml!-- 定义圆角按钮的静态样式 --Style x:KeyRoundCornerButtonStyle TargetTypeButtonSetter PropertyBackgroundSetter.ValueLinearGradientBrush StartPoint0,0 EndPoint1,0GradientStop Color#04D3F2 Offset0.6 /GradientStop Color#FFAB0D Offset2.8 //LinearGradientBrush/Setter.Value/SetterSetter PropertyBorderBrush ValueDarkGray/Setter PropertyBorderThickness Value0/Setter PropertyPadding Value5/Setter PropertyMargin Value10/Setter PropertyWidth Value60/Setter PropertyHeight Value20/!--设置模板样式--Setter PropertyTemplateSetter.ValueControlTemplate TargetTypeButton!--使用 Border 元素作为按钮的主要容器。 roundedRectangle名称方便在触发器中引用。Background绑定背景色到按钮的 Background 属性。BorderBrush绑定边框颜色到按钮的 BorderBrush 属性。BorderThickness绑定边框宽度到按钮的 BorderThickness 属性。CornerRadius设置边框的圆角半径为10使按钮具有圆角效果。ContentPresenter用于显示按钮的内容如文本或图标。--Border x:NameroundedRectangle Background{TemplateBinding Background} BorderBrush{TemplateBinding BorderBrush} BorderThickness{TemplateBinding BorderThickness} CornerRadius10!-- 设置顶部圆角 -- ContentPresenter HorizontalAlignmentCenter VerticalAlignmentCenter//BorderControlTemplate.Triggers!-- 鼠标悬停时 --Trigger PropertyIsMouseOver ValueTrueSetter TargetNameroundedRectangle PropertyBackgroundSetter.ValueLinearGradientBrush StartPoint0,0 EndPoint1,0GradientStop Color#FFB3B3 Offset0.4 /GradientStop Color#D68B8B Offset0.7 //LinearGradientBrush/Setter.Value/Setter/Trigger!-- 按钮被按下时 --Trigger PropertyIsPressed ValueTrueSetter TargetNameroundedRectangle PropertyBackgroundSetter.ValueLinearGradientBrush StartPoint0,0 EndPoint1,0GradientStop Color#D68B8B Offset0.4 /GradientStop Color#A05252 Offset0.7 //LinearGradientBrush/Setter.Value/Setter/Trigger/ControlTemplate.Triggers/ControlTemplate/Setter.Value/Setter/Style!-- 定义带图标的按钮的静态样式 --Style x:KeyIconButtonStyle TargetTypeButtonSetter PropertyBackgroundSetter.ValueLinearGradientBrush StartPoint0,0 EndPoint1,0GradientStop Color#AED3D2 Offset0.3 /!-- 淡色 --GradientStop Color#F0FBFF Offset0.7 /!-- 深色 --/LinearGradientBrush/Setter.Value/SetterSetter PropertyBorderBrush ValueDarkGray/SetterSetter PropertyBorderThickness Value0/SetterSetter PropertyPadding Value5/SetterSetter PropertyMargin Value5 5 5 5/SetterSetter PropertyFontSize Value20/Setter!-- 调整宽度以适应图标和文本 --Setter PropertyHeight Value50/Setter!-- 调整高度以适应图标和文本 --Setter PropertyTemplateSetter.ValueControlTemplate TargetTypeButtonBorder x:NameroundedRectangle Background{TemplateBinding Background} BorderBrush{TemplateBinding BorderBrush} BorderThickness{TemplateBinding BorderThickness} CornerRadius10!-- 使用 StackPanel 来布局图标和文本 --StackPanel OrientationHorizontal HorizontalAlignmentCenter VerticalAlignmentCenterContentPresenter Content{TemplateBinding Content} //StackPanel/BorderControlTemplate.Triggers!-- 鼠标悬停时 --Trigger PropertyIsMouseOver ValueTrueSetter TargetNameroundedRectangle PropertyBackgroundSetter.ValueLinearGradientBrush StartPoint0,0 EndPoint1,0GradientStop Color#FFB3B3 Offset0.4 /GradientStop Color#D68B8B Offset0.7 //LinearGradientBrush/Setter.Value/Setter/Trigger!-- 按钮被按下时 --Trigger PropertyIsPressed ValueTrueSetter TargetNameroundedRectangle PropertyBackgroundSetter.ValueLinearGradientBrush StartPoint0,0 EndPoint1,0GradientStop Color#D68B8B Offset0.4 /GradientStop Color#A05252 Offset0.7 //LinearGradientBrush/Setter.Value/Setter/Trigger/ControlTemplate.Triggers/ControlTemplate/Setter.Value/Setter/Style
/ResourceDictionaryMainWindow
Window x:ClassOfflineAI.MainWindowxmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:xhttp://schemas.microsoft.com/winfx/2006/xamlxmlns:dhttp://schemas.microsoft.com/expression/blend/2008xmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006xmlns:behaviorhttp://schemas.microsoft.com/xaml/behaviorsxmlns:localclr-namespace:OfflineAIxmlns:viewmodelsclr-namespace:OfflineAI.ViewModels WindowStartupLocationCenterScreenmc:IgnorabledTitleChatAI Height800 Width1000Icon/Views/Resources/app-logo128.icoMinHeight600 MinWidth800!--绑定上下文--Window.DataContextviewmodels:MainViewModel/viewmodels:MainViewModel/Window.DataContext!--样式资源--Window.ResourcesResourceDictionary!--资源字典: 添加控件样式--ResourceDictionary.MergedDictionariesResourceDictionary SourceViews/Styles/ButtonStyle.xaml/ResourceDictionary SourceViews/Styles/ComboBoxStyle.xaml//ResourceDictionary.MergedDictionaries/ResourceDictionary/Window.Resources!--事件命令绑定--behavior:Interaction.Triggers!--窗体加载命令绑定--behavior:EventTrigger EventNameLoadedbehavior:InvokeCommandAction Command{Binding LoadedWindowCommand} PassEventArgsToCommandTrue//behavior:EventTrigger!--窗体关闭命令绑定--behavior:EventTrigger EventNameClosingbehavior:InvokeCommandAction Command{Binding ClosingWindowCommand} PassEventArgsToCommandTrue//behavior:EventTrigger/behavior:Interaction.TriggersGrid!-- 定义3列--Grid.ColumnDefinitionsColumnDefinition Widthauto/ColumnDefinition Width*/ColumnDefinition Width10//Grid.ColumnDefinitions!-- 定义2行 --Grid.RowDefinitionsRowDefinition Height*/RowDefinition Height20//Grid.RowDefinitions!-- 折叠栏 Expander --Expander x:NameexpanderBox Grid.Row0 Grid.Column0 Header Background#AABBBB ExpandDirectionLeftIsExpandedFalseFlowDirectionLeftToRight Width{Binding ExpandedBarWidth}!--命令绑定事件--behavior:Interaction.Triggers!--折叠栏展开命令绑定--behavior:EventTrigger EventNameExpandedbehavior:InvokeCommandAction Command{Binding ExpandedMenuCommand} //behavior:EventTrigger!--折叠栏折叠命令绑定--behavior:EventTrigger EventNameCollapsedbehavior:InvokeCommandAction Command{Binding CollapsedMenuCommand} //behavior:EventTrigger/behavior:Interaction.TriggersScrollViewer Background#AEAEAE x:NameRecordScrollViewerListBox ItemsSource{Binding ChatRecordCollection} Margin5ListBox.ItemTemplateDataTemplate!-- 显示消息内容 --TextBlock Text{Binding Data} Margin10,0,0,0behavior:Interaction.Triggers!--鼠标点击命令事件--behavior:EventTrigger EventNamePreviewMouseDownbehavior:InvokeCommandActionCommand{Binding DataContext.ChatRecordMouseDownCommand, RelativeSource{RelativeSource AncestorTypeListBox}}CommandParameter{Binding}PassEventArgsToCommandTrue//behavior:EventTrigger/behavior:Interaction.Triggers/TextBlock/DataTemplate/ListBox.ItemTemplate/ListBox/ScrollViewer/Expander!-- 右侧内容区域 --Border BackgroundLightGray Grid.Row0 Grid.Column1 Padding10/!--主要区域--Grid Grid.Row0 Grid.Column1 Margin3!--定义三行--Grid.RowDefinitionsRowDefinition Height50/RowDefinition Height*/RowDefinition Height350//Grid.RowDefinitions!--设置背景色--Border Grid.Row0 Background#99BBCC/Border Grid.Row1 Background#FFFFFF Grid.RowSpan2/!--第一行内容左对齐内容--WrapPanel VerticalAlignmentCenter!--视图切换首页--Button x:NameBtn_HomePage Width50 Height36 FontSize16Style{StaticResource IconButtonStyle} Command{Binding SwitchViewCommand}CommandParameterUserChatViewStackPanel OrientationHorizontalImage SourceViews/Resources/home24-black.pngMargin5 Width24 Height24HorizontalAlignmentCenter VerticalAlignmentCenter//StackPanel/Button!--视图切换新聊天界面--Button x:NameBtn_Chat Width100 Height36 FontSize16Style{StaticResource IconButtonStyle} Command{Binding SwitchViewCommand}CommandParameterNewUserChatViewStackPanel OrientationHorizontalImage SourceViews/Resources/edit24-black.pngMargin5 Width24 Height24HorizontalAlignmentCenter VerticalAlignmentCenter/TextBlock Text新聊天 VerticalAlignmentCenter//StackPanel/Button!--模型列表--Label Content模型: Margin5 FontSize18 VerticalAlignmentCenter/ComboBox x:NameCbx_ModelList Style{StaticResource RoundComboBoxStyle} ItemsSource{Binding ModelListCollection}SelectedItem{Binding SelectedModel}/ComboBox/WrapPanel!--第一行内容右对齐内容--WrapPanel Margin0,0,0,0 HorizontalAlignmentRight VerticalAlignmentCenter Button Background#99BBCC Command{Binding SwitchViewCommand}CommandParameterSystemSettingViewImage Source/Views/Resources/setting64.png Margin5 Width24 Height24HorizontalAlignmentRight VerticalAlignmentCenter//Button/WrapPanel!--第二行内容显示当前视图--ContentControl Grid.Row1 Margin5,5,5,5Content{Binding CurrentView} HorizontalContentAlignmentStretch VerticalContentAlignmentStretch Grid.RowSpan2//Grid/Grid
/Window总结 以上为项目的全部代码。 实现功能: 1、添加折叠栏展开|折叠功能。 2、视图切换功能 1系统设置 2) 聊天 3、关闭窗体时提示是否关闭释放相关资源。 4、添加首页功能和修改新聊天功能。点击新聊天会创建新的会话Chat。 5、窗体加载时传递Ollama对象。 6、添加了窗体加载时加载聊天记录的功能。 7、添加AI聊天功能输出问题及结果到UI,并使用Markdown相关的库做简单渲染。 8、优化了构造函数使用无参构造方便在设计器中直接绑定数据上下文感觉。 9、 滚轮滑动显示内容,提交问题后滚动显示内容鼠标右键点击内容停止继续滚动回答结束停止滚动。 10、添加聊天记录保存功能。 11、添加聊天记录加载功能通过点击记录列表显示。 待完善 1、使用deepseek r*模型时控件刷新会把 的前面的一部分吞掉使用Debug打印的是完整的问题初步怀疑是异步刷新UI更不上的问题。 2、想使用Markdown的高级渲染功能使用起来目前仅是简单的渲染有空要做出来。 3、聊天记录仅仅是显示功能没有实现承接聊天记录回答问题。 4、参考网页端的功能开发更多功能。 项目下载地址https://github.com/timenodes/OfflineAI