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

C#实现本地Deepseek模型及其他模型的对话v1.4

前言

系 统:Window11
开发工具:Visual Studio 2022
相关技术:C# 、WPF .Net 8.0

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

WPF+OllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。

  1. 新增根据聊天记录回复的功能。

  2. 优化了部分ViewModel,将对应Model字段、属性移到Model中,方便后续扩展。

  3. 新增读取外部数据回复问题功能,目前支持txt文件。

  4. 新增添加图片提问题功能,模型需要支持视觉(如:minicpm-v:latest)。

  5. 优化了类结构,创建对应的Model(MainWindowModel),将所有字段、属性移到Model。

  6. 新增聊天记录窗体,修改了窗体加载时,加载聊天记录的功能。将其拆分成一个视图。

  7. 移除了折叠栏功能,更新为Grid区域的显示与隐藏。 将聊天记录列表从主窗体中分离)。

  8. 更新记录文件加载功能,显示提问日期。 新增选择文件类型设置预览图标。

  9. 新增功能,新聊天后第一次提问完成后,保存的记录刷新到记录列表、记录删除功能。

  10. 新增功能,创建新窗体判断显示Ollama服务运行状态。

2、相关依赖

OllamaSharpe:启用本地Ollama服务
Markdig.wpf : Markdown格式化输出功能。
Microsoft.Xaml.Behaviors.Wpf :解决部分不能进行命令绑定的控件实现命令绑定功能。
Newtonsoft.Json:Json数据的序列化和反序列化。

3 界面预览

①界面预览

在这里插入图片描述

②界面预览-聊天记录

在这里插入图片描述

③界面预览-图像分析

在这里插入图片描述

④项目结构

Commands: 用于命令绑定
ExtensionTool:目前没啥大功能…
Services :一些服务操作,如聊天数据处理、图像处理、序列化…
Models : 视图对应的模型,在类中创建一些字段、属性。
Views :视图以及一些视图控件的样式资源
ViewModels:视图模型,主要处理视图和模型的交互、以及一些业务处理
在这里插入图片描述



代码

Commands 目录

命名空间:OfflineAI.Commands

EventsCommand

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

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

public class ParameterlessCommand : ICommand
{
    private Action _execute;
    public ParameterlessCommand(Action execute)
    {
        this._execute = execute;
    }
    public event EventHandler? CanExecuteChanged;
    public bool CanExecute(object? parameter)
    {
        return CanExecuteChanged != null;
    }
    public void Execute(object? parameter)
    {
        _execute.Invoke();
    }
}


RelayCommand

 public class RelayCommand : ICommand
 {
     private readonly Action<object> _execute;
     private readonly Predicate<object> _canExecute;
     public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
     {
         _execute = execute ?? throw new ArgumentNullException(nameof(execute));
         _canExecute = canExecute;
     }
     public bool CanExecute(object parameter)
     {
         return _canExecute == null || _canExecute(parameter);
     }
     public void Execute(object parameter)
     {
         _execute(parameter);
     }
     public event EventHandler CanExecuteChanged
     {
         add { CommandManager.RequerySuggested += value; }
         remove { CommandManager.RequerySuggested -= value; }
     }
 }


ExtensionTool 目录

命名空间 OfflineAI.ExtensionTool

LimitedObservableCollection

/// <summary>
/// 限定大小集合
/// </summary>
public class LimitedObservableCollection<T> : ObservableCollection<T>
{
    private readonly int _maxSize;
    public LimitedObservableCollection(int maxSize)
    {
        _maxSize = maxSize;
        CollectionChanged += OnCollectionChanged;
    }
    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (Count > _maxSize)
        {
            // 移除超出部分的元素
            for (int i = Count - 1; i >= _maxSize; i--)
            {
                RemoveAt(i);
            }
        }
    }
}


Models 目录

命名空间 OfflineAI.Models

PropertyChangedBase

 public class PropertyChangedBase : INotifyPropertyChanged
 {
     public event PropertyChangedEventHandler? PropertyChanged;
     protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
     {
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
     }
 }


ChatDataModel

/// 聊天数据:路径、数据对象、右键菜单命令
public class ChatDataModel: PropertyChangedBase
{
    public ChatDataModel()
    {
        JsonModel = new ChatJsonDataModel();
    }
    public string? Uri { get; set; }
    public ChatJsonDataModel? JsonModel { get; set; }
    public ICommand MenuItemMouseDownCommand { get; set; }
}


ChatJsonDataModel

/// 聊天(Json)数据:角色、内容、图像、返回内容、时间
public class ChatJsonDataModel: PropertyChangedBase
{
    [JsonProperty("role")]
    public string Role { get; set; }
    [JsonProperty("content")]
    public string Content { get; set; }
    [JsonProperty("images[]")]
    public string Image { get; set; }
    [JsonProperty("result")]
    public string Result { get; set; }
    [JsonProperty("date")]
    public string Date { get; set; }
}


ExternalDataPreModel

/// 外部数据预览模型:文件名、索引、图像文件
public class ExternalDataPreModel:PropertyChangedBase
{
	public string FileName { get; set; }
    public ExternalDataType DataType
    {
        get; set;
    }
    private object _index;
    public object Index
    {
        get => _index;
        set
        {
            if (_index != value)
            {
                _index = value;
                OnPropertyChanged();
            }
        }
    }
    
    private string imageSource;
    public string ImageSource
    {
        get => imageSource;
        set
        {
            if (imageSource != value)
            {
                imageSource = value;
                OnPropertyChanged();
            }
        }
    }
}
/// 外部数据类型:文本、图像、其他未知
public enum ExternalDataType
{
    Text,
    Image,
    Unknown
}


/// 文件变更
public class FileChangedModel
{
    [Description("文件名")]
    public string? FileName {  get; set; }
    [Description("操作")]
    public FileChangeType Options { get; set; }
}

/// 文件变更类型,创建,删除,修改
public enum FileChangeType
{
    [Description("文件被创建")]
    Created,
    [Description("文件被删除")]
    Deleted,
    [Description("文件被修改")]
    Modified
}


FileOperationModel

///文件操作:1、是否生成目录 2、目录 3、日期目录 4、文件名 5、日期文件名 6、扩展名 7、文件名格式
public class FileOperationModel : PropertyChangedBase
{
    public bool IsGenerateDirectory {  get; set; }
    public string Directory {  get; set; }
    public string DirectoryDT { get; set; }
    public string FileName { get; set; }
    public string FileNameDT { get; set; }
    public string Extension { get; set; }
    public string FileNameFormat { get; set; }
}


MainWindowModel

///

public class MainWindowModel: PropertyChangedBase
{
    private int _expandedBarWidth = 50;     //折叠栏宽度
    private string _selectedModel;          //选择的模型
    private Visibility _expandedMenuIsHide = Visibility.Hidden;//折叠栏是否隐藏
    private OllamaService _ollamaService;                  		// Ollama服务对象
    private UserControl _currentView;                           // 当前显示视图
    private UserControl _expandedBarView;                       // 折叠栏视图
    private ObservableCollection<string> _modelListCollection;  // 模型列表集合
    /// 视图集合,保存视图
    public ObservableCollection<UserControl> ViewCollection { get; set; }
    /// 折叠栏宽度
    public int ExpandedBarWidth
    {
        get => _expandedBarWidth;
        set
        {
            if (value != _expandedBarWidth)
            {
                _expandedBarWidth = value;
                OnPropertyChanged();
            }
        }
    }
    /// 选择的模型
    public string SelectedModel 
    { 
        get => _selectedModel; 
        set 
        {
            if (value != _selectedModel)
            {
                _selectedModel = value;
                _ollamaService.SelectModel = _selectedModel;
                OnPropertyChanged();
            }
        }
    }
    public Visibility ExpandedMenuIsHide { 
        get => _expandedMenuIsHide; 
        set
        {
            if (value != _expandedMenuIsHide)
            {
                _expandedMenuIsHide = value;
                OnPropertyChanged();
            }
        }
    }
    /// Ollama服务对象
    public OllamaService Ollama
    {
        get => _ollamaService;
        set
        {
            if (_ollamaService != value)
            {
                _ollamaService = value;
                OnPropertyChanged();
            }
        }
    }
    /// 当前显示视图
    public UserControl CurrentView
    {
        get => _currentView;
        set
        {
            if (_currentView != value)
            {
                _currentView = value;
                OnPropertyChanged();
            }
        }
    }
    /// 当前折叠栏视图
    public UserControl ExpandedBarView
    {
        get => _expandedBarView;
        set
        {
            if (_expandedBarView != value)
            {
                _expandedBarView = value;
                OnPropertyChanged();
            }
        }
    }
    /// 模型列表集合
    public ObservableCollection<string> ModelListCollection
    {
        get => _modelListCollection;
        set
        {
            if (_modelListCollection != value)
            {
                _modelListCollection = value;
                OnPropertyChanged();
            }
        }
    }
}


UseChatModel

/// 用户聊天
public class UserChatModel : PropertyChangedBase
{
    private bool _isAutoScrolling = false;      //是否自动滚动
    private bool _isHintVisible = true;         //提示是否可见
    private string _inputText = string.Empty;   //输入文本
    private string _directory = string.Empty;   //目录
    private string _fileName = string.Empty;    //文件
    private string _submitButtonName = "提交";  //提交按钮名称
    private OllamaService _ollama;              //共享Ollama对象
    private bool _isShowRunState = true;        //是否显示运行状态
    private bool? _runState = false;            //运行状态
    /// 是否自动滚动
    public bool IsAutoScrolling {
        get => _isAutoScrolling;
        set
        {
            if (_isAutoScrolling != value)
            {
                _isAutoScrolling = value;
                OnPropertyChanged();
            }
        }
    }
    /// 是否显示运行状态
    public bool IsShowRunState
    {
        get => _isShowRunState;
        set
        {
            if (_isShowRunState != value)
            {
                _isShowRunState = value;
                OnPropertyChanged();
            }
        }
    }
    /// 输入文本
    public string InputText {
        get => _inputText;
        set
        {
            if (_inputText != value)
            {
                _inputText = value;
                OnPropertyChanged();
            }
        }
    }
    /// 运行状态
    public bool? RunState
    {
        get => _runState;
        set
        {
            if (_runState != value)
            {
                _runState = value;
                OnPropertyChanged();
            }
        }
    }
    /// 是否显示提示
    public bool IsHintVisible
    {
        get => _isHintVisible;
        set
        {
            if (_isHintVisible != value)
            {
                _isHintVisible = value;
                OnPropertyChanged();
            }
        }
    }
    /// 目录
    public string Directory {
        get => _directory;
        set
        {
            if (_directory != value)
            {
                _directory = value;
                OnPropertyChanged();
            }
        }
    }
    /// 文件名
    public string FileName {
        get => _fileName;
        set
        {
            if (_fileName != value)
            {
                _fileName = value;
                OnPropertyChanged();
            }
        }
    }
    /// 提交按钮名称
    public string SubmitButtonName {
        get => _submitButtonName;
        set
        {
            if (_submitButtonName != value)
            {
                _submitButtonName = value;
                OnPropertyChanged();
            }
        }
    }
    ///Ollama对象 
    public OllamaService Ollama
    {
        get => _ollama;
        set
        {
            if (_ollama != value)
            {
                _ollama = value;
                RunState = _ollama.Connected;
                OnPropertyChanged();
            }
        }
    }
}


Services 目录

命名空间:Offline.Services

DataService

 public class DataService
 {
     #region 字段、属性
     /// 外部数据模型集合:当前最大10个
     private LimitedObservableCollection<ExternalDataService> _externalDatas 
         = new LimitedObservableCollection<ExternalDataService>(10);
     /// 数据索引
     public int DataID { get; private set; }
     /// 外部数据模型集合个数
     public int ExternalDataCount => ExternalDatas.Count;
     /// 外部数据模型集合
     public LimitedObservableCollection<ExternalDataService> ExternalDatas { get => _externalDatas; }
     /// 文件操作模型
     private FileOperationModel _fileModel = new FileOperationModel();
     /// 文件操作模型
     public FileOperationModel FileModel
     {
         get => _fileModel;
         private set => _fileModel = value;
     }
     #endregion
     #region 构造函数
     public DataService()
     {
         FileModel = new FileOperationModel();
     }
     public DataService(string directory)
     {
         FileModel = new FileOperationModel()
         {
             IsGenerateDirectory = true,         //是否生成目录
             Directory = directory,              //目录
             FileNameFormat = "yyyyMMddHHmmss",  //生成文件名格式
             Extension = "json",                 //拓展名
         };
         FileModel.FileName = $"{directory}{GenFileNameDT(FileModel.FileNameFormat,FileModel.Extension)}";
         GenerateDirectoryDT();
         GenerateFileNameDT(FileModel.FileNameFormat, FileModel.Extension);
     }
     #endregion
     #region 公共方法
     /// 生成日期目录路径
     public void GenerateDirectoryDT()
     {
         //创建日期目录1
         string filePath = $"{FileModel.Directory}\\{DateTime.Now.ToString("yyyy")}";
         Directory.CreateDirectory($"{filePath}");
         //创建日期目录2
         filePath = $"{filePath}\\{DateTime.Now.ToString("yyyyMMdd")}\\";
         Directory.CreateDirectory($"{filePath}");
         //设置日期目录
         FileModel.DirectoryDT = filePath;
     }
     /// 生成日期文件名
     public void GenerateFileNameDT(string timeFormat, string extension)
     {
         FileModel.FileNameDT = $"{FileModel.DirectoryDT}" +
             $"{DateTime.Now.ToString(timeFormat)}.{extension}";
     }
     /// 更新路径
     public void UpdatePath(string filePath)
     {
         string fileName = Path.GetFileName(filePath);
         string extension = Path.GetExtension(filePath);
         string directory = Path.GetDirectoryName(filePath);
         FileModel.Extension = extension;
         FileModel.FileName = fileName;
         FileModel.Directory = directory;
         GenerateDirectoryDT();
         GenerateFileNameDT(FileModel.FileNameFormat, FileModel.Extension);
     }
     /// 生成文件名,根据时间格式和扩展名生成文件名
     public string GenFileNameDT(string timeFormat, string extension)
     {
         return $"{DateTime.Now.ToString(FileModel.FileNameFormat)}.{FileModel.Extension}";
     }
     /// 添加外部数据
     public void AddExternaalData(ExternalDataService dataModel)
     {
         ExternalDatas.Add(dataModel);
         DataID++;
     }
     /// 清空外部数据
     public void ClearExternalData()
     {
         ExternalDatas.Clear();
         DataID = 0;
     }

     #region 静态方法
     /// 获取指定目录下的所有文件(*.json)
     public static string[] GetFiles(string directory)
     {
         string[] files = Directory.GetFiles(directory, $"*.Json", SearchOption.AllDirectories);
         return files;
     }
     /// 写入数据到文件:以追加的方式将数据写入到 JSON 文件,如果文件不存在,则创建一个新文件添加数据。
     public static void AppendDataToJsonFile(ChatDataModel dataModel)
     {
         List<ChatJsonDataModel>? datas;
         /// 如果文件存在,读取现有数据
         if (File.Exists(dataModel.Uri))
         {
             var json = File.ReadAllText(dataModel.Uri);
             datas = JsonSerializer.Deserialize<List<ChatJsonDataModel>>(json);
         }
         /// 如果文件不存在,创建一个空的列表
         else
         {
             datas = new List<ChatJsonDataModel>();
         }
         // 添加新数据
         datas?.Add(dataModel.JsonModel);
         // 将更新后的数据写回文件
         var options = new JsonSerializerOptions { WriteIndented = true };
         var updatedJson = JsonSerializer.Serialize(datas, options);
         File.WriteAllText(dataModel.Uri, updatedJson);
     }
     /// 读取Json文件中的数据
     public static List<ChatJsonDataModel>? ReadDataFormJsonFile(string fileName)
     {
         List<ChatJsonDataModel>? datas = null;
         if (File.Exists(fileName))
         {
             var json = File.ReadAllText(fileName);
             datas = JsonSerializer.Deserialize<List<ChatJsonDataModel>>(json);
             return datas;
         }
         return null;
     }
     /// 检查文件扩展名
     public static bool FileFilter(string fileName)
     {
         string extension = Path.GetExtension(fileName);
         if (IsFileOfType(extension, ".txt")
          || IsFileOfType(extension, ".json")
          || IsFileOfType(extension, ".cs")
          || IsFileOfType(extension, ".xaml")
          || IsFileOfType(extension, ".js")
          || IsFileOfType(extension, ".css")
          || IsFileOfType(extension, ".cpp")
          || IsFileOfType(extension, ".c")
          || IsFileOfType(extension, ".py")
          || IsFileOfType(extension, ".xml")
          || IsFileOfType(extension, ".html")
          || IsFileOfType(extension, ".xml"))
         {
             return true;
         }
         return false;
     }
     /// 检查文件扩展名是否与指定的扩展名匹配(不区分大小写)
     public static bool IsFileOfType(string extension, string type)
     {
         return extension.Equals(type, StringComparison.OrdinalIgnoreCase);
     }
     /// 获取文件是否为图像
     public static ImageFormat GetFileIsImage(string filePath)
     {
         return ImageFormatService.GetImageFormat(filePath);
     }
     /// 获取文件是否为文本类型
     public static bool GetFileIsText(string filePath)
     {
         return FileFilter(filePath);
     }
     /// 获取文件类型
     public static ExternalDataType GetFileType(string filePath)
     {
         if (GetFileIsText(filePath))
         {
             return ExternalDataType.Text;
         }
         else if (GetFileIsImage(filePath) != null)
         {
             return ExternalDataType.Image;
         }
         else
         {
             return ExternalDataType.Unknown;
         }
     }
     #endregion
     #endregion
 }


ExternalDataService

/// 外部数据对象:
public class ExternalDataService
{
    public ExternalDataService(){}
    public ExternalDataService(ExternalDataPreView view){View = view;}
    /// 外部数据预览视图模型
    public ExternalDataPreViewModel? ViewModel { get => View?.ViewModel; }
    /// 外部数据预览视图
    public ExternalDataPreView? View { get; set; }
    /// 外部数据预览模型
    public ExternalDataPreModel? Model { get => ViewModel?.Model; }
    /// 生成外部数据预览对象:1、索引、2、文件路径、3、点击事件回调
    public static ExternalDataService GeneratePreObject(object index, string filePath, RoutedEventHandler clickCallback)
    {
        //创建外部数据预览面板
        FileInfo fileInfo = new FileInfo(filePath);
        ExternalDataPreView preView = new ExternalDataPreView();
        preView.ViewModel.Model.Index = index;
        preView.ViewModel.Model.FileName = filePath;
        preView.ViewModel.Model.DataType = DataService.GetFileType(filePath);
        preView.RemoveButton.Tag = preView.ViewModel.Model.Index;
        preView.Height = 32;
        preView.ViewModel.SetImageSource(DataService.GetFileType(filePath));
        preView.Tbx_Text.Clear();
        preView.Tbx_Text.AppendText($"{Path.GetFileName(filePath)}{Environment.NewLine}");
        preView.Tbx_Text.AppendText($"大小{FormatFileSize(fileInfo.Length)}");
        preView.RemoveButton.Click += clickCallback;
        return new ExternalDataService(preView);
    }
    /// 文件大小格式
    private static string FormatFileSize(long bytes)
    {
        string[] sizes = { "B", "KB", "MB", "GB", "TB" };
        int order = 0;
        while (bytes >= 1024 && order < sizes.Length - 1)
        {
            order++;
            bytes = bytes / 1024;
        }
        return $"{bytes:0.##} {sizes[order]}";
    }
}


ImageFormatService

public class ImageFormatService
{
    #region 判断图像的正确格式
    /// 图像格式工具:获取正确的图像格式,通过图像文件的二进制头部图像格式标识。
    public static ImageFormat GetImageFormat(string filePath)
    {
        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            using (BinaryReader br = new BinaryReader(fs))
            {
                // 读取文件的前几个字节
                byte[] headerBytes = br.ReadBytes(16);
                // 根据文件的前几个字节判断图像的实际格式
                if (IsJpeg(headerBytes)){return ImageFormat.Jpeg;}
                else if (IsPng(headerBytes)){return ImageFormat.Png;}
                else if (IsGif(headerBytes)){return ImageFormat.Gif;}
                else if (IsBmp(headerBytes)){return ImageFormat.Bmp;}
                else{return null;}
            }
        }
    }
    /// JPEG 文件的前两个字节是 0xFF, 0xD8
    private static bool IsJpeg(byte[] headerBytes)
    {
        
        return headerBytes.Length >= 2 && headerBytes[0] == 0xFF && headerBytes[1] == 0xD8;
    }
    /// PNG 文件的前八个字节是固定的签名:137 80 78 71 13 10 26 10
    private static bool IsPng(byte[] headerBytes)
    {
        return headerBytes.Length >= 8 && headerBytes[0] == 137
                && headerBytes[1] == 80 && headerBytes[2] == 78
                && headerBytes[3] == 71 && headerBytes[4] == 13
                && headerBytes[5] == 10 && headerBytes[6] == 26
                && headerBytes[7] == 10;
    }
    /// GIF 文件的前三个字节是 "GIF"
    private static bool IsGif(byte[] headerBytes)
    {
        return headerBytes.Length >= 3 && headerBytes[0] == 71
                && headerBytes[1] == 73 && headerBytes[2] == 70;
    }
    /// BMP 文件的前两个字节是 "BM"
    private static bool IsBmp(byte[] headerBytes)
    {
        return headerBytes.Length >= 2 && headerBytes[0] == 66&& headerBytes[1] == 77;
    }
    #endregion
}


OllamaService

 public class OllamaService
 {
     #region 字段|属性
     #region 字段
     private bool _connected = false;        //连接状态
     private Chat chat;                      //构建交互式聊天模型对象。
     private OllamaApiClient _ollama;        //OllamaAPI对象
     private string _selectModel;            //选择的模型名称
     #endregion
     #region 属性
     /// 连接状态
     public bool Connected
     {
         get { return _connected; }
         set { _connected = value; }
     }
     /// 选择的模型名称
     public string SelectModel { 
         get => _selectModel;
         set
         {
             if(value != _selectModel)
             {
                 _selectModel = value;
                 _ollama.SelectedModel = value;
                 chat.Model = value;
             }
         }
     }
     /// 构建交互式聊天模型对象。
     public Chat Chat
     {
         get { return chat; }
         set { chat = value; }
     }
     /// OllamaAPI对象
     public OllamaApiClient Ollama
     {
         get { return _ollama; }
         set { _ollama = value; }
     }
     /// 模型列表
     public ObservableCollection<string> ModelList { get; set; }
     #endregion
     #endregion
     #region 构造函数
     public OllamaService()
     {
         ProcessService.ExecuteCommand("ollama list");
         Initialize("llama3.2:3b");
     }
     #endregion
     #region 其他方法
     /// 初始化方法
     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服务启用成功
             chat = new Chat(_ollama);
             SelectModel = _ollama.SelectedModel;
             _connected = true;
         }
         catch (Exception)
         {
             _connected = false;     //Ollama服务启用失败
         }
     }
     /// 获取模型里列表
     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 CreateNewChat()
     {
         try
         {
             var version = _ollama.GetVersionAsync();
             if (version?.Result != null)
             {
                 chat = new Chat(_ollama);
                 Connected = true;
             }
             else
             {
                 chat = new Chat(_ollama);
                 Connected = false;
             }
         }
         catch (Exception ex)
         {
             Connected = false;
         }
     }
     #endregion
 }


ProcessService

public class ProcessService
{
    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;
	    }
	}
}


ViewModels目录

命名空间:Offline.ViewModels

ChatRecordListViewModel

 public class ChatRecordListViewModel: PropertyChangedBase
 {
     #region 字段、属性
     #region 字段
     /// 聊天记录集合:
     private ObservableCollection<ChatDataModel> _chatRecordCollection;
     /// 文件侦听
     private FileSystemWatcher FileWatcher = new FileSystemWatcher();
     #endregion
     #region 属性
     /// 聊天记录集合
     public ObservableCollection<ChatDataModel> ChatRecordCollection
     {
         get => _chatRecordCollection;
         set
         {
             if (_chatRecordCollection != value)
             {
                 _chatRecordCollection = value;
                 OnPropertyChanged();
             }
         }
     }
     /// 事件:加载聊天记录
     public event Action<string> LoadChatRecordEventHandler;
     /// 聊天记录鼠标点击事件
     public ICommand ChatRecordMouseDownCommand { get; set; }
     #endregion
     #endregion

     #region 构造函数、初始化方法
     public ChatRecordListViewModel() {Initialize();}
     ~ChatRecordListViewModel() {StopFileWatcher();}
     /// 初始化
     private void Initialize()
     {
         ChatRecordMouseDownCommand = new RelayCommand(OnChatRecordMouseDown);
         LoadChatRecord();
         StartFileWatcher($"{AppDomain.CurrentDomain.BaseDirectory}\\Record");
     }
     /// 菜单项鼠标按下:删除文件
     private void OnMenuItemMouseDown(object obj)
     {
         if(obj is string uri)
         {
             File.Delete(uri);
             Debug.Print($"删除记录:{uri}");
         }
     }
     #endregion

     #region 命令方法
     /// 事件:聊天记录鼠标按下:
     private void OnChatRecordMouseDown(object args)
     {
         if (args is MouseButtonEventArgs mouseDown)
         {
             if (mouseDown.ChangedButton == MouseButton.Left)
             {
                 if (mouseDown.Source is TextBlock textBlock)
                 {
                     Debug.Print(textBlock.Tag.ToString());
                     OnLoadChatRecordCallBack(textBlock.Tag.ToString());
                 }
             }
             if (mouseDown.ChangedButton == MouseButton.Right)
             {
                 if (mouseDown.Source is TextBlock textBlock)
                 {
                     Debug.Print(textBlock.Tag.ToString());
                 }
             }
         }
     }
     #endregion
     #region 其他方法
     /// 加载聊天记录:
     private void LoadChatRecord()
     {
         string directory = $"{AppDomain.CurrentDomain.BaseDirectory}\\Record";
         ObservableCollection<ChatDataModel> records = new ObservableCollection<ChatDataModel>();
         ChatRecordCollection = new ObservableCollection<ChatDataModel>();
         string[] files = DataService.GetFiles(directory);
         foreach (string file in files)
         { 
             List<ChatJsonDataModel>? datas = DataService.ReadDataFormJsonFile(file);
             if (datas != null && datas[0].Content.Trim().Length > 0)
             {
                 ChatDataModel dataModel = new ChatDataModel();
                 dataModel.JsonModel = datas[0];     //Json数据
                 dataModel.Uri = file;               //加载链接           
                 dataModel.MenuItemMouseDownCommand = new RelayCommand(OnMenuItemMouseDown);
                 records.Add(dataModel);
                 Debug.WriteLine(datas[0].Content);
             }
         }
         //排序:按时间排序
         var sortDatas = records.OrderByDescending(e => DateTime.Parse(e.JsonModel.Date)).ToList();
         foreach (ChatDataModel dataModel in sortDatas)
         {
             ChatRecordCollection.Add(dataModel);
             Debug.Print($"{dataModel.JsonModel.Date}");
         }
     }
     /// 开始文件侦听
     private void StartFileWatcher(string directory)
     {
         FileWatcher.Path = directory;
         FileWatcher.Filter = "*.json";
         FileWatcher.IncludeSubdirectories = true;
         FileWatcher.Created += OnCreated;       // 侦听文件创建事件
         FileWatcher.Deleted += OnDeleted;       // 侦听文件删除事件
         FileWatcher.EnableRaisingEvents = true; // 开始侦听
         Debug.Print("开始侦听目录: " + directory);
     }
     /// <summary>
     /// 停止文件侦听
     /// </summary>
     private void StopFileWatcher()
     {
         FileWatcher.EnableRaisingEvents = false; // 停止侦听
         Debug.Print("停止侦听目录: " + FileWatcher.Path);
     }
     #endregion

     #region 事件方法
     /// 触发回调:加载聊天记录:
     private void OnLoadChatRecordCallBack(string args)
     {
         LoadChatRecordEventHandler?.Invoke(args);
     }
     /// 注册回调:1、LoadChatRecordEventHandler事件
     public void RegisterCallBack(Action<string> action)
     {
         LoadChatRecordEventHandler += action;
     }
     /// 取消注册回调 1、LoadChatRecordEventHandler事件
     public void UnRegisterCallBack(Action<string> action)
     {
         LoadChatRecordEventHandler -= action;
     }
     /// 文件创建事件处理程序:
     private void OnCreated(object sender, FileSystemEventArgs e)
     {
         Debug.Print($"文件已创建: {e.FullPath}");
         OnFileChanged(e.FullPath, FileChangeType.Created);
     }
     /// 事件:文件删除事件
     private void OnDeleted(object sender, FileSystemEventArgs e)
     {
         Debug.Print($"文件已删除: {e.FullPath}");
         OnFileChanged(e.FullPath, FileChangeType.Deleted);
     }
     /// 事件:触发文件变更事件。
     private void OnFileChanged(string fileName, FileChangeType options)
     {
         if (options == FileChangeType.Created)
         {
             List<ChatJsonDataModel>? datas = DataService.ReadDataFormJsonFile(fileName);
             ///读取Json文件中的数据
             if (datas != null && datas[0].Content.Trim().Length > 0)
             {
                 ChatDataModel dataModel = new ChatDataModel();
                 dataModel.JsonModel = datas[0];
                 dataModel.Uri = fileName;
                 //使用线程异步删除
                 Application.Current.Dispatcher.BeginInvoke(new Action(() =>
                 {
                     ChatRecordCollection.Insert(0,dataModel);
                 }));
                 Debug.WriteLine(datas[0].Content);
             }
         }
         //执行移除操作
         if (options == FileChangeType.Deleted)
         {
             //使用线程异步删除
             Application.Current.Dispatcher.BeginInvoke(new Action(() =>
             {
                 ChatRecordCollection.Remove(ChatRecordCollection.FirstOrDefault(obj => obj.Uri.Equals(fileName)));
             }));
         }
     }
     #endregion
 }


External

/// 外部数据预览视图模型: 1、 根据文件类型设置预览图标。
public class ExternalDataPreViewModel:PropertyChangedBase
{
    public ExternalDataPreModel _model = new ExternalDataPreModel();
    public ExternalDataPreModel Model 
    { 
        get =>_model;  
        set
        {
            if (_model == value)
            {
                _model = value;
                OnPropertyChanged();
            }
        }
    }
    public ExternalDataPreViewModel()
    {
        Model.ImageSource = "/Views/Resources/text-file-blue-64.png";
    }
    /// 根据文件类型设置图像源
    public void SetImageSource(ExternalDataType fileType)
    {
        switch (fileType)
        {
            case ExternalDataType.Text:
                Model.ImageSource = "/Views/Resources/text-file-blue-64.png";
                break;
            case ExternalDataType.Image:
                Model.ImageSource = "/Views/Resources/image-file-blue-64.png";
                break;
            default:
                Model.ImageSource = "/Views/Resources/unknown-red-64.png";
                break;
        }
    }
}


MainViewModel

/// <summary>
/// 主窗体视图模型:
/// 作者:吾与谁归
/// 时间:2025年02月17日(首次创建时间)
/// 功能: 
/// 版本version: 1.0
///     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:添加窗体加载时,加载聊天记录的功能。
///     7、2025-02-28:修复创建新对话后,无法查看记录的问题。
/// 版本version: 1.1~1.4
///     1、2025-03-01:优化了类结构,创建对应的Model(MainWindowModel),将所有字段、属性移到Model。
///     2、2025-03-01:新增聊天记录窗体,修改了窗体加载时,加载聊天记录的功能。将其移动到了ChatRecordListView,在其视图模型中实现。
///     2、2025-03-10:移除了折叠栏功能,更新为Grid区域的显示与隐藏 
/// </summary>
public class MainViewModel : PropertyChangedBase
{
    #region 字段、属性、命令
    #region 字段
    /// 主窗体模型对象
    private MainWindowModel _mainModel = new MainWindowModel();
    #endregion
    #region 属性
    ///  获取聊天记录视图对象
    private ChatRecordListView? GetChatRecordView
    {
        get => MainModel.ExpandedBarView as ChatRecordListView;
    }
    /// 主窗体模型对象
    public MainWindowModel MainModel 
    { 
        get => _mainModel;
        set
        {
            if (_mainModel != value)
            {
                _mainModel = value;
                OnPropertyChanged();
            }
        }
    }
    #endregion
    #region 命令属性
    /// 折叠功能菜单命令
    public ICommand ExpandedMenuCommand { get; set; }
    /// 切换视图命令
    public ICommand SwitchViewCommand { get; set; }
    /// 窗体关闭命令
    public ICommand ClosingWindowCommand {  get; set; }
    /// 窗体加载命令
    public ICommand LoadedWindowCommand { get; set; }
    #endregion
    #endregion
    #region 构造函数
    public MainViewModel()
    {
        Initialize();
    }
    /// 初始化方法
    public void Initialize()
    {
        //初始化Ollama
        MainModel.Ollama = new OllamaService();
        MainModel.ModelListCollection = MainModel.Ollama.ModelList;
        MainModel.SelectedModel = MainModel.Ollama.SelectModel;
        //创建命令
        SwitchViewCommand = new ParameterCommand(OnSwitchView);
        LoadedWindowCommand = new EventsCommand<object>(OnLoadedWindow);
        ClosingWindowCommand = new EventsCommand<object>(OnClosingWindow);
        ExpandedMenuCommand = new EventsCommand<object>(OnExpandedMenu);
        //初始化视图集合
        MainModel.ViewCollection = new ObservableCollection<UserControl>();
        //添加视图到集合
        MainModel.ViewCollection.Add(new SystemSettingView());
        MainModel.ViewCollection.Add(new UserChatView());
        //默认显示视图
        MainModel.CurrentView = MainModel.ViewCollection[1];
        //设置折叠栏显示视图
        MainModel.ExpandedBarView = new ChatRecordListView();
        //获取聊天视图对象 //注册事件回调
        GetChatRecordView.ViewModel.RegisterCallBack(GetUserControl<UserChatView>().ViewModel.LoadChatRecordCallback);
        //折叠栏折叠状态
        MainModel.ExpandedBarWidth = 0;
    }
    #endregion
    #region 命令方法
    /// 触发主视图窗体加载方法:窗体加载时传递Ollama对象
    private void OnLoadedWindow(object obj)
    {
        Debug.Print(obj?.ToString());
        var userView = GetUserControl<UserChatView>();
        userView.ViewModel.ChatModel.Ollama = MainModel.Ollama;
    }
    /// 触发关闭窗体方法
    private void OnClosingWindow(object obj)
    {
        if (obj is CancelEventArgs cancelEventArgs)
        {
            if (MessageBox.Show("确定要关闭程序吗?", "确认关闭", MessageBoxButton.YesNo) == MessageBoxResult.No)
            {cancelEventArgs.Cancel = true; // 取消关闭}
            else{ClearResources();}
        }
    }
    /// 视图切换命令触发的方法
    private void OnSwitchView(object obj)
    {
        Debug.WriteLine(obj.ToString());
        switch (obj.ToString())
        {
            case "SystemSettingView":
                MainModel.CurrentView = MainModel.ViewCollection[0];
                break;
            case "UserChatView":
                MainModel.CurrentView = MainModel.ViewCollection[1];
                break;
            case "NewUserChatView": //新建聊天窗体
                NewChat();
                break;
        }
    }
    /// 折叠菜单功能
    private void OnExpandedMenu(object obj)
    {
        if (MainModel.ExpandedMenuIsHide == Visibility.Visible)
        {
            MainModel.ExpandedMenuIsHide = Visibility.Hidden;
            MainModel.ExpandedBarWidth = 0;
        }
        else
        {
            MainModel.ExpandedMenuIsHide = Visibility.Visible;
            MainModel.ExpandedBarWidth = 250;
        }
    }
    #endregion
    #region 其他方法
    /// 获取用户控件视图:获取集合中的视图对象
    public T? GetUserControl<T>() where T : UserControl
    {
        return MainModel.ViewCollection.FirstOrDefault(obj => obj is T) as T;
    }
    /// 释放资源:窗体关闭时触发
    private void ClearResources(){}
    /// 新建聊天窗体
    private void NewChat()
    {
        var view = GetUserControl<UserChatView>();
        UserChatView newChatView = new UserChatView();
        //取消注册的回调
       GetChatRecordView.ViewModel.UnRegisterCallBack(view.ViewModel.LoadChatRecordCallback);
        //给当前对象注册回调GetChatRecordView.ViewModel.RegisterCallBack(newChatView.ViewModel.LoadChatRecordCallback);
        //创建新的Chat对象并初始化数据
        MainModel.Ollama.CreateNewChat();
        newChatView.ViewModel.ChatModel.Ollama = MainModel.Ollama;
        MainModel.ViewCollection[1] = null;
        MainModel.ViewCollection[1] = newChatView;
        MainModel.CurrentView = newChatView;
    }
    #endregion
}


UserChatViewModel

 /// <summary>
 /// 描述:用户聊天视图模型:
 /// 作者:吾与谁归
 /// 时间: 2025年2月19日
 /// 功能:
 /// 版本version: 1.0
 ///    1、 2025-02-19:添加了AI聊天功能,输出问题及结果到UI,并使用Markdown相关的库做简单渲染。
 ///    2、 2025-02-20:优化了构造函数,使用无参构造,方便在设计器中直接绑定数据上下文(感觉)。
 ///    3、 2025-02-20:添加了滑动查看内容(自动滚动,鼠标滚动)。
 ///    4、 2025-02-24:添加了聊天记录保存功能。
 ///    5、 2025-02-24:添加了聊天记录加载功能,通过点击记录列表显示。
 /// 版本version: 1.1
 ///    1、 2025-03-01:添加了根据聊天记录回复的功能。
 ///    2、 2025-03-01:添加了UserChatViewModel对应Model,将字段、属性移到Model中,方便后续扩展。
 ///    3、 2025-03-05:新增读取外部数据回复问题功能,目前支持txt文件。
 ///    4、 2025-03-05:新增添加图片提问题功能,模型需要支持视觉(如:minicpm-v:latest)。
 /// </summary>
 public class UserChatViewModel:PropertyChangedBase
 {
     #region 字段、属性、命令
     #region 字段
     private MarkdownViewer _markdownViewer;                     //MarkdownViewer控件
     private ScrollViewer scrollViewer;                          //ScrollViewer滑动控件
     private WrapPanel wrapPanel;                                //水平排序容器
     private CancellationTokenSource? _cts_ChatThread;           //聊天异步线程:取消标记
     private CancellationTokenSource? _cts_MessageQueue;         //消息异步线程:取消标记
     private ConcurrentQueue<string> _messageQueue;              //消息异步线程:消息队列
     //自定义
     private DataService _userDataService;                       //聊天数据服务
     private UserChatModel _chatModel = new UserChatModel();     //用户聊天模型
     #endregion
     #region 属性
     /// 聊天数据服务
     public DataService UserDataService 
     { 
         get => _userDataService; 
         private set => _userDataService = value; 
     }
     /// 用户聊天模型
     public UserChatModel ChatModel
     { 
         get => _chatModel;
         set
         {
             if (_chatModel != value)
             {
                 _chatModel = value;
                 OnPropertyChanged();
             }
         }
     }
     #endregion
     #region 命令
     /// 展开功能菜单命令
     public ICommand SelecteAddFileCommand { get; set; }
     /// 提交命令
     public ICommand SubmitQuestionCommand { get; set; }
     /// 鼠标滚动
     public ICommand MouseWheelCommand { get; set; }
     /// 鼠标按下
     public ICommand MouseDownCommand { get; set; }
     /// Markdown对象命令
     public ICommand MarkdownOBJCommand { get; set; }
     /// 滑动条加载
     public ICommand ScrollLoadedCommand { get; set; }
     /// 外部数据面板加载
     public ICommand ExternalDataPanelLoadedCommand { get; set; }
     #endregion
     #endregion
     #region 构造函数
     public UserChatViewModel()
     {
         Initialize();
     }
     #endregion
     #region 初始化方法
     /// 初始化方法
     public void Initialize()
     {
         //初始化命令
         SelecteAddFileCommand = new ParameterCommand(OnSelecteAddFile);
         MouseWheelCommand = new EventsCommand<MouseWheelEventArgs>(OnMouseWheel);
         MouseDownCommand = new EventsCommand<MouseButtonEventArgs>(OnMouseDown);
         MarkdownOBJCommand = new EventsCommand<object>(OnMarkdownOBJ);
         SubmitQuestionCommand = new ParameterlessCommand(OnSubmitQuestion);
         ScrollLoadedCommand = new EventsCommand<RoutedEventArgs>(OnScrollLoaded);
         ExternalDataPanelLoadedCommand = new EventsCommand<RoutedEventArgs>(OnExternalDataPanelLoaded);
         //按钮名称
         ChatModel.SubmitButtonName = "提交";
         //聊天记录
         ChatModel.Directory = $"{AppDomain.CurrentDomain.BaseDirectory}\\Record\\";
         UserDataService = new DataService($"{ChatModel.Directory}");
         ChatModel.FileName = UserDataService.FileModel.FileNameDT;
     }
     ///  水平排列容器加载
     private void OnExternalDataPanelLoaded(RoutedEventArgs args)
     {
         if (args.Source is WrapPanel wrapP)
         {
             wrapPanel = wrapP;
             Debug.Print("wrapPanel loaded...");
         }
     }
     #endregion

     #region 命令方法
     /// 加载文件:选择添加文件,最多添加10个文件
     private void OnSelecteAddFile(object obj)
     {
         OpenFileDialog openFile = new OpenFileDialog();
         openFile.Filter = "(*.txt;*,png;*.jpg;*.jpeg;*.bmp)|*.txt;*.png;*.jpg;*.jpeg;*.bmp|(*.png)|*.png|(*.*)|*.*";
         openFile.Multiselect = true;
         if (openFile.ShowDialog() == true)
         {
             string[] files = openFile.FileNames;
             //多个文件时创建外部数据
             if (files.Count() > 1)
             {
                 wrapPanel.Children.Clear();
                 foreach (var file in files)
                 {
                     Debug.Print(file);
                     if (UserDataService.ExternalDataCount < 10)
                     {
                         ExternalDataService dataObj = ExternalDataService.GeneratePreObject(UserDataService.DataID, file, BtnRemoveControl_Click);
                         UserDataService.AddExternaalData(dataObj);
                         wrapPanel.Children.Add(dataObj.View);
                     }
                 }
             }
             //单个文件时创建外部数据
             else
             {
                 Debug.Print(openFile.FileName);
                 if (UserDataService.ExternalDataCount < 10)
                 {
                     ExternalDataService dataObj = ExternalDataService.GeneratePreObject(UserDataService.DataID, openFile.FileName, BtnRemoveControl_Click);
                     UserDataService.AddExternaalData(dataObj);
                     wrapPanel.Children.Add(dataObj.View);
                 }
             }
         }
     }
     /// 提交:  提交问题到AI并获取返回结果
     private async void OnSubmitQuestion()
     {
         _ = Task.Delay(1);
         string input = ChatModel.InputText;
         ChatDataModel chatData = new ChatDataModel();
         chatData.JsonModel.Role = "User";         
         chatData.JsonModel.Content = input;
         chatData.JsonModel.Date = DateTime.Now.ToString();
         string appendText = string.Empty;
         string tempText = string.Empty;
         try
         {
             if (!SubmintChecked(input))
             {
                 //直接回答完成:设置输入框为空,不自动滚动,按钮名称,按钮使能
                 ChatModel.IsAutoScrolling = false;
                 OnStopCurrentChat();
                 ChatModel.SubmitButtonName = "提交";
                 return;
             }
             ChatModel.SubmitButtonName = "停止";
             ChatModel.IsAutoScrolling = true;
             AppendText($"##{Environment.NewLine}");
             AppendText($"### [{chatData.JsonModel.Date}]{Environment.NewLine}");
             AppendText($"# 【User】{Environment.NewLine}");
             AppendText($"**{chatData.JsonModel.Content}**{Environment.NewLine}");
             AppendText($"---{Environment.NewLine}");
             AppendText($"{Environment.NewLine}");
             scrollViewer.ScrollToEnd();
             IEnumerable<string> imageBase64 = null;
             int index = 1;
             //加载文本|图像文件数据
             foreach (var data in UserDataService.ExternalDatas)
             {
                 //加载图像数据
                 if (data.Model.DataType == ExternalDataType.Image)
                 {
                     OllamaSharp.Models.Chat.Message externalMessage = new OllamaSharp.Models.Chat.Message();
                     imageBase64 = ConvertImageToBase64IEnumerable(data.Model.FileName);
                     externalMessage.Role = ChatRole.User;
                     externalMessage.Content = $"这是第{index++}张图像,如果用户问到,那么可能是这张!";
                     externalMessage.Images = imageBase64.ToArray();
                     ChatModel.Ollama.Chat.Messages.Add(externalMessage);
                     string ImageUri = "![Local Image](file:///" + data.Model.FileName.Replace("\\", "/") + ")";
                     appendText += $"{ImageUri}{Environment.NewLine}{Environment.NewLine}";
                     AppendText($"{ImageUri}{Environment.NewLine}{Environment.NewLine}");
                 }
                 //加载文本数据
                 if (data.Model.DataType == ExternalDataType.Text)
                 {
                     OllamaSharp.Models.Chat.Message externalMessage = new OllamaSharp.Models.Chat.Message();
                     externalMessage.Content = $"$\"文件名{{Path.GetFileName(data.FileName)}},以下是这一份文件内容:如果用户有问到文件,可能是这些内容:\\n{File.ReadAllText(data.Model.FileName)}";
                     externalMessage.Role = ChatRole.User;
                     ChatModel.Ollama.Chat.Messages.Add(externalMessage);
                 }
             }
             AppendText($"## 【AI】{Environment.NewLine}");
             //异步获取AI回答
             _cts_ChatThread = new CancellationTokenSource();
             _cts_MessageQueue = new CancellationTokenSource();
             _messageQueue = new ConcurrentQueue<string>();
             await foreach (var answerToken in ChatModel.Ollama.Chat.SendAsync(input, imageBase64, _cts_ChatThread.Token))
             {
                 if (answerToken.Equals("\n") || answerToken.Equals("\r\n"))
                 {
                     Debug.Print(answerToken);
                 }
                 appendText += answerToken;
                 AppendText(answerToken);
                 await Task.Delay(20);
                 if (ChatModel.IsAutoScrolling) scrollViewer.ScrollToEnd();//是否自动滚动
             }
             AppendText($"{Environment.NewLine}{Environment.NewLine}");
             chatData.JsonModel.Result = appendText;
             //创建聊天数据模型,保存记录
             chatData.Uri = UserDataService.FileModel.FileNameDT;
             DataService.AppendDataToJsonFile(chatData);
             //提交回答完后清空外部数据
             wrapPanel.Children.Clear(); 
             UserDataService.ClearExternalData();
         }
         catch (Exception ex)
         {
             AppendText($"Error: {ex.Message}");
             AppendText($"{Environment.NewLine}{Environment.NewLine}");
         }
         //回答完成:设置输入框为空,不自动滚动,按钮名称
         ChatModel.InputText = string.Empty;
         ChatModel.IsAutoScrolling = false;
         ChatModel.SubmitButtonName = "提交";
     }
     /// 停止当前聊天
     private void OnStopCurrentChat()
     {
         _cts_ChatThread?.CancelAsync();
         _cts_MessageQueue?.CancelAsync();
         AppendText($"{Environment.NewLine}");
         Debug.Print("-----------取消提问------------");
         Thread.Sleep(1);
     }
     /// 鼠标滚动上下滑动
     private void OnMouseWheel(MouseWheelEventArgs e)
     {
         try
         {
             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);
         }
     }
     /// Markdown中鼠标按下
     private void OnMouseDown(MouseButtonEventArgs args)
     {
         if (args.LeftButton == MouseButtonState.Pressed)
         {
             ChatModel.IsAutoScrolling = false;
             Debug.Print("Mouse Down...");
         }
     }
     /// 滚动栏触发
     private void OnScrollLoaded(RoutedEventArgs args)
     {
         if (args.Source is ScrollViewer scrollView)
         {
             scrollViewer = scrollView;
             Debug.Print("Scroll loaded...");
         }
     }
     /// Markdown控件对象更新触发
     private void OnMarkdownOBJ(object obj)
     {
         if (_markdownViewer != null) return;
         if (obj is MarkdownViewer markdownViewer)
         {
             _markdownViewer = markdownViewer;
             _markdownViewer.Markdown = string.Empty;
         }
     }
     #endregion
     #region 其他方法
     /// 输出文本
     public void AppendText(string newText)
     {
         Debug.Print(newText);
         ChatModel.IsShowRunState = false;
         _markdownViewer.Markdown += newText;
     }
     /// 提交校验
     private bool SubmintChecked(string input)
     {
         if (string.IsNullOrEmpty(input)) return false;
         if (input.Trim().Length<2) return false;
         if (ChatModel.SubmitButtonName.Equals("停止")) return false;
         return true;
     }
     /// 获取Markdown格式文本
     public void GetMarkdownFormat(string text)
     {
         // 解析Markdown
         var pipeline = new MarkdownPipelineBuilder().Build();
         var document = Markdig. Markdown.Parse(text, pipeline);
         // 遍历语法树并输出结果
         StringBuilder output = new StringBuilder();
         TraverseMarkdown(document, output);
         // 显示结果
         ChatJsonDataModel chatJsonDataModel = new ChatJsonDataModel();
         chatJsonDataModel.Result = output.ToString();
     }
     /// 遍历语法树:输出代码块字符串
     private void TraverseMarkdown(MarkdownObject markdownObject, StringBuilder output)
     {
         //代码块
         if (markdownObject is FencedCodeBlock codeBlock)
         {
             output.AppendLine(codeBlock.Lines.ToString());
         }
         else if (markdownObject is ContainerBlock containerBlock)
         {
             foreach (var child in containerBlock)
             {
                 TraverseMarkdown(child, output);
             }
         }
         else if (markdownObject is LeafBlock leafBlock)
         {
             output.AppendLine(leafBlock.Lines.ToString());
         }
     }
     /// 转换输出Markdown文本
     private void ConvertMarkdownOut(string text)
     {
         // .UseCustomRenderers()   // 使用自定义渲染器
         var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
         // 应用样式
         _markdownViewer.Pipeline = pipeline;
         _markdownViewer.Markdown = text;
     }
     ///  移除控件:移除外部数据预览控件
     private void BtnRemoveControl_Click(object sender, RoutedEventArgs e)
     {
         // 获取触发事件的按钮
         Button removeButton = sender as Button;
         if (removeButton != null)
         {
             removeButton.Click -= BtnRemoveControl_Click;
             var dataView = UserDataService.ExternalDatas.FirstOrDefault(obj => obj.Model.Index.Equals(removeButton.Tag));
             wrapPanel.Children.Remove(dataView.View);
             UserDataService.ExternalDatas.Remove(dataView);
         }
     }
     public OllamaSharp.Models.Chat.Message GenerateMessage(string content, string role = "user")
     {
         return new OllamaSharp.Models.Chat.Message()
         {
             Content = content,
             Role = role
         };
     }
     #endregion
     #region 图像功能
     /// 转换图像为Base64
     private IEnumerable<IEnumerable<byte>> ConvertImagesToBytes(string imagePaths)
     {
         List<IEnumerable<byte>> imagesAsBytes = new List<IEnumerable<byte>>();
         try
         {
             byte[] imageBytes = File.ReadAllBytes(imagePaths);
             imagesAsBytes.Add(imageBytes);
         }
         catch (Exception ex)
         {
             MessageBox.Show("Failed to convert image to bytes: " + ex.Message);
         }
         return imagesAsBytes;
     }
     /// 转换图像为Base64
     private string ConvertImageToBase64(string imagePath)
     {
         try
         {
             byte[] imageBytes = File.ReadAllBytes(imagePath);
             return Convert.ToBase64String(imageBytes);
         }
         catch (Exception ex)
         {
             MessageBox.Show("Failed to convert image to Base64: " + ex.Message);
             return null;
         }
     }
     /// 从字节中加载图像
     public void LoadImagesFromBytes(IEnumerable<IEnumerable<byte>> imagesAsBytes)
     {
         foreach (IEnumerable<byte> imageBytes in imagesAsBytes)
         {
             try
             {
                 BitmapImage bitmapImage = new BitmapImage();
                 bitmapImage.BeginInit();
                 bitmapImage.StreamSource = new MemoryStream(imageBytes.ToArray());
                 bitmapImage.EndInit();
             }
             catch (Exception ex)
             {
                 MessageBox.Show("Failed to load image: " + ex.Message);
             }
         }
     }
     /// 转换图像到base64 IEnumerable
     private IEnumerable<string> ConvertImageToBase64IEnumerable(string imagePath)
     {
         string base64String = ConvertImageToBase64(imagePath);
         return new List<string> { base64String };
     }
     /// 从Base 64加载图像
     public void LoadImagesFromBase64(IEnumerable<string> imagesAsBase64)
     {
         foreach (string base64String in imagesAsBase64)
         {
             try
             {
                 string base64Data = base64String.Split(',').Last();
                 byte[] imageBytes = Convert.FromBase64String(base64Data);
                 BitmapImage bitmapImage = new BitmapImage();
                 bitmapImage.BeginInit();
                 bitmapImage.StreamSource = new MemoryStream(imageBytes);
                 bitmapImage.EndInit();
             }
             catch (Exception ex)
             {
                 MessageBox.Show("Failed to load image: " + ex.Message);
             }
         }
     }
     #endregion
     #region 回调方法
     ///  加载聊天记录回调:
     public void LoadChatRecordCallback(string path)
     {
         Debug.Print(path);
         List<ChatJsonDataModel> datas = DataService.ReadDataFormJsonFile(path);
         StringBuilder appendText = new StringBuilder();
         //切换聊天记录时,清空Chat对象中的消息,避免重复加载。
         ChatModel.Ollama.Chat.Messages.Clear();        
         foreach (ChatJsonDataModel data in datas)
         {
             //文本追加、添加到Markdown控件中
             appendText.AppendLine($"### {data.Date}{Environment.NewLine}");
             appendText.AppendLine($"## 【{data.Role}{Environment.NewLine}");
             appendText.AppendLine($"#### {data.Content}{Environment.NewLine}");
             AppendText($"---{Environment.NewLine}");
             appendText.AppendLine($"## 【AI】{Environment.NewLine}");
             appendText.AppendLine($"{data.Result}{Environment.NewLine}");
             appendText.AppendLine($"{Environment.NewLine}");
             //创建并添加消息到Chat中
             ChatModel.Ollama.Chat.Messages.Add(GenerateMessage(data.Content));
             ChatModel.Ollama.Chat.Messages.Add(GenerateMessage(data.Result, "system"));
         }
         //设置当前路径为该文件路径,方便提问时将内容续写到该文件中
         UserDataService.FileModel.FileNameDT = path;  
         ConvertMarkdownOut(appendText.ToString());//显示内容
         scrollViewer.ScrollToTop();    //滚动到顶部
     }
     #endregion
 }


Views

命名空间 OfflineAI.Views

资源

在这里插入图片描述



样式

BorderStyle.xaml
<Style x:Key="BorderStyle" TargetType="Border">
    <Setter Property="Background" Value="#FFFFFF"/>
    <Setter Property="Margin" Value="5"/>
    <Setter Property="Padding" Value="1"/>
    <Setter Property="BorderThickness" Value="1 1 1 1"/>
    <Setter Property="BorderBrush" Value="#FFFFFF"/>
</Style>


ButtonStyle.xaml
<!-- 定义圆角按钮的静态样式 -->
<Style x:Key="RoundCornerButtonStyle" TargetType="Button">
    <Setter Property="Width" Value="60"/>
    <Setter Property="Height" Value="20"/>
    <Setter Property="Margin" Value="10"/>
    <Setter Property="Padding" Value="5"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="BorderBrush" Value="DarkGray"/>
    <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="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border x:Name="roundedRectangle" 
                        CornerRadius="10"
                        Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <!-- 设置顶部圆角 -->
                    <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="Padding" Value="5"/>
    <!-- 调整高度以适应图标和文本 -->
    <Setter Property="Height" Value="50"/>
    <Setter Property="FontSize" Value="20"/>
    <Setter Property="Margin" Value="5 5 5 5"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="BorderBrush" Value="#FFFFFF"/>
    <!--设置背景颜色-->
    <Setter Property="Background">
        <Setter.Value>
            <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                <!-- 淡色 -->
                <GradientStop Color="#AAAAAA" Offset="0.7" />
                <GradientStop Color="#666666" Offset="0.3" />
            </LinearGradientBrush>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border x:Name="roundedRectangle" 
                        CornerRadius="10"
                        Background="#FFFFFF"
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <!-- 使用 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="#4477BB" Offset="0.4" />
                                    <GradientStop Color="#5599BB" 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="#6655BB" Offset="0.4" />
                                    <GradientStop Color="#6699BB" Offset="0.7" />
                                </LinearGradientBrush>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>


ComboBoxStyle.xaml
 <Style  x:Key="RoundComboBoxStyle" TargetType="{x:Type ComboBox}">
     <Setter Property="Margin" Value="5"/>
     <Setter Property="Width" Value="200"/>
     <Setter Property="HorizontalAlignment" Value="Stretch"/>
     <Setter Property="Template">
         <Setter.Value>
             <ControlTemplate TargetType="{x:Type ComboBox}">
                 <!--边缘设置-->
                 <Border x:Name="roundedRectangle"
                         CornerRadius="5" 
                         BorderThickness="1" 
                         BorderBrush="#AAAAFF"
                         Background="#EEEEEE">
                     <Grid>
                         <!--下拉箭头:开关按钮:(检验下拉菜单是否打开:IsDropDownOpen)-->
                         <ToggleButton IsChecked="{Binding IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}">
                             <!--开关按钮样式-->
                             <ToggleButton.Style>
                                 <Style TargetType="{x:Type ToggleButton}">
                                     <Setter Property="Margin" Value="2"/>
                                     <Setter Property="Width" Value="Auto"/>
                                     <Setter Property="Height" Value="Auto"/>
                                     <Setter Property="MinWidth" Value="0"/>
                                     <Setter Property="MinHeight" Value="0"/>
                                     <Setter Property="ClickMode" Value="Press"/>
                                     <Setter Property="Focusable" Value="False"/>
                                     <Setter Property="BorderThickness" Value="3"/>
                                     <!--下拉箭头颜色-->
                                     <Setter Property="Foreground" Value="#000000"/>
                                     <!--下拉箭头颜色边缘线宽-->
                                     <Setter Property="BorderBrush" Value="#00000000"/>
                                     <Setter Property="Template">
                                         <Setter.Value>
                                             <ControlTemplate TargetType="{x:Type ToggleButton}">
                                                 <DockPanel LastChildFill="False" 
                                                            SnapsToDevicePixels="True">
                                                     <!--面板背景颜色-->
                                                     <DockPanel.Background>
                                                         <SolidColorBrush Color="{TemplateBinding Background}">
                                                         </SolidColorBrush>
                                                     </DockPanel.Background>
                                                     
                                                     <Border x:Name="Border" 
                                                             CornerRadius="5"
                                                             DockPanel.Dock="Right" 
                                                             BorderBrush="{TemplateBinding BorderBrush}" 
                                                             BorderThickness="{TemplateBinding BorderThickness}">
                                                         <Path Data="M0,0L3.5,4 7,0z"
                                                               VerticalAlignment="Center"
                                                               HorizontalAlignment="Center"
                                                               Fill="{TemplateBinding Foreground}" />
                                                     </Border>
                                                 </DockPanel>
                                                 <!--是否校验-->
                                                 <ControlTemplate.Triggers>
                                                     <Trigger Property="IsChecked" Value="True"/>
                                                 </ControlTemplate.Triggers>
                                             </ControlTemplate>
                                         </Setter.Value>
                                     </Setter>
                                     <Style.Triggers>
                                         <Trigger Property="IsEnabled" Value="False">
                                             <Setter Property="Foreground" 
                                                     Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
                                         </Trigger>
                                     </Style.Triggers>
                                 </Style>
                             </ToggleButton.Style>
                         </ToggleButton>
                         
                         <!--项内容-->
                         <ContentPresenter Margin="3"
                                           IsHitTestVisible="False"
                                           VerticalAlignment="Center"
                                           HorizontalAlignment="Stretch"
                                           Content="{TemplateBinding SelectionBoxItem}"
                                           ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                                           ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
                         <!--下拉显示面板:设置下拉面板的相对位置-->
                         <Popup PopupAnimation="Slide" 
                                Focusable="False" 
                                HorizontalOffset="-1"
                                Height="200"
                                Width="{TemplateBinding ActualWidth}"
                                IsOpen="{TemplateBinding IsDropDownOpen}">
                             <Grid  SnapsToDevicePixels="True" HorizontalAlignment="Stretch">
                                 <Border  CornerRadius="5"
                                          BorderBrush="#AAAAFF"
                                          BorderThickness="1,1,1,1" 
                                          HorizontalAlignment="Stretch">
                                     <!--下拉面板背景颜色-->
                                     <Border.Background>
                                         <SolidColorBrush Color="#EEEEEE" />
                                     </Border.Background>
                                 </Border>
                                 <!--滑动条-->
                                 <ScrollViewer  SnapsToDevicePixels="True" HorizontalAlignment="Stretch" >
                                     <StackPanel IsItemsHost="True"
                                                 HorizontalAlignment="Stretch" KeyboardNavigation.DirectionalNavigation="Contained">
                                     </StackPanel>
                                 </ScrollViewer>
                             </Grid>
                         </Popup>
                     </Grid>
                 </Border>
                 <ControlTemplate.Triggers>
                     <!-- 触发颜色: 鼠标悬停时 -->
                     <Trigger Property="IsMouseOver" Value="True">
                         <Setter TargetName="roundedRectangle" Property="Background">
                             <Setter.Value>
                                 <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                                     <GradientStop Color="#AABBCC" Offset="0.4" />
                                 </LinearGradientBrush>
                             </Setter.Value>
                         </Setter>
                     </Trigger>
                 </ControlTemplate.Triggers>
             </ControlTemplate>
         </Setter.Value>
     </Setter>
 </Style>


ChatRecordListView

在这里插入图片描述

<UserControl x:Class="OfflineAI.Views.ChatRecordListView"
             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:viewModel="clr-namespace:OfflineAI.ViewModels"
             xmlns:commands="clr-namespace:OfflineAI.Commands"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="300">
    <UserControl.DataContext>
        <viewModel:ChatRecordListViewModel x:Name="ViewModel"/>
    </UserControl.DataContext>

    <Grid Background="#FFFFFF">
        <ScrollViewer Background="Transparent" x:Name="RecordScrollViewer"
                      HorizontalScrollBarVisibility="Visible"
                      VerticalScrollBarVisibility="Visible">
            <ListBox Background="Transparent" Margin="5" 
                     HorizontalAlignment="Stretch"
                     ItemsSource="{Binding ChatRecordCollection}">
                <ListBox.ItemContainerStyle>
                    <Style TargetType="ListBoxItem">
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Setter Property="Background" Value="#FAFFFF"/>
                    </Style>
                </ListBox.ItemContainerStyle>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Vertical"  Margin="0,0,0,0">
                            <!--显示消息日期-->
                            <TextBlock FontSize="14"  Margin="0,0,0,0"
                                       Background="#EEEEEE" Foreground="#CCCCCC"
                                       Tag="{Binding Uri}"  Text="{Binding JsonModel.Date}" >
                                <TextBlock.ContextMenu>
                                    <ContextMenu Name="RightKeyMenu"
                                            DataContext="{Binding PlacementTarget.DataContext,RelativeSource={RelativeSource Self}}">
                                        <MenuItem Name="Delete" Header="删除"
                                           Command="{Binding MenuItemMouseDownCommand}"  CommandParameter="{Binding Uri}" />
                                        <Separator/>
                                    </ContextMenu>
                                </TextBlock.ContextMenu>
                            </TextBlock>
                            
                            <!-- 显示消息内容 -->
                            <TextBlock FontSize="14"  Margin="10,5,0,0" 
                                       Tag="{Binding Uri}" 
                                       Text="{Binding JsonModel.Content}" >
                                <behavior:Interaction.Triggers>
                                    <!--鼠标点击命令事件-->
                                    <behavior:EventTrigger EventName="PreviewMouseDown">
                                        <behavior:InvokeCommandAction
                                         Command="{Binding DataContext.ChatRecordMouseDownCommand, 
                                                   RelativeSource={RelativeSource AncestorType=ListBox}}"
                                                   PassEventArgsToCommand="True">
                                        </behavior:InvokeCommandAction>
                                    </behavior:EventTrigger>
                                </behavior:Interaction.Triggers>
                            </TextBlock>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </ScrollViewer>
    </Grid>
</UserControl>


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="600" d:DesignWidth="800"
             HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <!--绑定数据上下文-->
    <UserControl.DataContext>
        <viewmodels:UserChatViewModel  x:Name="ViewModel"/>
    </UserControl.DataContext>
    <!--控件资源-->
    <UserControl.Resources>
        <ResourceDictionary>
            <!--资源字典: 添加控件样式-->
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../Views/Styles/MarkDownViewerStyle.xaml"/>
                <ResourceDictionary Source="../Views/Styles/BorderStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

    <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="250"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <!--行背景色-->
        <Border Grid.Row="0" Background="#FFFFFF"/>
        <Border Grid.Row="1" Background="#EEEEEE"/>
        <Border Grid.Row="2" Background="#EEEEEE" BorderThickness="0" BorderBrush="Transparent"/>
        <Grid Grid.Row="0" Height="Auto" 
              HorizontalAlignment="Stretch" 
              VerticalAlignment="Stretch">
            <!--markdown 滑动条-->
            <ScrollViewer Background="#FFFFFF"
                          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">
                    <markdig:MarkdownViewer.Resources>
                        <!-- 应用自定义样式 -->
                        <Style TargetType="Paragraph" BasedOn="{StaticResource Heading1Style}" />
                        <Style TargetType="Hyperlink" BasedOn="{StaticResource LinkStyle}" />
                    </markdig:MarkdownViewer.Resources>
                    <!--命令绑定事件:鼠标滚动显示内容-->
                    <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>
            
            <!-- Ollama 运行状态显示 -->
            <TextBlock VerticalAlignment="Center"
                       HorizontalAlignment="Center"
                       FontSize="18"
                       IsHitTestVisible="False">
                <TextBlock.Style>
                    <Style TargetType="TextBlock">
                        <!-- 默认显示运行状态 -->
                        <Setter Property="Visibility" Value="Visible" />
                        <Style.Triggers>
                            <!-- 如果 ChatModel.显示状态为True,显示运行状态 -->
                            <DataTrigger Binding="{Binding ChatModel.IsShowRunState}" Value="True">
                                <Setter Property="Visibility" Value="Visible" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding ChatModel.IsShowRunState}" Value="False">
                                <Setter Property="Visibility" Value="Hidden" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
                <WrapPanel>
                    <Image Width="24" Height="24"  Margin="0,0,5,0"  
                           HorizontalAlignment="Center" VerticalAlignment="Center">
                        <Image.Style>
                            <Style TargetType="Image">
                                <Style.Triggers>
                                    <!-- 如果 RunState 为 true,显示绿色图像 -->
                                    <DataTrigger Binding="{Binding ChatModel.RunState}" Value="False">
                                        <Setter Property="Source" Value="../Views/Resources/ollama-run-state-red-32.png" />
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding ChatModel.RunState}" Value="True">
                                        <Setter Property="Source" Value="../Views/Resources/ollama-run-state-green-32.png" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Image.Style>
                    </Image>
                <TextBox VerticalAlignment="Center" HorizontalAlignment="Center" 
                         Background="Transparent" BorderThickness="0">
                     <TextBox.Style>
                         <Style TargetType="TextBox">
                             <Style.Triggers>
                                 <!-- 如果 RunState 为 true,显示绿色图像 -->
                                 <DataTrigger Binding="{Binding ChatModel.RunState}" Value="False">
                                     <Setter Property="Text" Value="Ollama停止运行" />
                                 </DataTrigger>
                                 <DataTrigger Binding="{Binding ChatModel.RunState}" Value="True">
                                     <Setter Property="Text" Value="Ollama正在运行" />
                                 </DataTrigger>
                             </Style.Triggers>
                         </Style>
                     </TextBox.Style>
                </TextBox>
                </WrapPanel>
            </TextBlock>
        </Grid>
        <!--2行内容:显示回话内容-->
        <Grid Grid.Row="1"  Margin="2" Background="#FFFFFF">
            <!--定义三行-->
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="40"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>

            <!--1行Border样式:-->
            <Border Grid.Row="0" Grid.Column="1" BorderThickness="0" Style="{StaticResource BorderStyle}"/>
            <!--2行Border样式:-->
            <Border Grid.Row="1" Grid.Column="1" Style="{StaticResource BorderStyle}"/>
            <!--3行Border样式:-->
            <Border Grid.Row="2" Grid.Column="1" 
                    Background="#EEEEEE"
                    BorderThickness="1 0 1 1"
                    BorderBrush ="#000000" />
            
            <!--1行内容区域:外部数据预览面板-->
            <Grid Grid.Row="0" Grid.Column="1"  Background="#FFFFFF">
                <WrapPanel Name="FileShowArea" Margin="0,0,0,0" 
                           HorizontalAlignment="Left" 
                           VerticalAlignment="Top">
                    <behavior:Interaction.Triggers>
                        <behavior:EventTrigger EventName="Loaded">
                            <behavior:InvokeCommandAction 
                              Command="{Binding ExternalDataPanelLoadedCommand}"
                              PassEventArgsToCommand="True"/>
                        </behavior:EventTrigger>
                    </behavior:Interaction.Triggers>
                </WrapPanel>
            </Grid>
            
            <!--2行内容区域:文本输入框-->
            <Grid Grid.Row="1" Grid.Column="1">
                <TextBox Padding="5" 
                         FontSize="14"
                         BorderThickness="1,1,1,0"
                         BorderBrush ="#000000"
                         AcceptsReturn="True" 
                         Background="#EEEEEE"  
                         Foreground="#000000"
                         TextWrapping="WrapWithOverflow"
                         Text="{Binding ChatModel.InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                         VerticalScrollBarVisibility="Auto">
                    <!--回车发送-->
                    <TextBox.InputBindings>
                        <KeyBinding Command="{Binding SubmitQuestionCommand}" Key="Enter"/>
                    </TextBox.InputBindings>
                </TextBox>

                <!-- 提示文本 -->
                <TextBlock Text="给AI发送消息"
                       Foreground="#555555"
                       VerticalAlignment="Top"
                       HorizontalAlignment="Left"
                       Margin="10,5,0,0"
                       FontSize="14"
                       IsHitTestVisible="False">
                    <TextBlock.Style>
                        <Style TargetType="TextBlock">
                            <!-- 默认隐藏提示文本 -->
                            <Setter Property="Visibility" Value="Collapsed" />
                            <Style.Triggers>
                                <!-- 如果 ChatModel.InputText 为空,显示提示文本 -->
                                <DataTrigger Binding="{Binding ChatModel.InputText}" Value="">
                                    <Setter Property="Visibility" Value="Visible" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding ChatModel.InputText}" Value="{x:Null}">
                                    <Setter Property="Visibility" Value="Visible" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </Grid>
            
            <!--3行内容区域:文件选择,提交按钮-->
            <Grid Grid.Row="2" Grid.Column="1">
                <WrapPanel  Margin="0,0,5,0" 
                            HorizontalAlignment="Right" 
                            VerticalAlignment="Center">
                    <!--选择添加文件按钮-->
                    <Button Width="50" Command="{Binding SelecteAddFileCommand}" BorderThickness="0" Background="Transparent">
                        <Image Width="24" Height="24" x:Name="FileIco"
                            Source="/Views/Resources/append-black-24.png" 
                            HorizontalAlignment="Right" VerticalAlignment="Center"/>
                        <!-- 定义ToolTip -->
                        <Button.ToolTip>
                            <ToolTip Content="添加文件" Placement="Top"  HorizontalOffset="0" VerticalOffset="0">
                                <ToolTip.Triggers>
                                    <!-- 设置ToolTip显示延迟1-->
                                    <EventTrigger RoutedEvent="ToolTip.Opened">
                                        <BeginStoryboard>
                                            <Storyboard>
                                                <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1"/>
                                            </Storyboard>
                                        </BeginStoryboard>
                                    </EventTrigger>
                                </ToolTip.Triggers>
                            </ToolTip>
                        </Button.ToolTip>
                        <!-- 设置ToolTip的显示和隐藏 -->
                        <Button.Style>
                            <Style TargetType="Button">
                                <Setter Property="ToolTipService.ShowDuration" Value="10000"/>
                                <!-- ToolTip显示持续时间 -->
                                <Setter Property="ToolTipService.InitialShowDelay" Value="1000"/>
                                <!-- 延迟1秒显示 -->
                                <Setter Property="ToolTipService.BetweenShowDelay" Value="0"/>
                                <!-- 防止快速显示 -->
                            </Style>
                        </Button.Style>
                    </Button>
                    
                    <!--问题提交按钮-->
                    <Button Width="50"  Margin="5 0 0 0" BorderThickness="0"   Background="Transparent"
                            Command="{Binding SubmitQuestionCommand}" 
                            Content="{Binding ChatModel.SubmitButtonName}">
                    </Button>
                </WrapPanel>
            </Grid>
        </Grid>
        
        <!--3行内容:-->
        <Grid Grid.Row="2">
        </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>
    </Grid>
</UserControl>


UserViews

命名空间:OfflineAI.Views.UserViews

在这里插入图片描述

<UserControl x:Class="OfflineAI.Views.UserViews.ExternalDataPreView"
             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.UserViews"
             xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels"
             mc:Ignorable="d"
             HorizontalAlignment="Stretch"
             VerticalAlignment="Stretch">
    <!--绑定数据上下文-->
    <UserControl.DataContext>
        <viewmodels:ExternalDataPreViewModel  x:Name="ViewModel"/>
    </UserControl.DataContext>
    <Grid Background="Transparent" >
        <!--外部数据预览视图:1、显示加载的外部数据文件名2、隐藏显示文件路径3、点击按钮可以删除 -->
        <Border Background="Transparent"/>
        <WrapPanel HorizontalAlignment="Left" VerticalAlignment="Center" Margin="1" Background="#AAAABB">
            <Image x:Name="ImgFileIco" Width="20" Height="20" 
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   Source="{Binding Model.ImageSource}" >
            </Image>
            <TextBox x:Name="Tbx_Text"  
              Text="External Data"
              IsReadOnly="True"
              Background="#AAAABB"
              HorizontalContentAlignment="Left"
              VerticalContentAlignment="Center"
              TextWrapping="Wrap"/>
            <Button x:Name="RemoveButton" 
                    Tag="{Binding Model.Index}"
                    Background="#AAAABB" 
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"
                    VerticalContentAlignment="Center" 
                    Content=" × ">
            </Button>
        </WrapPanel>
    </Grid>
</UserControl>


MainWindow

命名空间:OfflineAI
<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-logo-128.ico"
        MinHeight="600" MinWidth="800">
    <!--绑定上下文-->
    <Window.DataContext>
        <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 >
        <!-- 定义2-->
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <!-- 定义3列:-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="10"/>
        </Grid.ColumnDefinitions>

        <Grid Grid.Column="0" Visibility="{Binding MainModel.ExpandedMenuIsHide}">
            <!--折叠栏内添加的视图-->
            <ContentControl Margin="5,5,5,5" Width="{Binding MainModel.ExpandedBarWidth}"
                         Content="{Binding MainModel.ExpandedBarView}" 
                         HorizontalContentAlignment="Stretch" 
                         VerticalContentAlignment="Stretch"/>
        </Grid>
        <!-- 右侧内容区域 -->
        <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="#EEEEEE"/>
            <Border Grid.Row="1" Background="#FFFFFF" Grid.RowSpan="2"/>
            <!--第一行内容:左对齐内容-->
            <WrapPanel VerticalAlignment="Center">
                <Button  Background="Transparent" BorderThickness="0"
                         Command="{Binding ExpandedMenuCommand}"
                         CommandParameter="expanded">
                    <StackPanel>
                        <Image Source="Views/Resources/expended-bar-black-64.png"
                          Margin="5" Width="32" Height="32"
                          HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </StackPanel>
                </Button>
                <!--视图切换:首页-->
                <Button x:Name="Btn_HomePage" Width="40" Height="36" FontSize="16"
                   Style="{StaticResource IconButtonStyle}" 
                   Command="{Binding SwitchViewCommand}"
                   CommandParameter="UserChatView">
                    <StackPanel Orientation="Horizontal">
                        <Image Source="Views/Resources/home-black-24.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/edit-black-24.png"
                                Margin="5" Width="24" Height="24"
                                HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        <TextBlock  Text="新聊天" VerticalAlignment="Center"/>
                    </StackPanel>
                </Button>
                <!--模型列表-->
                <Label  Foreground="#000000" Margin="5" FontSize="16" VerticalAlignment="Center" >
                    <TextBlock Foreground="#333333"  Text="模型:" VerticalAlignment="Center"/>
                </Label>
                <ComboBox x:Name="Cbx_ModelList" 
                          Style="{StaticResource RoundComboBoxStyle}" 
                          ItemsSource="{Binding MainModel.ModelListCollection}"
                          SelectedItem="{Binding MainModel.SelectedModel}">
                </ComboBox>
            </WrapPanel>
            <!--第一行内容:右对齐内容-->
            <WrapPanel Margin="0,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" >
                <Button Width="36" Height="36"
                        Style="{StaticResource IconButtonStyle}" 
                        Command="{Binding SwitchViewCommand}"
                        CommandParameter="SystemSettingView">
                    <StackPanel Orientation="Horizontal" Background="Transparent">
                        <Image Source="/Views/Resources/setting-64.png" 
                            Width="24" Height="24"
                            HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </StackPanel>
                </Button>
            </WrapPanel>
            <!--第二行内容:显示当前视图-->
            <ContentControl Grid.Row="1" Margin="5,5,5,5"
                 Content="{Binding MainModel.CurrentView}" 
                 HorizontalContentAlignment="Stretch" 
                 VerticalContentAlignment="Stretch" Grid.RowSpan="2"/>
        </Grid>
    </Grid>
</Window>





代码完结

附上项目地址:https://github.com/timenodes/OfflineAI


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

相关文章:

  • Python Flask 从 HTTP 请求中解包参数
  • 内检实验室LIMS系统在汽车制造行业的应用
  • 【每日学点HarmonyOS Next知识】粘贴板、异步、用户权限、锁屏事件、对话框
  • [网络爬虫] 动态网页抓取 — Selenium 入门操作
  • FANUC机器人字符串寄存器及指令介绍
  • yolov8在昇腾芯片上的测试
  • Python与Solidity联手:从跨语言智能合约开发到区块链生态跃迁
  • 塔能科技:智能机箱,为城市安防 “智” 造坚实堡垒
  • CES Asia 2025:AR/VR/XR论坛峰会备受瞩目
  • android开发:activity
  • L2-4 吉利矩阵
  • 【后端】【ubuntu】 ubuntu目录权限查看的几种方法
  • 推理大模型时代,TextIn ParseX助力出版业知识资产重构
  • Stable Diffusion 模型文件 .ckpt 与 .safetensors 的区别
  • 全方位 JVM 调优参数详解
  • Pytorch实现之利用普通GAN的人脸修复
  • Git 高级指南:完整命令大全及进阶用法
  • 速算迷你世界脚本UI
  • 【使用 Python 结合 ADB 监控进程状态】
  • Webpack 深度解析:构建现代前端工程的基石