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

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

相关文章:

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

前言 系 统&#xff1a;Window11 开发工具&#xff1a;Visual Studio 2022 相关技术&#xff1a;C# 、WPF .Net 8.0 1、C#实现本地AI聊天功能 WPFOllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。 新增根据聊天记录回复的功能。 优化了部分ViewModel&#xff…...

用sphinx-doc整理文档#2

上一篇博客&#xff1a;用sphinx-doc整理文档 回头看&#xff0c;上一篇博客已经是18年的事情了。最近我又开始维护起18年的项目了。最近策划同事提了一些需求。我又改进了一波&#xff0c;所以有本文。 sphinx支持导出pdf sphinx本身是支持导出pdf的&#xff0c;命令如下&am…...

DBeaver部分操作指南(数据库连接,构造ERD图,格式化SQL)

详细步骤指导如何使用DBeaver来连接到数据库&#xff1a; 步骤 1: 下载并安装 DBeaver 如果还没有安装DBeaver&#xff0c;请访问DBeaver官网下载适合操作系统的版本&#xff0c;并按照指示完成安装。 步骤 2: 启动 DBeaver 安装完成后&#xff0c;启动DBeaver应用程序。 …...

十种处理权重矩阵的方法及数学公式

1. 权重归一化&#xff08;Weight Normalization&#xff09; 目的&#xff1a;通过分离权重向量的范数和方向来加速训练。公式&#xff1a;对于权重向量 w \mathbf{w} w&#xff0c;归一化后的权重 w ′ \mathbf{w} w′ 为&#xff1a; w ′ w ∥ w ∥ \mathbf{w} \frac{…...

姚安娜新剧瘦了一圈,《仁心俱乐部》急诊医生顾诗宜在线上岗

《仁心俱乐部》在芒果 TV 播出&#xff0c;湖南卫视金鹰独播剧场也随之播出&#xff0c;这一剧集受到了不少观众的关注。姚安娜在剧中饰演的急诊科医生顾诗宜&#xff0c;她为患者检查身体时动作娴熟&#xff0c;与患者沟通时展现出的耐心和专注&#xff0c;都展现出很高的专业…...

postgresql源码安装

步骤 1: 安装依赖 在开始之前&#xff0c;请确保您的系统上安装了编译 PostgreSQL 所需的依赖包。使用以下命令安装必要的软件包&#xff1a; 对于 Debian/Ubuntu 系统&#xff1a; sudo apt update sudo apt install build-essential libreadline-dev zlib1g-dev flex biso…...

【51单片机】程序实验15.DS18B20温度传感器

主要参考学习资料&#xff1a;B站【普中官方】51单片机手把手教学视频 开发资料下载链接&#xff1a;http://www.prechin.cn/gongsixinwen/208.html 单片机套装&#xff1a;普中STC51单片机开发板A4标准版套餐7 目录 DS18B20介绍主要特性内部结构控制时序初始化时序写时序读时序…...

Java 集合框架:数据管理的强大工具

Java集合框架&#xff1a;数据管理的强大工具 目录 Java集合框架&#xff1a;数据管理的强大工具引言一、Set集合1. 定义与特点2. 常用实现类 - HashSet创建方式常用方法遍历方式 二、Map集合1. 定义与特点2. 常用实现类 - HashMap创建方式常用方法遍历方式 三、List集合1. 定义…...

AIM-T500绝缘监测仪:实时监测,确保IT系统绝缘安全-安科瑞 蒋静

在现代工业生产中&#xff0c;电力系统的稳定性和安全性至关重要。电力系统的绝缘状况直接关系到设备的正常运行和人员的安全。为了确保IT配电系统&#xff08;不接地系统&#xff09;的绝缘状况始终处于好的状态&#xff0c;安科瑞电气股份有限公司推出了AIM-T500绝缘监测仪&a…...

css3-学习

css 特性 选择器 基础选择器 标签选择器 ID 选择器 类选择器 CSS 的优先级是什么&#xff1f; !important > 内联样式(style“”) > ID 选择器 > 类选择器 属性选择器 伪类 > 标签选择器 伪元素选择器。 层级选择器 foo bar 后代选择器&#xff0c;元素的…...

neo4j图数据库

neo4j图数据库 学习neo4j图数据库的使用 文章目录 neo4j图数据库[toc]1-核心知识点2-思路整理3-参考网址4-上手实操1-安装neo4j-docker2-要学习CQL语句3-解决cmake问题1-可能的原因及解决办法1. cmake 未正确安装或者版本不兼容2. 依赖库缺失3. 环境变量配置问题4. 网络问题 2-…...

go下的Prototype学习笔记

本博文讲解prototype的学习笔记&#xff0c;内容包含了syntax、option go_package、message、service进行讲解&#xff0c;举例了常见写法。 文章目录 1.syntax 声明语法版本2.package 给当前的proto分配包名称3.option4.message4.1 切片数组4.2 map4.3 类型嵌套 5. 消息定义 …...

[高阶技术了解]WebRPC详解

“webrpc详解”&#xff0c;我需要根据我搜索到的资料来回答这个问题。首先&#xff0c;得弄清楚webrpc到底是什么。根据&#xff0c;Webrpc是一个模式驱动的后端服务开发工具&#xff0c;允许通过RIDL或JSON模式文件生成代码&#xff0c;支持多种语言如Go、TypeScript等。它类…...

地基注解@Controller和@RestController区别

前记&#xff1a;基础啊&#xff0c;区别点重点理解&#xff1b; 在Spring和Spring Boot框架中&#xff0c;Controller和RestController都用于处理HTTP请求&#xff0c;但它们在设计目的和用法上有显著区别。以下是它们的核心区别及示例说明&#xff1a; ​1. 核心区别 特性C…...

UI自动化:seldom框架和Selenium

以下是关于 seldom框架 和 Selenium 的对比解析及结合使用的详细说明&#xff0c;帮助理解二者的定位、功能差异和应用场景&#xff1a; 1. 核心定位 工具定位Selenium浏览器自动化工具库&#xff0c;提供直接操控浏览器的底层API&#xff08;如点击、输入、获取元素等&#x…...

机器学习项目实战——信用评分与贷款风险评估(主页有源码)

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​ ​​​ 1. 领域介绍 信用评分与贷款风险评估是金融领域中的一个重要应用场景。随着金融科技的快速发展&#xff0c;银行、信用卡公司、P2P…...

使用 OptiSLang 和 MotorCAD 构建一个强大的电机优化元模型

介绍 在本文中&#xff0c;我们将检查这些敏感性分析的结果&#xff0c;并构建一个健壮的元模型&#xff0c;作为优化过程的基础。 本文涵盖&#xff1a; 解释敏感性分析结果了解元模型及其在优化中的重要性构建和完善最佳预后模型 &#xff08;MOP&#xff09;使用预后系数…...

【科研绘图系列】python绘制分组点图(grouped dot plot)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据函数`generateRectBoxDF` 函数主要作用参数解释逻辑流程`nmfDotPlot` 函数主要作用参数解释逻辑流程画图1画图2画图3画图4介绍 【科研绘图系列】python绘制…...

【Android】adb shell基本使用教程

adb shell 是 Android Debug Bridge (ADB) 工具中的一个命令&#xff0c;用于在连接的 Android 设备或模拟器上执行 shell 命令。通过 adb shell&#xff0c;你可以直接与设备的 Linux 内核交互&#xff0c;执行各种操作。 基本用法 启动 adb shell&#xff1a; 在终端或命令提…...

257. 二叉树的所有路径(递归+回溯)

257. 二叉树的所有路径 力扣题目链接(opens new window) 给定一个二叉树&#xff0c;返回所有从根节点到叶子节点的路径。 说明: 叶子节点是指没有子节点的节点。 示例: 思路&#xff1a;在叶子节点收割结果&#xff0c;如果不是叶子节点&#xff0c;则依次处理左右子树&a…...

C++和标准库速成(一)——HelloWorld和名称空间

目录 1. 引言1. 简单小程序"Hello World"1.1 模块导入1.2 预处理指令1.2.1 简介1.2.2 常用的预处理指令 1.3 main()函数1.4 输入输出流1.4.1 输出流1.4.2 转义字符1.4.3 输入流 2. 名称空间2.1 定义名称空间2.2 using指令2.3 嵌套名称空间2.4 名称空间别名 参考 1. 引…...

OpenHarmony 5.0 MP4封装的H265视频播放失败的解决方案

问题现象 OpenHarmony 5.0版本使用AVPlayer播放MP4封装格式的H.265(HEVC)编码格式的视频时解码失败导致播放失败 问题原因 OpenHarmony 5.0版本AVPlayer播放器使用histreamer引擎&#xff0c;因为 libav_codec_hevc_parser.z.so 动态库未开源导致从MP4封装中分离的HVCC格式的…...

索引-最左匹配

在数据库索引中&#xff0c;最左匹配原则确实在遇到某些范围查询时会停止向右匹配&#xff0c;但对于 >、<、BETWEEN 和前缀匹配的 LIKE&#xff0c;索引匹配可以继续使用后续列。以下是详细分析&#xff1a; 1. 最左匹配原则的核心规则 最左匹配原则要求查询条件从复合…...

感觉自己邮电部诗人

中心扩散 第二次做这道题&#xff0c;求回文子串最大长度的时候&#xff0c;计算写成了j-i1&#xff0c;看了15分钟才看发现哪里出了问题&#xff0c;感觉自己邮电部诗人&#xff0c;望周知。...

Java代理方式的详细介绍,包括代码示例、注释说明及其差异对比表格

Java代理方式 Java中的代理模式是一种结构型设计模式&#xff0c;用于在不修改原始类的情况下增强其功能。Java支持两种代理方式&#xff1a; 静态代理动态代理 JDK动态代理CGLIB动态代理 1. 静态代理 静态代理通过手动编写代理类实现&#xff0c;代理类和目标类实现相同的…...

接口对外安全交互新姿势

文章目录 1.前言2.姿势2.1 AES2.2 body参数签名及验签2.3使用sm2 加ip白名单 3.总结 1.前言 由于这久做了一个乐企数电开票的项目&#xff0c;已经上线了&#xff0c;真的是一言难尽&#xff0c;再回首已经是轻舟已过万重山&#xff0c;接口通过外网暴露给业务方使用&#xff0…...

Docker基础篇——Ubuntu下Docker安装

大家好我是木木&#xff0c;在当今快速发展的云计算与云原生时代&#xff0c;容器化技术蓬勃兴起&#xff0c;Docker 作为实现容器化的主流工具之一&#xff0c;为开发者和运维人员带来了极大的便捷 。下面我们一起进行Docker安装。 Docker的官方Ubuntu安装文档&#xff0c;如…...

《深度解析DeepSeek-M8:量子经典融合,重塑计算能效格局》

在科技飞速发展的今天&#xff0c;量子计算与经典算法的融合成为了前沿领域的焦点。DeepSeek-M8的“量子神经网络混合架构”&#xff0c;宛如一把钥匙&#xff0c;开启了经典算法与量子计算协同推理的全新大门&#xff0c;为诸多复杂问题的解决提供了前所未有的思路。 量子计算…...

关于C/C++语言的初学者在哪刷题,怎么刷题

引言&#xff1a; 这篇博客主要是针对初学者关于怎么在网上刷题&#xff0c;以及在哪里刷题。 1.介绍平台&#xff08;在哪刷题&#xff09;&#xff1a; 1.牛客牛客网https://www.nowcoder.com/ &#xff1a;有许多面试题&#xff0c;也有许多供学习者练习的题 2.洛谷洛谷 …...

【redis】string类型相关操作:SET、GET、MSET、MGET、SETNX、SETEX、PSETEX

文章目录 二进制存储编码转换SET 和 GETSETGET MSET 和 MGETSETNX、SETEX 和 PSETEX Redis 所有的 key 都是字符串&#xff0c;value 的类型是存在差异的 二进制存储 Redis 中的字符串&#xff0c;直接就是按照二进制数据的方式存储的 不仅仅可以存储文本数据&#xff0c;还可…...

el-table中嵌套了el-form-item 导致的内容不垂直居中展示的问题

el-table中嵌套了el-form-item 导致的内容不垂直居中展示的问题 这个问题原先我一直没有找到问题的关键点&#xff0c;后来看了一篇文章得知由于el-form-item的margin导致的 下面的css类告诉我们。正常的表单校验margin就是20px&#xff0c;在el-table中的只有是校验失败的才会…...

LVCMOS(Low Voltage Complementary Metal-Oxide-Semiconductor)电平详解

一、LVCMOS电平的定义与核心特性 LVCMOS&#xff08;低压互补金属氧化物半导体&#xff09;是 CMOS技术的低电压版本&#xff0c;专为现代低功耗、高集成度芯片设计&#xff0c;支持 1.2V、1.8V、2.5V、3.3V 等多种电压等级。其通过优化晶体管结构和供电电压&#xff0c;显著降…...

计算机操作系统(一) 什么是操作系统

计算机操作系统&#xff08;一&#xff09; 什么是操作系统 前言一、什么是操作系统二、操作系统的作用三、推动操作系统发展的主要动力总结&#xff08;核心概念速记&#xff09;&#xff1a; 前言 当你打开电脑、点击应用、播放音乐时&#xff0c;是谁在背后默默协调这一切&…...

《用 python、MySQL 和 Chart.js 打造炫酷数据看板》实战案例笔记

今天&#xff0c;我们要构建一个数据看板系统。在这个过程中&#xff0c;我们会利用 MySQL 来存储数据&#xff0c;使用 Python 搭建后端 API&#xff0c;还会借助 Chart.js 在前端呈现各式各样的图表。 整个流程涵盖多个环节&#xff0c;首先要进行数据库表的设计&#xff0c…...

Android ANR 监控方法与事件分发耗时优化实战

一、ANR 监控方法 &#xff08;一&#xff09;系统日志分析 系统日志始终是查找 ANR 根源的重要依据。利用日志分析&#xff0c;不仅可以锁定 ANR 发生的精确时刻&#xff0c;还能追踪到主线程、关键函数调用的阻塞细节。 日志关键词检索&#xff1a;利用 ADB 命令&#xff…...

【蓝桥杯单片机】第十一届省赛

一、真题 二、创建工程 1.在C盘以外的盘新建文件夹&#xff0c;并在文件夹里面创建两个文件夹Driver 和Project 2.打开keil软件&#xff0c;在新建工程并选择刚刚建好的project文件夹&#xff0c;以准考证号命名 3.选择对应的芯片型号 4.选择否&#xff0c;即不创建启动文件 …...

【ES6】模块化

概述 模块功能主要有两个命令&#xff0c;export和import。 一个js文件就是一个模块。 参考视频 【一小时速通JavaScript模块化&#xff0c;涵盖CommonJS与ES6模块化-哔哩哔哩】 https://b23.tv/gZ1uK7V 导出成员 在正常变量、函数前加export关键字。 导入模块 在另一个…...

C++学习——顺序表(六)

文章目录 前言一、找到数组的中间位置二、有序数组中的单一元素三、杨辉三角&#xff08;Ⅱ&#xff09;四、超过阈值的最小操作数Ⅰ五、找出峰值六、统计已测试设备七、统计和小于目标的下标对数目1.单向遍历法2.双指针法&#xff08;时间复杂度小&#xff09; 八、计算K置位下…...

python迭代器生成器

迭代器生成器区别 通俗版概念 ​迭代器&#xff08;Iterator&#xff09;​ ​像“快递员送快递”​&#xff1a; 你有一个包裹清单&#xff08;比如Excel里的测试用例&#xff09;&#xff0c;快递员&#xff08;迭代器&#xff09;会按顺序一个一个送&#xff08;遍历&#x…...

Hive SQL 精进系列:字符串拼接的三种常用方式

Hive字符串拼接&#xff1a;三种常用方式深度剖析 目录 Hive字符串拼接&#xff1a;三种常用方式深度剖析引言一、简洁直观的||操作符1. 基础语法规则2. 丰富多样的示例展示3. 优势与局限分析 二、规范通用的CONCAT函数1. 全面的语法解析2. 生动的示例说明3. 优势与局限剖析 三…...

MATLAB—从入门到精通的第二天

在第一天的学习中&#xff0c;我们掌握了 MATLAB 的安装配置、基础语法、变量管理和运算符的使用。本文将深入讲解 控制结构&#xff08;嵌套 if、switch&#xff09;、循环类型 和 向量操作&#xff0c;帮助读者进一步掌握 MATLAB 的核心编程技能。 1. 条件语句进阶 1.1 嵌套…...

韦伯望远镜的拉格朗日点计算推导过程,包含MATLAB和python运动轨迹仿真代码

研究过程 起源与提出&#xff1a;1687 年牛顿提出 “三体问题”&#xff0c;旨在研究三个可视为质点的天体在相互之间万有引力作用下的运动规律&#xff0c;但因运动方程过于复杂&#xff0c;难以得到完全解。欧拉的贡献1&#xff1a;1767 年&#xff0c;瑞士数学家莱昂哈德・…...

【 现代后端架构演进:微服务设计与云原生】

现代后端架构演进&#xff1a;微服务设计与云原生 一、架构演进历程 1. 单体架构到分布式系统 单体架构瓶颈 典型问题&#xff1a;代码耦合&#xff08;代码行超百万级&#xff09;、扩展困难&#xff08;垂直扩容成本 > 1 0 5 >10^5 >105美元/节点&#xff09;、技术…...

[JAVASE] 注解

一. 注解是什么? 注解是一种为程序元素提供元数据的方法.注解就是为程序做特殊标记的. 二. java内置的注解 分别是: 作用在代码的注解是: Override - 检查该方法是否是重写方法。如果发现其父类&#xff0c;或者是引用的接口中并没有该方法时&#xff0c;会报编译错误。 De…...

热成像仪真不错

我挂在外面的网路设备箱 室内的机柜 室外的猫 所用型号为优利德UTi160S&#xff0c;显示模式为&#xff08;可见光与热成像&#xff09;融合模式。...

Vue-Virtual-Scroller虚拟滚动

前端优化不可不避的一谈之虚拟滚动&#xff1a;众所周知&#xff0c;滚动是直挺挺的往dom树加东西&#xff0c;如果滚太多滚到万级&#xff0c;渲染过多就会卡顿&#xff0c;而vue-virtual-scroll的灵活懒渲染就能解决这个问题 1&#xff0c;下载与配置 npm install --save v…...

Matlab:矩阵运算篇——矩阵

目录 1.定义 实例——创建矩阵 实例——创建复数矩阵 2.矩阵的生成 实例——M文件矩阵 2.利用文本创建 实例——创建生活用品矩阵 3.创建特殊矩阵 实例——生成特殊矩阵 4.矩阵元素的运算 1.矩阵元素的修改 实例——新矩阵的生成 2.矩阵的变维 实例——矩阵维度修…...

[Java]使用java进行JDBC编程

首先要从中央仓库下载api(类似驱动程序)&#xff0c;为了链接java和mysql 下载jar包&#xff0c;需要注意的是jar包的版本要和mysql保持一致 下面是新建文件夹lib&#xff0c;把jar包放进去&#xff0c;并添加为库 sql固定的情况下运行 import com.mysql.cj.jdbc.MysqlDataSo…...

HippoRAG 2 原理精读

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 整体流程离线索引阶段在线检索和问答阶段 总结 整体流程 从上图可以看出&#xff0c;整个流程分为两个阶段 1、离线索引阶段 2、在线检索和问答阶段 离线索引阶段…...

HTTPS协议原理:在Linux世界里的加密冒险

大家好&#xff0c;欢迎来到这次奇妙的HTTPS协议探险之旅&#xff01;今天&#xff0c;我们将一起潜入Linux的深处&#xff0c;揭开HTTPS协议那神秘而迷人的面纱。别担心&#xff0c;即使你是技术小白&#xff0c;也能在这场冒险中找到乐趣和收获。想象一下&#xff0c;你是一位…...