当前位置: 首页 > article >正文

C#实现本地AI聊天功能(Deepseek R1及其他模型)。

前言

1、C#实现本地AI聊天功能

WPF+OllamaSharpe实现本地聊天功能,可以选择使用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 EventsCommand<T> : ICommand
{
    private readonly Action<T> _execute;
    private readonly Func<T, bool> _canExecute;
    public EventsCommand(Action<T> execute, Func<T, 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>
    /// 参数命令:
    ///     可以带参数的命令:
    /// </summary>
    public class ParameterCommand : ICommand
    {
        public Action<object> execute;
        public ParameterCommand(Action<object> 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>
    /// 无参数命令:
    ///     无参数的命令:
    /// </summary>
    public 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>
    /// 聊天记录模型
    /// </summary>
    public 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
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 日期
        /// </summary>
        public string DateTime { get; set; }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 完整名称
        /// </summary>
        public string FullName { get; set; }
        /// <summary>
        /// 数据
        /// </summary>
        public string Data { get; set; }
    }
}
FileOperationModel
namespace OfflineAI.Models
{
    public class FileOperationModel
    {
        /// <summary>
        /// 是否生成目录
        /// </summary>
        public bool IsGenerateDirectory {  get; set; }
        /// <summary>
        /// 文件目录
        /// </summary>
        public string Directory {  get; set; }
        /// <summary>
        /// 日期目录(生成的目录)
        /// </summary>
        public string DirectoryDateTime { get; set; }
        /// <summary>
        /// 文件名称(全路径)
        /// </summary>
        public string FileName { get; set; }
        /// <summary>
        /// 文件名称(生成文件全路径)
        /// </summary>
        public 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格式)
    /// </summary>
    public class FileOperation
    {
        private FileOperationModel _fileOperation;

        #region 构造函数
        public FileOperation(string fileName)
        {
            _fileOperation = new FileOperationModel();
            _fileOperation.IsGenerateDirectory = true;
            UpdataFileName(fileName);
        }
        #endregion
        #region 公共方法
        /// <summary>
        /// 更新文件名
        /// </summary>
        public 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>
        /// 写入文本
        /// </summary>
        public void WriteTxt(string data)
        {
            SaveDataAsTxt(data);
        }
        /// <summary>
        /// 写入文本,指定文件名
        /// </summary>
        public 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)
        /// </summary>
        public string[] GetFiles()
        {
            string[] files = Directory.GetFiles(_fileOperation.Directory, "*.txt", SearchOption.AllDirectories);
            return files;
        }
        /// <summary>
        /// 获取指定目录下的所有文件(*.txt)
        /// </summary>
        public static string[] GetFiles(string directory)
        {
            string[] files = Directory.GetFiles(directory, "*.txt", SearchOption.AllDirectories);
            return files;
        }
        #endregion

        #region 私有方法
        /// <summary>
        /// 保存数据为Txt类型的文本
        /// </summary>
        private 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>
        /// 创建日期目录
        /// </summary>
        private 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指令
        /// </summary>
        public static bool ExecuteCommand(string command)
        {
            // 创建一个新的进程启动信息
            ProcessStartInfo processStartInfo = new ProcessStartInfo
            {
                FileName = "cmd.exe",           // 设置要启动的程序为cmd.exe
                Arguments = $"/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、初始化模型。
    /// </summary>
    public class ShareOllamaObject
    {
        #region 字段|属性|集合
        #region 字段
        private bool _connected = false;        //连接状态
        private Chat chat;                      //构建交互式聊天模型对象。
        private OllamaApiClient _ollama;        //OllamaAPI对象
        private string _selectModel;        //选择的模型名称
        #endregion
        #region 属性
        /// <summary>
        /// 连接状态
        /// </summary>
        public bool Connected
        {
            get { return _connected; }
            set { _connected = value; }
        }

        public string SelectModel { get => _selectModel; set => _selectModel = value; }
        /// <summary>
        /// 构建交互式聊天模型对象。
        /// </summary>
        public Chat Chat
        {
            get { return chat; }
            set { chat = value; }
        }
        /// <summary>
        /// OllamaAPI对象
        /// </summary>
        public OllamaApiClient Ollama
        {
            get { return _ollama; }
            set { _ollama = value; }
        }
        #endregion
        #region 集合
        /// <summary>
        /// 模型列表
        /// </summary>
        public ObservableCollection<string> ModelList { get; set; }
        #endregion
        #endregion
        #region 构造函数
        public ShareOllamaObject()
        {
            ProcessService.ExecuteCommand("ollama list");
            Initialize("llama3.2:3b");
            ProcessService.GetProcessId("ollama");
        }
        #endregion
        #region 其他方法
        /// <summary>
        /// 初始化方法
        /// </summary>
        private void Initialize( string modelName)
        {
            try
            {
                // 设置默认设备为GPU
                Environment.SetEnvironmentVariable("OLLAMA_DEFAULT_DEVICE", "gpu");
                //连接Ollama,并设置初始模型
                Ollama = new OllamaApiClient(new Uri("http://localhost:11434"));
                //获取本地可用的模型列表
                ModelList = (ObservableCollection<string>)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>
        /// 获取模型里列表
        /// </summary>
        public Collection<string> GetModelList()
        {
            var models = _ollama.ListLocalModelsAsync();
            var modelList = new ObservableCollection<string>();
            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:添加了窗体加载时,加载聊天记录的功能。
    /// </summary>
    public class MainViewModel : PropertyChangedBase
    {
        #region 字段、属性、集合、命令
        #region 字段
        private UserControl _currentView;           //当前视图
        private ShareOllamaObject _ollamaService;   //共享Ollama服务对象
        private string _selectedModel;              //选择的模型
        private ObservableCollection<string> _modelListCollection;  //模型列表
        private int _expandedBarWidth = 50;         //折叠栏宽度
        private string _directory;                  //目录
        private string _fileName;                   //文件
        private ObservableCollection<ChatRecordModel> _chatRecordCollection;
        public event Action<string> LoadChatRecordEventHandler;
        #endregion
        #region 属性
        /// <summary>
        /// 当前显示视图
        /// </summary>
        public 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>
        /// 视图集合,保存视图
        /// </summary>
        public ObservableCollection<UserControl> ViewCollection { get; set; }
        public ObservableCollection<string> ModelListCollection
        {
            get => _modelListCollection;
            set
            {
                if (_modelListCollection != value)
                {
                    _modelListCollection = value;
                    OnPropertyChanged();
                }
            }
        }
        public ObservableCollection<ChatRecordModel> ChatRecordCollection
        {
            get => _chatRecordCollection;
            set
            {
                if (_chatRecordCollection != value)
                {
                    _chatRecordCollection = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion
        #region 命令
        /// <summary>
        /// 展开功能菜单命令
        /// </summary>
        public ICommand ExpandedMenuCommand { get; set; }
        /// <summary>
        /// 折叠功能菜单命令
        /// </summary>
        public ICommand CollapsedMenuCommand { get; set; }
        /// <summary>
        /// 切换视图命令
        /// </summary>
        public ICommand SwitchViewCommand { get; set; }
        /// <summary>
        /// 窗体关闭命令
        /// </summary>
        public ICommand ClosingWindowCommand {  get; set; }
        /// <summary>
        /// 窗体加载命令
        /// </summary>
        public ICommand LoadedWindowCommand { get; set; }
        /// <summary>
        /// 聊天记录鼠标按下命令
        /// </summary>
        public ICommand ChatRecordMouseDownCommand { get; set; }
        #endregion
        #endregion

        #region 构造函数
        public MainViewModel()
        {
            Initialize();
        }
        /// <summary>
        /// 初始化方法
        /// </summary>
        public void Initialize()
        {
            //初始化Ollama
            _ollamaService = new ShareOllamaObject();
            ModelListCollection = _ollamaService.ModelList;
            SelectedModel = _ollamaService.SelectModel;
            //创建命令
            SwitchViewCommand = new ParameterCommand(SwitchViewTrigger);
            LoadedWindowCommand = new EventsCommand<object>(LoadedWindowTrigger);
            CollapsedMenuCommand = new EventsCommand<object>(CollapsedMenuTrigger);
            ExpandedMenuCommand = new EventsCommand<object>(ExpandedMenuTrigger);
            ClosingWindowCommand = new EventsCommand<object>(ClosingWindowTrigger);
            ChatRecordMouseDownCommand = new EventsCommand<ChatRecordModel>(ChatRecordMouseDownTrigger);
            ViewCollection = new ObservableCollection<UserControl>();
            //添加视图到集合
            ViewCollection.Add(new SystemSettingView());
            ViewCollection.Add(new UserChatView());
            //默认显示窗体
            CurrentView = ViewCollection[1];
            //折叠栏折叠状态
            ExpandedBarWidth = 25;
            //加载聊天记录
            LoadChatRecord();
        }
        #endregion
        #region 命令方法
        /// <summary>
        /// 聊天记录鼠标按下
        /// </summary>
        private void ChatRecordMouseDownTrigger(ChatRecordModel obj)
        {
            Debug.Print(obj.ToString());
            OnLoadChatRecordCallBack(obj.FullName.ToString());
        }
        /// <summary>
        /// 触发主视图窗体加载方法
        /// </summary>
        private 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>
        /// 触发关闭窗体方法
        /// </summary>
        private void ClosingWindowTrigger(object obj)
        {
            if (obj is CancelEventArgs cancelEventArgs)
            {
                if (MessageBox.Show("确定要关闭程序吗?", "确认关闭", MessageBoxButton.YesNo) == MessageBoxResult.No)
                {
                    cancelEventArgs.Cancel = true; // 取消关闭
                }
                else
                {
                    ClearingResources();
                }
            }
        }
        /// <summary>
        /// 视图切换命令触发的方法
        /// </summary>
        private 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>
        /// 折叠菜单触发方法
        /// </summary>
        private void CollapsedMenuTrigger(object e)
        {
            ExpandedBarWidth = 25;
            Debug.WriteLine("折叠");
        }
        /// <summary>
        /// 展开菜单触发方法
        /// </summary>
        private void ExpandedMenuTrigger(object e)
        {
            ExpandedBarWidth = 250;
            Debug.WriteLine("展开");
        }
        #endregion
        #region 其他方法
        /// <summary>
        /// 加载聊天记录
        /// </summary>
        private void LoadChatRecord()
        {
            _directory = $"{Environment.CurrentDirectory}\\Record";
            string[] files = FileOperation.GetFiles(_directory);
            ObservableCollection<ChatRecordModel> records = new ObservableCollection<ChatRecordModel>();
            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>
        /// 触发事件:加载聊天记录回调
        /// </summary>
        private void OnLoadChatRecordCallBack(object sender)
        {
            LoadChatRecordEventHandler.Invoke(sender.ToString());
        }
        /// <summary>
        /// 释放资源:窗体关闭时触发
        /// </summary>
        private void ClearingResources()
        {
            //ProcessService.GetPIDAndCloseByPort(11434);
        }
        #endregion
    }
}
PropertyChangedBase
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace OfflineAI.ViewModels
{
    /// <summary>
    /// 属性变更基类
    /// </summary>
    public 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:添加聊天记录加载功能,通过点击记录列表显示。
    /// </summary>
    public 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;              //文件IO
        private ShareOllamaObject _ollama;          //Ollama 对象实例
        private string _submitButtonName;
        #endregion
        #region 属性
        /// <summary>
        /// 提交按钮名称
        /// </summary>
        public string SubmitButtonName
        {
            get => _submitButtonName;
            set
            {
                if (_submitButtonName != value)
                {
                    _submitButtonName = value;
                    OnPropertyChanged();
                }
            }
        }
        /// <summary>
        /// 消息内容
        /// </summary>
        public string? MessageContent
        {
            get => _messageContent;
            set
            {
                _messageContent = value;
                OnPropertyChanged();
            }
        }
        /// <summary>
        /// 当前输入文本
        /// </summary>
        public string CurrentInputText
        {
            get => _currentInputText;
            set
            {
                if (_currentInputText != value)
                {
                    _currentInputText = value;
                    OnPropertyChanged();
                }
            }
        }
        /// <summary>
        /// 共享Ollama对象 
        /// </summary>
        public ShareOllamaObject Ollama 
        {
            get => _ollama;
            set
            {
                if (_ollama != value)
                {
                    _ollama = value;
                    OnPropertyChanged();
                }
            }
        }
        /// <summary>
        /// 自动滚动消息
        /// </summary>
        public bool IsAutoScrolling
        {
            get => _isAutoScrolling;
            set
            {
                if (_isAutoScrolling != value)
                {
                    _isAutoScrolling = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion
        #region 集合
        #endregion
        #region 命令
        /// <summary>
        /// 展开功能菜单命令
        /// </summary>
        public ICommand LoadFileCommand { get; set; }
        /// <summary>
        /// 提交命令
        /// </summary>
        public ICommand SubmiQuestionCommand { get; set; }
        /// <summary>
        /// 鼠标滚动
        /// </summary>
        public ICommand MouseWheelCommand { get; set; }
        /// <summary>
        /// 鼠标按下
        /// </summary>
        public ICommand MouseDownCommand { get; set; }
        /// <summary>
        /// Markdown对象命令
        /// </summary>
        public ICommand MarkdownOBJCommand { get; set; }
        /// <summary>
        /// 滑动条加载
        /// </summary>
        public ICommand ScrollLoadedCommand { get; set; }
        #endregion
        #endregion
        #region 构造函数
        public UserChatViewModel()
        {
            Initialize();
        }
        #endregion
        #region 初始化方法
        /// <summary>
        /// 初始化方法
        /// </summary>
        public void Initialize()
        {
            //文件加载
            LoadFileCommand = new ParameterCommand(LoadFileTrigger);
            MouseWheelCommand = new EventsCommand<MouseWheelEventArgs>(MouseWheelTrigger);
            MouseDownCommand = new EventsCommand<MouseButtonEventArgs>(MouseDownTrigger);
            MarkdownOBJCommand = new EventsCommand<object>(MarkdownOBJTrigger);
            SubmiQuestionCommand = new ParameterlessCommand(SubmitQuestionTrigger);
            ScrollLoadedCommand = new EventsCommand<RoutedEventArgs>(ScrollLoadedTrigger);
            //
            SubmitButtonName = "提交";
            //日志记录
            _directory = $"{Environment.CurrentDirectory}\\Record\\";
            _fileName = $"{_directory}\\{DateTime.Now.ToString("yyyyMMddHHmmss")}";
            _fileIO = new FileOperation($"{_fileName}");
            //
        }
        #endregion
        #region 命令方法
        /// <summary>
        /// 加载文件
        /// </summary>
        private 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并获取返回结果
        /// </summary>
        private 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>
        /// 鼠标滚动上下滑动
        /// </summary>
        private 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中鼠标按下
        /// </summary>
        private void MouseDownTrigger(MouseButtonEventArgs args)
        {
            if (args.LeftButton == MouseButtonState.Pressed)
            {
                IsAutoScrolling = false;
                Debug.Print("Mouse Down...");
            }
        }

        /// <summary>
        /// 滚动栏触发
        /// </summary>
        private void ScrollLoadedTrigger(RoutedEventArgs args)
        {
            if (args.Source is ScrollViewer scrollView )
            {
                _scrollViewer = scrollView;
                Debug.Print("Scroll loaded...");
            }
        }

        /// <summary>
        /// Markdown控件对象更新触发
        /// </summary>
        private void MarkdownOBJTrigger(object obj)
        {
            if (_markdownViewer != null) return;
            if (obj is MarkdownViewer markdownViewer)
            {
                _markdownViewer = markdownViewer;
                _markdownViewer.Markdown = "";
            }
        }
        #endregion

        #region 其他方法

        /// <summary>
        /// 输出文本
        /// </summary>
        public void AppendText(string newText)
        {
            Debug.Print(newText);
            _markdownViewer.Markdown += newText;
            _message.Append(newText);
        }

        /// <summary>
        /// 提交校验
        /// </summary>
        private bool SubmintChecked(string input)
        {
            if (string.IsNullOrEmpty(input)) return false;
            if (input.Length<2) return false;
            if (input.Equals("停止")) return false;
            return true;
        }
        #endregion

        #region 回调方法

        /// <summary>
        ///  加载聊天记录回调
        /// </summary>
        public void LoadChatRecordCallback(string path)
        {
            Debug.Print(path);
            _scrollViewer.ScrollToTop();
            _markdownViewer.Markdown = _fileIO. ReadTxt(path);
        }
        #endregion

    }
}

Views

UserChatView
<UserControl x:Class="OfflineAI.Views.UserChatView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:local="clr-namespace:OfflineAI.Views"
             xmlns:markdig ="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
             xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <!--绑定数据上下文-->
    <UserControl.DataContext>
        <viewmodels:UserChatViewModel  x:Name="UserWindow"/>
    </UserControl.DataContext>
    <Grid>
        <!--命令绑定事件:窗体加载时传参数Markdown控件对象。在Grid中创建,否则会出现null异常-->
        <behavior:Interaction.Triggers>
            <behavior:EventTrigger EventName="Loaded">
                <behavior:InvokeCommandAction 
                  Command="{Binding MarkdownOBJCommand}"
                  CommandParameter="{Binding ElementName=MarkdownContent}"/>
            </behavior:EventTrigger>
        </behavior:Interaction.Triggers>
        <!--定义行-->
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="300"/>
        </Grid.RowDefinitions>
        <!--行背景色-->
        <Border Grid.Row="0" Background="#FFFFFF"/>
        <Border Grid.Row="1" Background="#5E5E5E"/>
        <Grid>
            <!--markdown 滑动条-->
            <ScrollViewer Background="#AEAEAE"
                          x:Name="MarkDownScrollViewer">
                <behavior:Interaction.Triggers>
                    <behavior:EventTrigger EventName="Loaded">
                        <behavior:InvokeCommandAction 
                         Command="{Binding ScrollLoadedCommand}"
                         PassEventArgsToCommand="True"/>
                    </behavior:EventTrigger>
                </behavior:Interaction.Triggers>
                <!--markdown-->
                <markdig:MarkdownViewer
                        Name="MarkdownContent">
                    <!--命令绑定事件:鼠标滚动显示内容-->
                    <behavior:Interaction.Triggers>
                        <!--鼠标滚动命令事件-->
                        <behavior:EventTrigger EventName="PreviewMouseWheel">
                            <behavior:InvokeCommandAction 
                             Command="{Binding MouseWheelCommand}"
                             PassEventArgsToCommand="True"/>
                        </behavior:EventTrigger>
                        <!--鼠标点击命令事件-->
                        <behavior:EventTrigger EventName="PreviewMouseDown">
                            <behavior:InvokeCommandAction 
                             Command="{Binding MouseDownCommand}"
                             PassEventArgsToCommand="True"/>
                        </behavior:EventTrigger>
                    </behavior:Interaction.Triggers>
                </markdig:MarkdownViewer>
            </ScrollViewer>
        </Grid>
        <!--第三行内容:显示回话内容-->
        <Grid Grid.Row="1"  Margin="2">
            <!--定义三行-->
            <Grid.RowDefinitions>
                <RowDefinition Height="25"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="30"/>
            </Grid.RowDefinitions>
            <!--设置Border样式-->
            <Border Grid.Row="0" Margin="150,0,150,0" Background="#5E5E5E">
                <Border.BorderThickness>2,2,2,0</Border.BorderThickness>
                <Border.BorderBrush>
                    <SolidColorBrush Color="#000000"/>
                </Border.BorderBrush>
            </Border>
            
            <Border Grid.Row="1" Margin="150,0,150,0" Background="#5E5E5E">
                <Border.BorderThickness>2,0,2,0</Border.BorderThickness>
                <Border.BorderBrush>
                    <SolidColorBrush Color="#000000"/>
                </Border.BorderBrush>
            </Border>
            
            <Border Grid.Row="2" Margin="150,0,150,0"  Background="#5E5E5E">
                <Border.BorderThickness>2,0,2,2</Border.BorderThickness>
                <Border.BorderBrush>
                    <SolidColorBrush Color="#000000"/>
                </Border.BorderBrush>
            </Border>
            <!--2行内容区域-->
            <Grid Grid.Row="1" Margin="150,0,150,0">
                <TextBox x:Name="InputBox" Background="#5E5E5E"
                     Text="{Binding CurrentInputText , Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                     Grid.Row="1"  Margin="5" AcceptsReturn="True" 
                     VerticalScrollBarVisibility="Auto">
                    <!--回车发送-->
                    <TextBox.InputBindings>
                        <KeyBinding Command="{Binding SubmiQuestionCommand}" Key="Enter"/>
                    </TextBox.InputBindings>
                </TextBox>
            </Grid>
            <!--3行内容区域-->
            <Grid Grid.Row="2" Margin="150,0,150,0">
                <WrapPanel HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0">
                    <Button Width="50" Command="{Binding LoadFileCommand}">
                        <Image Width="24" Height="24"
                            Source="/Views/Resources/append24-black.png" 
                            HorizontalAlignment="Right" VerticalAlignment="Center"/>
                    </Button>
                    <Button Width="50" Command="{Binding SubmiQuestionCommand}" Content="{Binding SubmitButtonName}"></Button>
                </WrapPanel>
            </Grid>
        </Grid>
    </Grid>
</UserControl>
SystemSettingView
<UserControl x:Class="OfflineAI.Views.SystemSettingView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:OfflineAI.Views"
             xmlns:viewModels="clr-namespace:OfflineAI.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <Grid>
        <StackPanel Background="#FFFFFF" Margin="5">
            <TextBox FontSize="36" IsReadOnly="True"
                     HorizontalContentAlignment="Center" VerticalContentAlignment="Center">系统设置</TextBox>
            <CheckBox Width="200" Margin="5" HorizontalAlignment="Left" IsChecked="True">是否滚动显示</CheckBox>
            <ComboBox Width="200" Margin="5" HorizontalAlignment="Left">
            </ComboBox>
        </StackPanel>
    </Grid>
</UserControl>

Styles \ ButtonStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <!-- 定义圆角按钮的静态样式 -->
    <Style x:Key="RoundCornerButtonStyle" TargetType="Button">
        <Setter Property="Background">
            <Setter.Value>
                <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                    <GradientStop Color="#04D3F2" Offset="0.6" />
                    <GradientStop Color="#FFAB0D" Offset="2.8" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="BorderBrush" Value="DarkGray"/>
        <Setter Property="BorderThickness" Value="0"/>
        <Setter Property="Padding" Value="5"/>
        <Setter Property="Margin" Value="10"/>
        <Setter Property="Width" Value="60"/>
        <Setter Property="Height" Value="20"/>
        <!--设置模板样式-->
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <!--使用 Border 元素作为按钮的主要容器。  
                            roundedRectangle:名称,方便在触发器中引用。
                            Background:绑定背景色到按钮的 Background 属性。
                            BorderBrush:绑定边框颜色到按钮的 BorderBrush 属性。
                            BorderThickness:绑定边框宽度到按钮的 BorderThickness 属性。
                            CornerRadius:设置边框的圆角半径为10,使按钮具有圆角效果。
                    
                            ContentPresenter:用于显示按钮的内容(如文本或图标)。
                    -->
                    <Border x:Name="roundedRectangle" Background="{TemplateBinding Background}" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="{TemplateBinding BorderThickness}" 
                            CornerRadius="10">
                        <!-- 设置顶部圆角 --> 
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <!-- 鼠标悬停时 -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="roundedRectangle" Property="Background">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                                        <GradientStop Color="#FFB3B3" Offset="0.4" />
                                        <GradientStop Color="#D68B8B" Offset="0.7" />
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        <!-- 按钮被按下时 -->
                        <Trigger Property="IsPressed" Value="True">
                            <Setter TargetName="roundedRectangle" Property="Background">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                                        <GradientStop Color="#D68B8B" Offset="0.4" />
                                        <GradientStop Color="#A05252" Offset="0.7" />
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                    </ControlTemplate.Triggers>

                </ControlTemplate>
            </Setter.Value>
        </Setter>

    </Style>

    <!-- 定义带图标的按钮的静态样式 -->
    <Style x:Key="IconButtonStyle" TargetType="Button">
        <Setter Property="Background">
            <Setter.Value>
                <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                    <GradientStop Color="#AED3D2" Offset="0.3" />
                    <!-- 淡色 -->
                    <GradientStop Color="#F0FBFF" Offset="0.7" />
                    <!-- 深色 -->
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="BorderBrush" Value="DarkGray"></Setter>
        <Setter Property="BorderThickness" Value="0"></Setter>
        <Setter Property="Padding" Value="5"></Setter>
        <Setter Property="Margin" Value="5 5 5 5"></Setter>
        <Setter Property="FontSize" Value="20"></Setter>
        <!-- 调整宽度以适应图标和文本 -->
        <Setter Property="Height" Value="50"></Setter>
        <!-- 调整高度以适应图标和文本 -->
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border x:Name="roundedRectangle" Background="{TemplateBinding Background}" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="{TemplateBinding BorderThickness}" 
                            CornerRadius="10">
                        <!-- 使用 StackPanel 来布局图标和文本 -->
                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
                            <ContentPresenter Content="{TemplateBinding Content}" />
                        </StackPanel>
                    </Border>
                    <ControlTemplate.Triggers>
                        <!-- 鼠标悬停时 -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="roundedRectangle" Property="Background">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                                        <GradientStop Color="#FFB3B3" Offset="0.4" />
                                        <GradientStop Color="#D68B8B" Offset="0.7" />
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        <!-- 按钮被按下时 -->
                        <Trigger Property="IsPressed" Value="True">
                            <Setter TargetName="roundedRectangle" Property="Background">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                                        <GradientStop Color="#D68B8B" Offset="0.4" />
                                        <GradientStop Color="#A05252" Offset="0.7" />
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

MainWindow

<Window x:Class="OfflineAI.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:behavior="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:OfflineAI"
        xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels" 
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d"
        Title="ChatAI" Height="800" Width="1000"
        Icon="/Views/Resources/app-logo128.ico"
        MinHeight="600" MinWidth="800">
    <!--绑定上下文-->
    <Window.DataContext>
        <viewmodels:MainViewModel>
        </viewmodels:MainViewModel>
    </Window.DataContext>

    <!--样式资源-->
    <Window.Resources>
        <ResourceDictionary>
            <!--资源字典: 添加控件样式-->
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Views/Styles/ButtonStyle.xaml"/>
                <ResourceDictionary Source="Views/Styles/ComboBoxStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <!--事件命令绑定-->
    <behavior:Interaction.Triggers>
        <!--窗体加载命令绑定-->
        <behavior:EventTrigger EventName="Loaded">
            <behavior:InvokeCommandAction Command="{Binding LoadedWindowCommand}" 
                                          PassEventArgsToCommand="True"/>
        </behavior:EventTrigger>
        <!--窗体关闭命令绑定-->
        <behavior:EventTrigger EventName="Closing">
            <behavior:InvokeCommandAction Command="{Binding ClosingWindowCommand}" 
                                       PassEventArgsToCommand="True"/>
        </behavior:EventTrigger>
    </behavior:Interaction.Triggers>
    <Grid>
        <!-- 定义3列:-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="10"/>
        </Grid.ColumnDefinitions>
        <!-- 定义2-->
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>

        <!-- 折叠栏 Expander -->
        <Expander x:Name="expanderBox" Grid.Row="0" Grid.Column="0"  Header="" 
                  Background="#AABBBB" ExpandDirection="Left"
                  IsExpanded="False"
                  FlowDirection="LeftToRight" Width="{Binding ExpandedBarWidth}">
            <!--命令绑定事件-->
            <behavior:Interaction.Triggers>
                <!--折叠栏展开命令绑定-->
                <behavior:EventTrigger EventName="Expanded">
                    <behavior:InvokeCommandAction Command="{Binding ExpandedMenuCommand}" />
                </behavior:EventTrigger>
                <!--折叠栏折叠命令绑定-->
                <behavior:EventTrigger EventName="Collapsed">
                    <behavior:InvokeCommandAction Command="{Binding CollapsedMenuCommand}" />
                </behavior:EventTrigger>
            </behavior:Interaction.Triggers>
            
            <ScrollViewer Background="#AEAEAE" x:Name="RecordScrollViewer">
                <ListBox ItemsSource="{Binding ChatRecordCollection}" Margin="5">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <!-- 显示消息内容 -->
                            <TextBlock Text="{Binding Data}"  Margin="10,0,0,0">
                                <behavior:Interaction.Triggers>
                                    <!--鼠标点击命令事件-->
                                    <behavior:EventTrigger EventName="PreviewMouseDown">
                                        <behavior:InvokeCommandAction
                                         Command="{Binding DataContext.ChatRecordMouseDownCommand, 
                                            RelativeSource={RelativeSource AncestorType=ListBox}}"
                                         CommandParameter="{Binding}"
                                         PassEventArgsToCommand="True"/>
                                    </behavior:EventTrigger>
                                </behavior:Interaction.Triggers>
                            </TextBlock>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </ScrollViewer>
        </Expander>

        <!-- 右侧内容区域 -->
        <Border Background="LightGray" Grid.Row="0" Grid.Column="1" Padding="10"/>

        <!--主要区域-->
        <Grid Grid.Row="0" Grid.Column="1" Margin="3">
            <!--定义三行-->
            <Grid.RowDefinitions>
                <RowDefinition Height="50"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="350"/>
            </Grid.RowDefinitions>
            <!--设置背景色-->
            <Border Grid.Row="0" Background="#99BBCC"/>
            <Border Grid.Row="1" Background="#FFFFFF" Grid.RowSpan="2"/>
            <!--第一行内容:左对齐内容-->
            <WrapPanel VerticalAlignment="Center">
                <!--视图切换:首页-->
                <Button x:Name="Btn_HomePage" Width="50" Height="36" FontSize="16"
                   Style="{StaticResource IconButtonStyle}" 
                   Command="{Binding SwitchViewCommand}"
                   CommandParameter="UserChatView">
                    <StackPanel Orientation="Horizontal">
                        <Image Source="Views/Resources/home24-black.png"
                             Margin="5" Width="24" Height="24"
                             HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </StackPanel>
                </Button>

                <!--视图切换:新聊天界面-->
                <Button x:Name="Btn_Chat" Width="100" Height="36" FontSize="16"
                  Style="{StaticResource IconButtonStyle}" 
                  Command="{Binding SwitchViewCommand}"
                  CommandParameter="NewUserChatView">
                    <StackPanel Orientation="Horizontal">
                        <Image Source="Views/Resources/edit24-black.png"
                                Margin="5" Width="24" Height="24"
                                HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        <TextBlock Text="新聊天" VerticalAlignment="Center"/>
                    </StackPanel>
                </Button>
                <!--模型列表-->
                <Label Content="模型:" Margin="5" FontSize="18" VerticalAlignment="Center"/>
                <ComboBox x:Name="Cbx_ModelList" 
                          Style="{StaticResource RoundComboBoxStyle}" 
                          ItemsSource="{Binding ModelListCollection}"
                          SelectedItem="{Binding SelectedModel}">
                </ComboBox>
            </WrapPanel>
            <!--第一行内容:右对齐内容-->
            <WrapPanel Margin="0,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" >
                <Button Background="#99BBCC" 
                        Command="{Binding SwitchViewCommand}"
                        CommandParameter="SystemSettingView">
                    <Image Source="/Views/Resources/setting64.png" 
                        Margin="5"  Width="24" Height="24"
                        HorizontalAlignment="Right" VerticalAlignment="Center"/>
                </Button>
            </WrapPanel>

            <!--第二行内容:显示当前视图-->
            <ContentControl Grid.Row="1" Margin="5,5,5,5"
                 Content="{Binding CurrentView}" 
                 HorizontalContentAlignment="Stretch" 
                 VerticalContentAlignment="Stretch" Grid.RowSpan="2"/>
        </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


http://www.kler.cn/a/561933.html

相关文章:

  • Android 键盘输入按确认或换行 直接触发提交
  • 用AI写游戏3——python实现坦克大战1
  • 网络原理--TCP的特性
  • 中国旅游行业年度报告2024
  • JAVA【微服务】Spring AI 使用详解
  • 【STL专题】优先级队列priority_queue的使用和模拟实现,巧妙利用仿函数解决优先级
  • 操作系统前置汇编知识学习第九天
  • 将CUBE或3DL LUT转换为PNG图像
  • 项目访问使用 docker bridge 网络模式(端口映射)配置详解
  • Visual Studio Code 2025 安装与高效配置教程
  • Github 2025-02-25 Python开源项目日报 Top10
  • 【Linux】gdb/cgdb调试工具
  • window平台上qtcreator上使用opencv报错
  • uniApp小程序保存canvas图片
  • JavaScript基础(函数及面向对象)
  • 网络安全入门|从防护到溯源:HTTP慢速攻击的深度对抗
  • 警惕将“数据标注”岗位包装为“大数据工程师”充数
  • 电子商务网站租用香港服务器的好处有哪些?
  • Lab14_ Blind SQL injection with time delays
  • 【三维分割】LangSplat: 3D Language Gaussian Splatting(CVPR 2024 highlight)