江門恒陽網(wǎng)站建設(shè)百度推廣首次開戶需要多少錢
前言
1、C#實(shí)現(xiàn)本地AI聊天功能
WPF+OllamaSharpe實(shí)現(xiàn)本地聊天功能,可以選擇使用Deepseek 及其他模型。
2、此程序默認(rèn)你已經(jīng)安裝好了Ollama。
在運(yùn)行前需要線安裝好Ollama,如何安裝請自行搜索
Ollama下載地址: https://ollama.org.cn
Ollama模型下載地址: https://ollama.org.cn/library
基本運(yùn)行環(huán)境: 根據(jù)自己使用的AI搜索對應(yīng)模型基本配置,有需要使用GPU運(yùn)行的模型。
此程序除了安裝Ollama外,無需安裝其他配置。
.
3、相關(guān)依賴
OllamaSharpe:啟用本地Ollama服務(wù)
Markdig.wpf : Markdown格式化輸出功能。
Microsoft.Xaml.Behaviors.Wpf :解決部分不能進(jìn)行命令綁定的控件實(shí)現(xiàn)命令綁定功能。
運(yùn)行
項(xiàng)目
項(xiàng)目結(jié)構(gòu)
項(xiàng)目結(jié)構(gòu)包含如下目錄:
.
Commands: 用于命令綁定
Models : 視圖對應(yīng)的模型
Services :一些操作服務(wù)
ViewModels:視圖模型,主要的業(yè)務(wù)處理
Views :視圖以及一些視圖控件的樣式資源
具體如下圖:
項(xiàng)目代碼
Commands
EventsCommand
using System.Windows.Input;
/// <summary>
/// 事件命令:
/// 有些控件的無法綁定命令,但是想要實(shí)現(xiàn)命令綁定功能,可通過創(chuàng)建該命令實(shí)現(xiàn)。
/// 需要引用Microsoft.Xaml.Behaviors.Wpf組合實(shí)現(xiàn)。
/// </summary>
public class EventsCommand<T> : ICommand
{private readonly Action<T> _execute;private readonly Func<T, bool> _canExecute;public EventsCommand(Action<T> execute, Func<T, bool> canExecute = null){_execute = execute ?? throw new ArgumentNullException(nameof(execute));_canExecute = canExecute;}public bool CanExecute(object parameter){return _canExecute?.Invoke((T)parameter) ?? true;}public void Execute(object parameter){_execute((T)parameter);}public event EventHandler CanExecuteChanged{add { CommandManager.RequerySuggested += value; }remove { CommandManager.RequerySuggested -= value; }}
}
ParameterCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{/// <summary>/// 參數(shù)命令:/// 可以帶參數(shù)的命令:/// </summary>public class ParameterCommand : ICommand{public Action<object> execute;public ParameterCommand(Action<object> execute){this.execute = execute;}public event EventHandler? CanExecuteChanged;public bool CanExecute(object? parameter){return CanExecuteChanged != null;}public void Execute(object? parameter){execute?.Invoke(parameter);}}
}
ParameterlessCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{/// <summary>/// 無參數(shù)命令:/// 無參數(shù)的命令:/// </summary>public class ParameterlessCommand : ICommand{private Action _execute;public ParameterlessCommand(Action execute){_execute = execute;}public event EventHandler? CanExecuteChanged;public bool CanExecute(object? parameter){return CanExecuteChanged != null;}public void Execute(object? parameter){_execute.Invoke();}}
}
Models
ChatRecordModel
namespace OfflineAI.Models
{/// <summary>/// 聊天記錄模型/// </summary>public class ChatRecordModel{public ChatRecordModel(int id, string dateTime, string name,string fullName, string data){Id = id;DateTime = dateTime;Name = name;FullName = fullName;Data = data;}/// <summary>/// ID/// </summary>public int Id { get; set; }/// <summary>/// 日期/// </summary>public string DateTime { get; set; }/// <summary>/// 名稱/// </summary>public string Name { get; set; }/// <summary>/// 完整名稱/// </summary>public string FullName { get; set; }/// <summary>/// 數(shù)據(jù)/// </summary>public string Data { get; set; }}
}
FileOperationModel
namespace OfflineAI.Models
{public class FileOperationModel{/// <summary>/// 是否生成目錄/// </summary>public bool IsGenerateDirectory { get; set; }/// <summary>/// 文件目錄/// </summary>public string Directory { get; set; }/// <summary>/// 日期目錄(生成的目錄)/// </summary>public string DirectoryDateTime { get; set; }/// <summary>/// 文件名稱(全路徑)/// </summary>public string FileName { get; set; }/// <summary>/// 文件名稱(生成文件全路徑)/// </summary>public string FileNameDateTime { get; set; }}
}
Services
FileOperation
using OfflineAI.Models;
using System.IO;
namespace OfflineAI.Services
{/// <summary>/// 文件操作類:/// 1、2025-02-24:添加創(chuàng)建日期目錄方法。輸入文件名,添加時(shí)間目錄。/// 2、2025-02-24:添加寫入數(shù)據(jù)到文件方法(.txt格式)/// </summary>public class FileOperation{private FileOperationModel _fileOperation;#region 構(gòu)造函數(shù)public FileOperation(string fileName){_fileOperation = new FileOperationModel();_fileOperation.IsGenerateDirectory = true;UpdataFileName(fileName);}#endregion#region 公共方法/// <summary>/// 更新文件名/// </summary>public void UpdataFileName(string fileName){if (Path.GetExtension(fileName).ToLower().Equals("txt"))_fileOperation.FileName = fileName;else_fileOperation.FileName = fileName + ".txt";_fileOperation.Directory = Path.GetDirectoryName(fileName);CreateDateTime();_fileOperation.FileNameDateTime = $"{_fileOperation.DirectoryDateTime}\\{Path.GetFileName(_fileOperation.FileName)}";}/// <summary>/// 寫入文本/// </summary>public void WriteTxt(string data){SaveDataAsTxt(data);}/// <summary>/// 寫入文本,指定文件名/// </summary>public void WriteTxt(string fileName, string data){UpdataFileName(fileName);SaveDataAsTxt(data);}public string ReadTxt(string fileName){// 使用 using 語句確保資源被正確釋放using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))using (StreamReader sr = new StreamReader(fs)){return sr.ReadToEnd();}}/// <summary>/// 獲取指定目錄下的所有文件(*.txt)/// </summary>public string[] GetFiles(){string[] files = Directory.GetFiles(_fileOperation.Directory, "*.txt", SearchOption.AllDirectories);return files;}/// <summary>/// 獲取指定目錄下的所有文件(*.txt)/// </summary>public static string[] GetFiles(string directory){string[] files = Directory.GetFiles(directory, "*.txt", SearchOption.AllDirectories);return files;}#endregion#region 私有方法/// <summary>/// 保存數(shù)據(jù)為Txt類型的文本/// </summary>private void SaveDataAsTxt(string data){if (_fileOperation.IsGenerateDirectory){try{string fileName = _fileOperation.FileName;if (_fileOperation.IsGenerateDirectory){fileName = _fileOperation.FileNameDateTime;}using (FileStream fileStream = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)){using (StreamWriter writer = new StreamWriter(fileStream)){writer.Write(data);}}Console.WriteLine("數(shù)據(jù)已成功寫入文件。");}catch (Exception ex){Console.WriteLine("寫入文件時(shí)發(fā)生錯(cuò)誤: " + ex.Message);}}}/// <summary>/// 創(chuàng)建日期目錄/// </summary>private void CreateDateTime(){if (_fileOperation.IsGenerateDirectory){string path = $"{_fileOperation.Directory}\\{DateTime.Now.ToString("yyyy")}";Directory.CreateDirectory($"{path}");path = $"{path}\\{DateTime.Now.ToString("yyyyMMdd")}\\";Directory.CreateDirectory($"{path}");_fileOperation.DirectoryDateTime = path;}}#endregion}
}
ProcessService
using System.ComponentModel;
using System.Diagnostics;
namespace OfflineAI.Services
{public class ProcessService{/// <summary>/// 執(zhí)行CMD指令/// </summary>public static bool ExecuteCommand(string command){// 創(chuàng)建一個(gè)新的進(jìn)程啟動(dòng)信息ProcessStartInfo processStartInfo = new ProcessStartInfo{FileName = "cmd.exe", // 設(shè)置要啟動(dòng)的程序?yàn)閏md.exeArguments = $"/C {command}", // 設(shè)置要執(zhí)行的命令UseShellExecute = true, // 使用操作系統(tǒng)shell啟動(dòng)進(jìn)程CreateNoWindow = false, //不創(chuàng)建窗體};try{Process process = Process.Start(processStartInfo);// 啟動(dòng)進(jìn)程process.WaitForExit(); // 等待進(jìn)程退出process.Close(); // 返回是否成功執(zhí)行return process.ExitCode == 0;}catch (Exception ex){Debug.WriteLine($"發(fā)生錯(cuò)誤: {ex.Message}");// 其他異常處理return false;}}}
}
ShareOllamaObject
using OfflineAI.Services;
using OllamaSharp;
using System.Collections.ObjectModel;namespace OfflineAI.Sevices
{/// <summary>/// 共享Ollama對象類:保持Ollama對象一致才能使用當(dāng)前對象實(shí)現(xiàn)對話/// 作 者:吾與誰歸/// 時(shí) 間:2025年02月18日/// 功 能:/// 1) 2025-02-18:使用cmd命令啟動(dòng)Ollama服務(wù),目前使用ollama list();/// 2) 2025-02-18:初始化模型參數(shù),在初始化時(shí)啟用GPU、連接ollama、初始化模型。/// </summary>public class ShareOllamaObject{#region 字段|屬性|集合#region 字段private bool _connected = false; //連接狀態(tài)private Chat chat; //構(gòu)建交互式聊天模型對象。private OllamaApiClient _ollama; //OllamaAPI對象private string _selectModel; //選擇的模型名稱#endregion#region 屬性/// <summary>/// 連接狀態(tài)/// </summary>public bool Connected{get { return _connected; }set { _connected = value; }}public string SelectModel { get => _selectModel; set => _selectModel = value; }/// <summary>/// 構(gòu)建交互式聊天模型對象。/// </summary>public Chat Chat{get { return chat; }set { chat = value; }}/// <summary>/// OllamaAPI對象/// </summary>public OllamaApiClient Ollama{get { return _ollama; }set { _ollama = value; }}#endregion#region 集合/// <summary>/// 模型列表/// </summary>public ObservableCollection<string> ModelList { get; set; }#endregion#endregion#region 構(gòu)造函數(shù)public ShareOllamaObject(){ProcessService.ExecuteCommand("ollama list");Initialize("llama3.2:3b");ProcessService.GetProcessId("ollama");}#endregion#region 其他方法/// <summary>/// 初始化方法/// </summary>private void Initialize( string modelName){try{// 設(shè)置默認(rèn)設(shè)備為GPUEnvironment.SetEnvironmentVariable("OLLAMA_DEFAULT_DEVICE", "gpu");//連接Ollama,并設(shè)置初始模型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"));//設(shè)置的模型不為空if (tmepModelName != null){Ollama.SelectedModel = tmepModelName;}//模型列表不為空else if (ModelList.Count > 0){_ollama.SelectedModel = ModelList[ModelList.Count - 1];}//Ollama服務(wù)啟用成功SelectModel = _ollama.SelectedModel;_connected = true;chat = new Chat(_ollama);}catch (Exception){_connected = false; //Ollama服務(wù)啟用失敗}}/// <summary>/// 獲取模型里列表/// </summary>public Collection<string> GetModelList(){var models = _ollama.ListLocalModelsAsync();var modelList = new ObservableCollection<string>();foreach (var model in models.Result){modelList.Add(model.Name);}return modelList;}public void ReCreateChat(){chat = new Chat(_ollama);}#endregion}
}
ViewModels
MainViewModel
using OfflineAI.Sevices;
using OfflineAI.Commands;
using OfflineAI.Views;
using System.Windows;
using System.Diagnostics;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.IO;
using OfflineAI.Services;
using OfflineAI.Models;
namespace OfflineAI.ViewModels
{/// <summary>/// 主窗體視圖模型:/// 作者:吾與誰歸/// 時(shí)間:2025年02月17日(首次創(chuàng)建時(shí)間)/// 更新: /// 1、2025-02-17:添加折疊欄展開|折疊功能。/// 2、2025-02-17:視圖切換功能 1)系統(tǒng)設(shè)置 2) 聊天/// 3、2025-02-18:關(guān)閉窗體時(shí)提示是否關(guān)閉,釋放相關(guān)資源。/// 4、2025-02-19:添加首頁功能,和修改新聊天功能。點(diǎn)擊新聊天會(huì)創(chuàng)建新的會(huì)話(Chat)。/// 5、2025-02-20:窗體加載時(shí)傳遞Ollama對象。/// 6、2025-02-24:添加了窗體加載時(shí),加載聊天記錄的功能。/// </summary>public class MainViewModel : PropertyChangedBase{#region 字段、屬性、集合、命令#region 字段private UserControl _currentView; //當(dāng)前視圖private ShareOllamaObject _ollamaService; //共享Ollama服務(wù)對象private string _selectedModel; //選擇的模型private ObservableCollection<string> _modelListCollection; //模型列表private int _expandedBarWidth = 50; //折疊欄寬度private string _directory; //目錄private string _fileName; //文件private ObservableCollection<ChatRecordModel> _chatRecordCollection;public event Action<string> LoadChatRecordEventHandler;#endregion#region 屬性/// <summary>/// 當(dāng)前顯示視圖/// </summary>public UserControl CurrentView { get => _currentView;set{if (_currentView != value){_currentView = value;OnPropertyChanged();}}}public ShareOllamaObject OllamaService{get => _ollamaService;set{if (_ollamaService != value){_ollamaService = value;OnPropertyChanged();}}}public string SelectedModel { get => _selectedModel;set{if (_selectedModel != value){_selectedModel = value;OllamaService.Ollama.SelectedModel = value;OllamaService.Chat.Model = value;OnPropertyChanged();}}}public int ExpandedBarWidth{get => _expandedBarWidth;set{if (_expandedBarWidth != value){_expandedBarWidth = value;OnPropertyChanged();}}}#endregion#region 集合/// <summary>/// 視圖集合,保存視圖/// </summary>public ObservableCollection<UserControl> ViewCollection { get; set; }public ObservableCollection<string> ModelListCollection{get => _modelListCollection;set{if (_modelListCollection != value){_modelListCollection = value;OnPropertyChanged();}}}public ObservableCollection<ChatRecordModel> ChatRecordCollection{get => _chatRecordCollection;set{if (_chatRecordCollection != value){_chatRecordCollection = value;OnPropertyChanged();}}}#endregion#region 命令/// <summary>/// 展開功能菜單命令/// </summary>public ICommand ExpandedMenuCommand { get; set; }/// <summary>/// 折疊功能菜單命令/// </summary>public ICommand CollapsedMenuCommand { get; set; }/// <summary>/// 切換視圖命令/// </summary>public ICommand SwitchViewCommand { get; set; }/// <summary>/// 窗體關(guān)閉命令/// </summary>public ICommand ClosingWindowCommand { get; set; }/// <summary>/// 窗體加載命令/// </summary>public ICommand LoadedWindowCommand { get; set; }/// <summary>/// 聊天記錄鼠標(biāo)按下命令/// </summary>public ICommand ChatRecordMouseDownCommand { get; set; }#endregion#endregion#region 構(gòu)造函數(shù)public MainViewModel(){Initialize();}/// <summary>/// 初始化方法/// </summary>public void Initialize(){//初始化Ollama_ollamaService = new ShareOllamaObject();ModelListCollection = _ollamaService.ModelList;SelectedModel = _ollamaService.SelectModel;//創(chuàng)建命令SwitchViewCommand = new ParameterCommand(SwitchViewTrigger);LoadedWindowCommand = new EventsCommand<object>(LoadedWindowTrigger);CollapsedMenuCommand = new EventsCommand<object>(CollapsedMenuTrigger);ExpandedMenuCommand = new EventsCommand<object>(ExpandedMenuTrigger);ClosingWindowCommand = new EventsCommand<object>(ClosingWindowTrigger);ChatRecordMouseDownCommand = new EventsCommand<ChatRecordModel>(ChatRecordMouseDownTrigger);ViewCollection = new ObservableCollection<UserControl>();//添加視圖到集合ViewCollection.Add(new SystemSettingView());ViewCollection.Add(new UserChatView());//默認(rèn)顯示窗體CurrentView = ViewCollection[1];//折疊欄折疊狀態(tài)ExpandedBarWidth = 25;//加載聊天記錄LoadChatRecord();}#endregion#region 命令方法/// <summary>/// 聊天記錄鼠標(biāo)按下/// </summary>private void ChatRecordMouseDownTrigger(ChatRecordModel obj){Debug.Print(obj.ToString());OnLoadChatRecordCallBack(obj.FullName.ToString());}/// <summary>/// 觸發(fā)主視圖窗體加載方法/// </summary>private void LoadedWindowTrigger(object sender){Debug.Print(sender?.ToString());var userView = ViewCollection.FirstOrDefault(obj => obj is UserChatView) as UserChatView;userView.UserWindow.Ollama = _ollamaService;LoadChatRecordEventHandler += userView.UserWindow.LoadChatRecordCallback;}/// <summary>/// 觸發(fā)關(guān)閉窗體方法/// </summary>private void ClosingWindowTrigger(object obj){if (obj is CancelEventArgs cancelEventArgs){if (MessageBox.Show("確定要關(guān)閉程序嗎?", "確認(rèn)關(guān)閉", MessageBoxButton.YesNo) == MessageBoxResult.No){cancelEventArgs.Cancel = true; // 取消關(guān)閉}else{ClearingResources();}}}/// <summary>/// 視圖切換命令觸發(fā)的方法/// </summary>private void SwitchViewTrigger(object obj){Debug.WriteLine(obj.ToString());switch (obj.ToString()){case "SystemSettingView":CurrentView = ViewCollection[0];break;case "UserChatView":CurrentView = ViewCollection[1];break;case "NewUserChatView":UserChatView newChatView = new UserChatView();OllamaService.ReCreateChat();newChatView.UserWindow.Ollama = OllamaService;ViewCollection[1] = newChatView;CurrentView = newChatView;break;}}/// <summary>/// 折疊菜單觸發(fā)方法/// </summary>private void CollapsedMenuTrigger(object e){ExpandedBarWidth = 25;Debug.WriteLine("折疊");}/// <summary>/// 展開菜單觸發(fā)方法/// </summary>private void ExpandedMenuTrigger(object e){ExpandedBarWidth = 250;Debug.WriteLine("展開");}#endregion#region 其他方法/// <summary>/// 加載聊天記錄/// </summary>private void LoadChatRecord(){_directory = $"{Environment.CurrentDirectory}\\Record";string[] files = FileOperation.GetFiles(_directory);ObservableCollection<ChatRecordModel> records = new ObservableCollection<ChatRecordModel>();string name = string.Empty;string data = string.Empty;foreach (var item in files){name = Path.GetFileNameWithoutExtension(item);data = File.ReadAllLines(item)[3];if (data.Trim().Length > 1 ){records.Add(new ChatRecordModel(records.Count, name, name, item, data.Substring(1)));}}ChatRecordCollection = records;}/// <summary>/// 觸發(fā)事件:加載聊天記錄回調(diào)/// </summary>private void OnLoadChatRecordCallBack(object sender){LoadChatRecordEventHandler.Invoke(sender.ToString());}/// <summary>/// 釋放資源:窗體關(guān)閉時(shí)觸發(fā)/// </summary>private void ClearingResources(){//ProcessService.GetPIDAndCloseByPort(11434);}#endregion}
}
PropertyChangedBase
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace OfflineAI.ViewModels
{/// <summary>/// 屬性變更基類/// </summary>public class PropertyChangedBase : INotifyPropertyChanged{public event PropertyChangedEventHandler? PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}
UserChatViewModel
using Markdig.Wpf;
using OfflineAI.Commands;
using OfflineAI.Services;
using OfflineAI.Sevices;
using System.Diagnostics;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
namespace OfflineAI.ViewModels
{/// <summary>/// 描述:用戶聊天視圖模型:/// 作者:吾與誰歸/// 時(shí)間: 2025年2月19日/// 更新:/// 1、 2025-02-19:添加AI聊天功能,輸出問題及結(jié)果到UI,并使用Markdown相關(guān)的庫做簡單渲染。/// 2、 2025-02-20:優(yōu)化了構(gòu)造函數(shù),使用無參構(gòu)造,方便在設(shè)計(jì)器中直接綁定數(shù)據(jù)上下文(感覺)。/// 3、 2025-02-20:滾輪滑動(dòng)顯示內(nèi)容,提交問題后滾動(dòng)顯示內(nèi)容,鼠標(biāo)右鍵點(diǎn)擊內(nèi)容停止繼續(xù)滾動(dòng),回答結(jié)束停止?jié)L動(dòng)。/// 4、 2025-02-24:添加聊天記錄保存功能。/// 5、 2025-02-24:添加聊天記錄加載功能,通過點(diǎn)擊記錄列表顯示。/// </summary>public class UserChatViewModel:PropertyChangedBase{#region 字段、屬性、集合、命令#region 字段private bool _isAutoScrolling = false; //是否自動(dòng)滾動(dòng)private string _currentInputText; //當(dāng)前輸入文本private string _messageContent; //消息內(nèi)容private string _directory; //目錄private string _fileName; //文件名private MarkdownViewer _markdownViewer; //MarkdownViewer控件private ScrollViewer _scrollViewer; //ScrollViewer滑動(dòng)控件private StringBuilder _message = new StringBuilder(); //消息字符串拼接private CancellationToken cancellationToken; //異步線程取消標(biāo)記private FileOperation _fileIO; //文件IOprivate ShareOllamaObject _ollama; //Ollama 對象實(shí)例private string _submitButtonName;#endregion#region 屬性/// <summary>/// 提交按鈕名稱/// </summary>public string SubmitButtonName{get => _submitButtonName;set{if (_submitButtonName != value){_submitButtonName = value;OnPropertyChanged();}}}/// <summary>/// 消息內(nèi)容/// </summary>public string? MessageContent{get => _messageContent;set{_messageContent = value;OnPropertyChanged();}}/// <summary>/// 當(dāng)前輸入文本/// </summary>public string CurrentInputText{get => _currentInputText;set{if (_currentInputText != value){_currentInputText = value;OnPropertyChanged();}}}/// <summary>/// 共享Ollama對象 /// </summary>public ShareOllamaObject Ollama {get => _ollama;set{if (_ollama != value){_ollama = value;OnPropertyChanged();}}}/// <summary>/// 自動(dòng)滾動(dòng)消息/// </summary>public bool IsAutoScrolling{get => _isAutoScrolling;set{if (_isAutoScrolling != value){_isAutoScrolling = value;OnPropertyChanged();}}}#endregion#region 集合#endregion#region 命令/// <summary>/// 展開功能菜單命令/// </summary>public ICommand LoadFileCommand { get; set; }/// <summary>/// 提交命令/// </summary>public ICommand SubmiQuestionCommand { get; set; }/// <summary>/// 鼠標(biāo)滾動(dòng)/// </summary>public ICommand MouseWheelCommand { get; set; }/// <summary>/// 鼠標(biāo)按下/// </summary>public ICommand MouseDownCommand { get; set; }/// <summary>/// Markdown對象命令/// </summary>public ICommand MarkdownOBJCommand { get; set; }/// <summary>/// 滑動(dòng)條加載/// </summary>public ICommand ScrollLoadedCommand { get; set; }#endregion#endregion#region 構(gòu)造函數(shù)public UserChatViewModel(){Initialize();}#endregion#region 初始化方法/// <summary>/// 初始化方法/// </summary>public void Initialize(){//文件加載LoadFileCommand = new ParameterCommand(LoadFileTrigger);MouseWheelCommand = new EventsCommand<MouseWheelEventArgs>(MouseWheelTrigger);MouseDownCommand = new EventsCommand<MouseButtonEventArgs>(MouseDownTrigger);MarkdownOBJCommand = new EventsCommand<object>(MarkdownOBJTrigger);SubmiQuestionCommand = new ParameterlessCommand(SubmitQuestionTrigger);ScrollLoadedCommand = new EventsCommand<RoutedEventArgs>(ScrollLoadedTrigger);//SubmitButtonName = "提交";//日志記錄_directory = $"{Environment.CurrentDirectory}\\Record\\";_fileName = $"{_directory}\\{DateTime.Now.ToString("yyyyMMddHHmmss")}";_fileIO = new FileOperation($"{_fileName}");//}#endregion#region 命令方法/// <summary>/// 加載文件/// </summary>private void LoadFileTrigger(object obj){OpenFileDialog openFile = new OpenFileDialog();openFile.Multiselect = true;if (openFile.ShowDialog() == DialogResult.OK){string[] files = openFile.FileNames;if (files.Count() > 1){foreach (var item in files){Debug.WriteLine(item);}}else{Debug.WriteLine(openFile.FileName);}}}/// <summary>/// 提交: 提交問題到AI并獲取返回結(jié)果/// </summary>private async void SubmitQuestionTrigger(){_ = Task.Delay(1);string input = CurrentInputText;try{if (!SubmintChecked(input)) return; SubmitButtonName = "停止";_message.Clear();_isAutoScrolling = true;AppendText($"##{Environment.NewLine}");AppendText($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}]{Environment.NewLine}");AppendText($"## 【User】{Environment.NewLine}");AppendText($">{input}{Environment.NewLine}");AppendText($"{Environment.NewLine}");AppendText($"## 【AI】{Environment.NewLine}");await foreach (var answerToken in Ollama.Chat.SendAsync(input)){AppendText(answerToken);await Task.Delay(20);if (_isAutoScrolling) _scrollViewer.ScrollToEnd();//是否自動(dòng)滾動(dòng)}AppendText($"{Environment.NewLine}{Environment.NewLine}");}catch (Exception ex){AppendText($"Error: {ex.Message}");AppendText($"{Environment.NewLine}{Environment.NewLine}");}//回答完成_fileIO.WriteTxt($"{_fileName}", _message.ToString());CurrentInputText = string.Empty;_isAutoScrolling = false;SubmitButtonName = "提交";}/// <summary>/// 鼠標(biāo)滾動(dòng)上下滑動(dòng)/// </summary>private void MouseWheelTrigger(MouseWheelEventArgs e){try{// 獲取 ScrollViewer 對象if (e.Source is FrameworkElement element && element.Parent is ScrollViewer scrollViewer){// 獲取當(dāng)前的垂直偏移量double currentOffset = scrollViewer.VerticalOffset;if (e.Delta > 0){scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);}else{scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);}// 標(biāo)記事件已處理,防止默認(rèn)滾動(dòng)行為e.Handled = true;}}catch (Exception ex){Debug.Print(ex.Message);}}/// <summary>/// Markdown中鼠標(biāo)按下/// </summary>private void MouseDownTrigger(MouseButtonEventArgs args){if (args.LeftButton == MouseButtonState.Pressed){IsAutoScrolling = false;Debug.Print("Mouse Down...");}}/// <summary>/// 滾動(dòng)欄觸發(fā)/// </summary>private void ScrollLoadedTrigger(RoutedEventArgs args){if (args.Source is ScrollViewer scrollView ){_scrollViewer = scrollView;Debug.Print("Scroll loaded...");}}/// <summary>/// Markdown控件對象更新觸發(fā)/// </summary>private void MarkdownOBJTrigger(object obj){if (_markdownViewer != null) return;if (obj is MarkdownViewer markdownViewer){_markdownViewer = markdownViewer;_markdownViewer.Markdown = "";}}#endregion#region 其他方法/// <summary>/// 輸出文本/// </summary>public void AppendText(string newText){Debug.Print(newText);_markdownViewer.Markdown += newText;_message.Append(newText);}/// <summary>/// 提交校驗(yàn)/// </summary>private bool SubmintChecked(string input){if (string.IsNullOrEmpty(input)) return false;if (input.Length<2) return false;if (input.Equals("停止")) return false;return true;}#endregion#region 回調(diào)方法/// <summary>/// 加載聊天記錄回調(diào)/// </summary>public void LoadChatRecordCallback(string path){Debug.Print(path);_scrollViewer.ScrollToTop();_markdownViewer.Markdown = _fileIO. ReadTxt(path);}#endregion}
}
Views
UserChatView
<UserControl x:Class="OfflineAI.Views.UserChatView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"xmlns:local="clr-namespace:OfflineAI.Views"xmlns:markdig ="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels"mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"HorizontalAlignment="Stretch" VerticalAlignment="Stretch"><!--綁定數(shù)據(jù)上下文--><UserControl.DataContext><viewmodels:UserChatViewModel x:Name="UserWindow"/></UserControl.DataContext><Grid><!--命令綁定事件:窗體加載時(shí)傳參數(shù)Markdown控件對象。在Grid中創(chuàng)建,否則會(huì)出現(xiàn)null異常--><behavior:Interaction.Triggers><behavior:EventTrigger EventName="Loaded"><behavior:InvokeCommandAction Command="{Binding MarkdownOBJCommand}"CommandParameter="{Binding ElementName=MarkdownContent}"/></behavior:EventTrigger></behavior:Interaction.Triggers><!--定義行--><Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="300"/></Grid.RowDefinitions><!--行背景色--><Border Grid.Row="0" Background="#FFFFFF"/><Border Grid.Row="1" Background="#5E5E5E"/><Grid><!--markdown 滑動(dòng)條--><ScrollViewer Background="#AEAEAE"x:Name="MarkDownScrollViewer"><behavior:Interaction.Triggers><behavior:EventTrigger EventName="Loaded"><behavior:InvokeCommandAction Command="{Binding ScrollLoadedCommand}"PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers><!--markdown--><markdig:MarkdownViewerName="MarkdownContent"><!--命令綁定事件:鼠標(biāo)滾動(dòng)顯示內(nèi)容--><behavior:Interaction.Triggers><!--鼠標(biāo)滾動(dòng)命令事件--><behavior:EventTrigger EventName="PreviewMouseWheel"><behavior:InvokeCommandAction Command="{Binding MouseWheelCommand}"PassEventArgsToCommand="True"/></behavior:EventTrigger><!--鼠標(biāo)點(diǎn)擊命令事件--><behavior:EventTrigger EventName="PreviewMouseDown"><behavior:InvokeCommandAction Command="{Binding MouseDownCommand}"PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers></markdig:MarkdownViewer></ScrollViewer></Grid><!--第三行內(nèi)容:顯示回話內(nèi)容--><Grid Grid.Row="1" Margin="2"><!--定義三行--><Grid.RowDefinitions><RowDefinition Height="25"/><RowDefinition Height="*"/><RowDefinition Height="30"/></Grid.RowDefinitions><!--設(shè)置Border樣式--><Border Grid.Row="0" Margin="150,0,150,0" Background="#5E5E5E"><Border.BorderThickness>2,2,2,0</Border.BorderThickness><Border.BorderBrush><SolidColorBrush Color="#000000"/></Border.BorderBrush></Border><Border Grid.Row="1" Margin="150,0,150,0" Background="#5E5E5E"><Border.BorderThickness>2,0,2,0</Border.BorderThickness><Border.BorderBrush><SolidColorBrush Color="#000000"/></Border.BorderBrush></Border><Border Grid.Row="2" Margin="150,0,150,0" Background="#5E5E5E"><Border.BorderThickness>2,0,2,2</Border.BorderThickness><Border.BorderBrush><SolidColorBrush Color="#000000"/></Border.BorderBrush></Border><!--第2行內(nèi)容區(qū)域--><Grid Grid.Row="1" Margin="150,0,150,0"><TextBox x:Name="InputBox" Background="#5E5E5E"Text="{Binding CurrentInputText , Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" Margin="5" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"><!--回車發(fā)送--><TextBox.InputBindings><KeyBinding Command="{Binding SubmiQuestionCommand}" Key="Enter"/></TextBox.InputBindings></TextBox></Grid><!--第3行內(nèi)容區(qū)域--><Grid Grid.Row="2" Margin="150,0,150,0"><WrapPanel HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0"><Button Width="50" Command="{Binding LoadFileCommand}"><Image Width="24" Height="24"Source="/Views/Resources/append24-black.png" HorizontalAlignment="Right" VerticalAlignment="Center"/></Button><Button Width="50" Command="{Binding SubmiQuestionCommand}" Content="{Binding SubmitButtonName}"></Button></WrapPanel></Grid></Grid></Grid>
</UserControl>
SystemSettingView
<UserControl x:Class="OfflineAI.Views.SystemSettingView"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:OfflineAI.Views"xmlns:viewModels="clr-namespace:OfflineAI.ViewModels"mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"HorizontalAlignment="Stretch" VerticalAlignment="Stretch"><Grid><StackPanel Background="#FFFFFF" Margin="5"><TextBox FontSize="36" IsReadOnly="True"HorizontalContentAlignment="Center" VerticalContentAlignment="Center">系統(tǒng)設(shè)置</TextBox><CheckBox Width="200" Margin="5" HorizontalAlignment="Left" IsChecked="True">是否滾動(dòng)顯示</CheckBox><ComboBox Width="200" Margin="5" HorizontalAlignment="Left"></ComboBox></StackPanel></Grid>
</UserControl>
Styles \ ButtonStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"><!-- 定義圓角按鈕的靜態(tài)樣式 --><Style x:Key="RoundCornerButtonStyle" TargetType="Button"><Setter Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#04D3F2" Offset="0.6" /><GradientStop Color="#FFAB0D" Offset="2.8" /></LinearGradientBrush></Setter.Value></Setter><Setter Property="BorderBrush" Value="DarkGray"/><Setter Property="BorderThickness" Value="0"/><Setter Property="Padding" Value="5"/><Setter Property="Margin" Value="10"/><Setter Property="Width" Value="60"/><Setter Property="Height" Value="20"/><!--設(shè)置模板樣式--><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><!--使用 Border 元素作為按鈕的主要容器。 roundedRectangle:名稱,方便在觸發(fā)器中引用。Background:綁定背景色到按鈕的 Background 屬性。BorderBrush:綁定邊框顏色到按鈕的 BorderBrush 屬性。BorderThickness:綁定邊框?qū)挾鹊桨粹o的 BorderThickness 屬性。CornerRadius:設(shè)置邊框的圓角半徑為10,使按鈕具有圓角效果。ContentPresenter:用于顯示按鈕的內(nèi)容(如文本或圖標(biāo))。--><Border x:Name="roundedRectangle" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="10"><!-- 設(shè)置頂部圓角 --> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/></Border><ControlTemplate.Triggers><!-- 鼠標(biāo)懸停時(shí) --><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><!-- 按鈕被按下時(shí) --><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><!-- 定義帶圖標(biāo)的按鈕的靜態(tài)樣式 --><Style x:Key="IconButtonStyle" TargetType="Button"><Setter Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#AED3D2" Offset="0.3" /><!-- 淡色 --><GradientStop Color="#F0FBFF" Offset="0.7" /><!-- 深色 --></LinearGradientBrush></Setter.Value></Setter><Setter Property="BorderBrush" Value="DarkGray"></Setter><Setter Property="BorderThickness" Value="0"></Setter><Setter Property="Padding" Value="5"></Setter><Setter Property="Margin" Value="5 5 5 5"></Setter><Setter Property="FontSize" Value="20"></Setter><!-- 調(diào)整寬度以適應(yīng)圖標(biāo)和文本 --><Setter Property="Height" Value="50"></Setter><!-- 調(diào)整高度以適應(yīng)圖標(biāo)和文本 --><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Border x:Name="roundedRectangle" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="10"><!-- 使用 StackPanel 來布局圖標(biāo)和文本 --><StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"><ContentPresenter Content="{TemplateBinding Content}" /></StackPanel></Border><ControlTemplate.Triggers><!-- 鼠標(biāo)懸停時(shí) --><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><!-- 按鈕被按下時(shí) --><Trigger Property="IsPressed" Value="True"><Setter TargetName="roundedRectangle" Property="Background"><Setter.Value><LinearGradientBrush StartPoint="0,0" EndPoint="1,0"><GradientStop Color="#D68B8B" Offset="0.4" /><GradientStop Color="#A05252" Offset="0.7" /></LinearGradientBrush></Setter.Value></Setter></Trigger></ControlTemplate.Triggers></ControlTemplate></Setter.Value></Setter></Style>
</ResourceDictionary>
MainWindow
<Window x:Class="OfflineAI.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"xmlns:local="clr-namespace:OfflineAI"xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels" WindowStartupLocation="CenterScreen"mc:Ignorable="d"Title="ChatAI" Height="800" Width="1000"Icon="/Views/Resources/app-logo128.ico"MinHeight="600" MinWidth="800"><!--綁定上下文--><Window.DataContext><viewmodels:MainViewModel></viewmodels:MainViewModel></Window.DataContext><!--樣式資源--><Window.Resources><ResourceDictionary><!--資源字典: 添加控件樣式--><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="Views/Styles/ButtonStyle.xaml"/><ResourceDictionary Source="Views/Styles/ComboBoxStyle.xaml"/></ResourceDictionary.MergedDictionaries></ResourceDictionary></Window.Resources><!--事件命令綁定--><behavior:Interaction.Triggers><!--窗體加載命令綁定--><behavior:EventTrigger EventName="Loaded"><behavior:InvokeCommandAction Command="{Binding LoadedWindowCommand}" PassEventArgsToCommand="True"/></behavior:EventTrigger><!--窗體關(guān)閉命令綁定--><behavior:EventTrigger EventName="Closing"><behavior:InvokeCommandAction Command="{Binding ClosingWindowCommand}" PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers><Grid><!-- 定義3列:--><Grid.ColumnDefinitions><ColumnDefinition Width="auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/></Grid.ColumnDefinitions><!-- 定義2行 --><Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="20"/></Grid.RowDefinitions><!-- 折疊欄 Expander --><Expander x:Name="expanderBox" Grid.Row="0" Grid.Column="0" Header="" Background="#AABBBB" ExpandDirection="Left"IsExpanded="False"FlowDirection="LeftToRight" Width="{Binding ExpandedBarWidth}"><!--命令綁定事件--><behavior:Interaction.Triggers><!--折疊欄展開命令綁定--><behavior:EventTrigger EventName="Expanded"><behavior:InvokeCommandAction Command="{Binding ExpandedMenuCommand}" /></behavior:EventTrigger><!--折疊欄折疊命令綁定--><behavior:EventTrigger EventName="Collapsed"><behavior:InvokeCommandAction Command="{Binding CollapsedMenuCommand}" /></behavior:EventTrigger></behavior:Interaction.Triggers><ScrollViewer Background="#AEAEAE" x:Name="RecordScrollViewer"><ListBox ItemsSource="{Binding ChatRecordCollection}" Margin="5"><ListBox.ItemTemplate><DataTemplate><!-- 顯示消息內(nèi)容 --><TextBlock Text="{Binding Data}" Margin="10,0,0,0"><behavior:Interaction.Triggers><!--鼠標(biāo)點(diǎn)擊命令事件--><behavior:EventTrigger EventName="PreviewMouseDown"><behavior:InvokeCommandActionCommand="{Binding DataContext.ChatRecordMouseDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"CommandParameter="{Binding}"PassEventArgsToCommand="True"/></behavior:EventTrigger></behavior:Interaction.Triggers></TextBlock></DataTemplate></ListBox.ItemTemplate></ListBox></ScrollViewer></Expander><!-- 右側(cè)內(nèi)容區(qū)域 --><Border Background="LightGray" Grid.Row="0" Grid.Column="1" Padding="10"/><!--主要區(qū)域--><Grid Grid.Row="0" Grid.Column="1" Margin="3"><!--定義三行--><Grid.RowDefinitions><RowDefinition Height="50"/><RowDefinition Height="*"/><RowDefinition Height="350"/></Grid.RowDefinitions><!--設(shè)置背景色--><Border Grid.Row="0" Background="#99BBCC"/><Border Grid.Row="1" Background="#FFFFFF" Grid.RowSpan="2"/><!--第一行內(nèi)容:左對齊內(nèi)容--><WrapPanel VerticalAlignment="Center"><!--視圖切換:首頁--><Button x:Name="Btn_HomePage" Width="50" Height="36" FontSize="16"Style="{StaticResource IconButtonStyle}" Command="{Binding SwitchViewCommand}"CommandParameter="UserChatView"><StackPanel Orientation="Horizontal"><Image Source="Views/Resources/home24-black.png"Margin="5" Width="24" Height="24"HorizontalAlignment="Center" VerticalAlignment="Center"/></StackPanel></Button><!--視圖切換:新聊天界面--><Button x:Name="Btn_Chat" Width="100" Height="36" FontSize="16"Style="{StaticResource IconButtonStyle}" Command="{Binding SwitchViewCommand}"CommandParameter="NewUserChatView"><StackPanel Orientation="Horizontal"><Image Source="Views/Resources/edit24-black.png"Margin="5" Width="24" Height="24"HorizontalAlignment="Center" VerticalAlignment="Center"/><TextBlock Text="新聊天" VerticalAlignment="Center"/></StackPanel></Button><!--模型列表--><Label Content="模型:" Margin="5" FontSize="18" VerticalAlignment="Center"/><ComboBox x:Name="Cbx_ModelList" Style="{StaticResource RoundComboBoxStyle}" ItemsSource="{Binding ModelListCollection}"SelectedItem="{Binding SelectedModel}"></ComboBox></WrapPanel><!--第一行內(nèi)容:右對齊內(nèi)容--><WrapPanel Margin="0,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" ><Button Background="#99BBCC" Command="{Binding SwitchViewCommand}"CommandParameter="SystemSettingView"><Image Source="/Views/Resources/setting64.png" Margin="5" Width="24" Height="24"HorizontalAlignment="Right" VerticalAlignment="Center"/></Button></WrapPanel><!--第二行內(nèi)容:顯示當(dāng)前視圖--><ContentControl Grid.Row="1" Margin="5,5,5,5"Content="{Binding CurrentView}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Grid.RowSpan="2"/></Grid></Grid>
</Window>
總結(jié)
以上為項(xiàng)目的全部代碼。
實(shí)現(xiàn)功能:
1、添加折疊欄展開|折疊功能。
2、視圖切換功能 1)系統(tǒng)設(shè)置 2) 聊天
3、關(guān)閉窗體時(shí)提示是否關(guān)閉,釋放相關(guān)資源。
4、添加首頁功能,和修改新聊天功能。點(diǎn)擊新聊天會(huì)創(chuàng)建新的會(huì)話(Chat)。
5、窗體加載時(shí)傳遞Ollama對象。
6、添加了窗體加載時(shí),加載聊天記錄的功能。
7、添加AI聊天功能,輸出問題及結(jié)果到UI,并使用Markdown相關(guān)的庫做簡單渲染。
8、優(yōu)化了構(gòu)造函數(shù),使用無參構(gòu)造,方便在設(shè)計(jì)器中直接綁定數(shù)據(jù)上下文(感覺)。
9、 滾輪滑動(dòng)顯示內(nèi)容,提交問題后滾動(dòng)顯示內(nèi)容,鼠標(biāo)右鍵點(diǎn)擊內(nèi)容停止繼續(xù)滾動(dòng),回答結(jié)束停止?jié)L動(dòng)。
10、添加聊天記錄保存功能。
11、添加聊天記錄加載功能,通過點(diǎn)擊記錄列表顯示。
待完善:
1、使用deepseek r*模型時(shí),控件刷新會(huì)把 的前面的一部分吞掉,使用Debug打印的是完整的問題,初步懷疑是異步刷新UI更不上的問題。
2、想使用Markdown的高級渲染功能使用起來,目前僅是簡單的渲染(有空要做出來)。
3、聊天記錄僅僅是顯示功能,沒有實(shí)現(xiàn)承接聊天記錄回答問題。
4、參考網(wǎng)頁端的功能開發(fā)更多功能。
項(xiàng)目下載地址:https://github.com/timenodes/OfflineAI