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(){//创建日期目录1string filePath = $"{FileModel.Directory}\\{DateTime.Now.ToString("yyyy")}";Directory.CreateDirectory($"{filePath}");//创建日期目录2filePath = $"{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, 0xD8private static bool IsJpeg(byte[] headerBytes){return headerBytes.Length >= 2 && headerBytes[0] == 0xFF && headerBytes[1] == 0xD8;}/// PNG 文件的前八个字节是固定的签名:137 80 78 71 13 10 26 10private 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{// 设置默认设备为GPUEnvironment.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.exeArguments = $"/C {command}",    // 设置要执行的命令UseShellExecute = true,         // 使用操作系统shell启动进程CreateNoWindow = false,         //不创建窗体};try{Process process = Process.Start(processStartInfo);// 启动进程process.WaitForExit();    // 等待进程退出process.Close();          // 返回是否成功执行return process.ExitCode == 0;}catch (Exception ex){Debug.WriteLine($"发生错误: {ex.Message}");// 其他异常处理return false;}}
}


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(){//初始化OllamaMainModel.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){// 解析Markdownvar 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 图像功能/// 转换图像为Base64private 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;}/// 转换图像为Base64private 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 IEnumerableprivate 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:InvokeCommandActionCommand="{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:MarkdownViewerName="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.rhkb.cn/news/32466.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

若依框架-给sys_user表添加新字段并获取当前登录用户的该字段值

目录 添加字段 修改SysUser类 修改SysUserMapper.xml 修改user.js 前端获取字段值 添加字段 若依框架的sys_user表是没有age字段的&#xff0c;但由于业务需求&#xff0c;我需要新添加一个age字段&#xff1a; 修改SysUser类 添加age字段后&#xff0c;要在SysUser类 …

用Qt手搓AI助手,挑战24小时开发DeepSeek Assistant!

一、项目需求分析与技术选型 DeepSeekAssistant是一款基于深度求索&#xff08;DeepSeek&#xff09;API的智能对话助手&#xff0c;核心需求包括&#xff1a; 用户界面友好&#xff1a;支持多轮对话展示数据持久化&#xff1a;历史记录存储与检索异步网络通信&#xff1a;AP…

linux 软件扩展GPU显存

概述 共享内存可以通过 Unified Memory&#xff08;统一内存&#xff09;来实现&#xff0c;它允许 CPU 和 GPU 共享相同的内存地址空间&#xff0c;从而方便数据的传输和访问。 利用该技术可解决家用GPU 机器学习时显存不足的问题 &#xff08;注&#xff1a; 虽然解决了爆显…

Linux——进程初步

学进程前我们需要知道什么&#xff1f; 一、冯诺依曼体系结构 图中就是我们电脑运作时的大致工作流程&#xff0c;其中输入设备、输出设备我们也叫外设。其中&#xff0c;输入设备有比如键盘、鼠标、磁盘、摄像头等。输出设备有显示器、磁盘、打印机等。图中的存储器我们也叫内…

LeetCode-122. 买卖股票的最佳时机 II

其实这题画个折线图就很清晰了&#xff0c;因为我们每天都可以买卖股票&#xff0c;所有我们就只计算上升趋势的股票收益就好了&#xff0c;最小刻度为1&#xff0c;进行差值计算&#xff0c;取总和。 var maxProfit function(prices){let sum0;for(let i1;i<prices.leng…

关于前后端整合和打包成exe文件的个人的总结和思考

前言 感觉有很多东西&#xff0c;不知道写什么&#xff0c;随便写点吧。 正文 前后端合并 就不说怎么开发的&#xff0c;就说点个人感觉重要的东西。 前端用ReactViteaxios随便写一个demo&#xff0c;用于CRUD。 后端用Django REST Framework。 设置前端打包 import { …

Vue | 开学第一课!零基础教程

目录 背景介绍 安装方式 下载环境软件 NodeJS手册 如何查看node版本 镜像源 完整流程 创建根文件夹并拖进 VSCode 调用控制台 安装 vite 脚手架 配置项目 安装依赖 启动项目 查看页面 问题 创建项目失败 解决方法 权限问题 解决方法 其他问题 背景介绍 今…

泛微ecode的页面开发发送请求参数携带集合

1.在开发过程中我们难免遇见会存在需要将集合传递到后端的情况&#xff0c;那么这里就有一些如下的注意事项&#xff0c;如以下代码&#xff1a; // 新增action.boundasync addQuestion(formData) {var theList this.questionAnswerList;var questionAnswerListArray new Ar…

Tomato靶机攻略

将tomato改为NAT模式 扫描ip arp-scan -l 扫描端口&#xff0c;发现ssh服务端口从22改为2211 扫描目录 发现http://192.168.31.134/antibot_image/ 访问 查看所有php文件的源码&#xff0c;看看有什么不同的地方 在info.php的源码中发现问题 在输入后&#xff0c;成功显示…

EasyRTC嵌入式音视频通话SDK:基于纯C语言的跨平台实时通信系统设计与实践

随着物联网、移动互联网的快速发展&#xff0c;实时音视频通信技术在智能硬件、远程协作、工业控制等领域广泛应用。然而&#xff0c;跨平台兼容性差、资源占用高、定制化难等问题&#xff0c;仍是传统RTC方案的痛点。 EasyRTC嵌入式音视频通话SDK凭借纯C语言设计与全平台覆盖…

HCIP复习拓扑练习(修改版)

拓扑&#xff1a; 实际&#xff1a; 需求&#xff1a; 需求分析 1.这意味着学校内部网络能够正常解析域名并进行互联网访问。 2. PC1和PC2处于同一个内网192.168.1.0/24&#xff0c;其中PC1有权限访问外部网段3.3.3.0/24&#xff0c;而PC2没有。这涉及ACL&#xff08;访问控制…

vue-next-admin修改配置指南

目录 1.如何开启侧边栏logo 2.修改侧边栏顶部的logo与文字 3.修改侧边栏路由logo 4.浏览器标题栏图标与文字修改 5.修改侧边栏的背景颜色、顶部导航栏背景颜色、字体颜色、激活时颜色等 6.去除或添加修改右上方放大、信息、头像昵称&#xff08;登录获取之后存储进行修改图…

ruoyi-cloud-plus编译记录-1

dockerfile部署jar 添加一个run configuration ‘ruoyi-nacos’ run configuration 参考Docker - 在IntelliJ IDEA中一键部署项目 - hucat - 博客园 jar包编译不成功&#xff0c;没有jar&#xff0c;docker部署nacos就没法进行下去 参考链接 maven 构建报错 This failure was…

【算法day8】整数反转

整数反转 https://leetcode.cn/problems/reverse-integer/description/ class Solution { public:int reverse(int x) {int MAX_LENGTH 11; // 32位整数的最大数字的位数int* num (int*)calloc(sizeof(int), MAX_LENGTH); //用于保存进位每一位的数字int current x;int pos…

MySQL库和表的操作详解:从创建库到表的管理全面指南

目录 一、MySQL库的操作详解 〇、登录MySQL 一、数据库的创建与字符集设置 1. 创建数据库的语法 2. 创建数据库示例 查看创建出来的文件: bash下查看MySQL创建的文件 二、字符集与校验规则 1. 查看系统默认设置 2. 查看支持的字符集与校验规则 3. 校验规则对查询的影响…

Linux中的基本指令(上)

目录 ls指令 判断linux中文件 pwd指令 认识路径 ​编辑 绝对路径/相对路径 cd指令 简要理解用户 理解家目录 echo指令和printf指令 touch指令 mkdir指令 cat指令 tree指令 rmdir指令和rm指令 man指令 cp指令 which指令 alias 指令 date指令 cal指令 理解…

WPF 与 GMap.NET 结合实现雷达目标动态显示与地图绘制

概述 雷达上位机是雷达系统中用于数据可视化、分析和控制的核心软件。本文将介绍如何使用 C# 和 WPF 框架开发一个雷达上位机程序&#xff0c;主要功能包括&#xff1a; 显示目标轨迹&#xff1a;在界面上实时绘制雷达探测到的目标轨迹。点击显示详细信息&#xff1a;用户点击…

「string」笔记

参考&#xff1a;比特鹏哥 1. string string是一种类型&#xff0c;指的是字符串&#xff0c;比字符数组更高级 头文件 <string> #include <string>int main() {string a;//未初始化string b "good good";//初始化string c("good sfternoon&q…

AutoGen使用学习

AutoGen使用学习 上篇文件使用使用【autoGenchainlitdeepSeek】实现【多角色、多用户、多智能体对话系统】&#xff0c;本次系统的学习autoGen的使用方法 文章目录 AutoGen使用学习[toc]1-核心知识点2-参考网址3-实战案例1-autoGen安装和基础使用主要功能安装方法使用示例注意事…

207、【图论】孤岛的总面积

题目 思路 相比于 206、【图论】岛屿数量&#xff0c;就是在这个代码的基础上。先遍历边界&#xff0c;将边界连接的岛屿变为0&#xff0c;然后再计算一遍当前为1的岛屿面积。 代码实现 import collectionsn, m list(map(int, input().split())) graph []for _ in range(n…