添加项目文件。

This commit is contained in:
yangwx
2025-03-12 20:02:52 +08:00
parent 4d34907fa7
commit 3ccd6d9a39
32 changed files with 1833 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
using Markdig.Wpf;
using OllamaSharp;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using System.Windows.Input;
using YwxApp.AiChat.Commands;
namespace YwxApp.AiChat.ViewModels
{
/// <summary>
/// 0、Current class:
/// </summary>
public class ChatMdViewModel : INotifyPropertyChanged
{
#region Field | Property | Collection | Command
#region Field
private string? _inputText; //User input text.
private Chat? chat; //Build interactive chat.
private ShareOllamaObject _ollama; //share Ollama object.
private CancellationTokenSource _cancellationTokenSource; //Termination chat Token
private bool _useExtensions = true; //whether enable Markdown extensions function.
private string _markdownContent; //Markdown context.
private MarkdownViewer markdownViewer; //Markdwon viewer.
private bool _isAutoScrolling = false; //whather enable scroll
private double _textWidth; // MarkdownViewer width
#endregion
#region Property : Support property changed notify.
//InputText
public string? InputText
{
get => _inputText;
set
{
_inputText = value;
OnPropertyChanged();
}
}
public string MarkdownContent
{
get => _markdownContent;
set
{
_markdownContent = value;
// Notify property changed if needed
OnPropertyChanged();
}
}
public double TextWidth
{
get => _textWidth;
set
{
_textWidth = value;
OnPropertyChanged();
}
}
#endregion
#region Collection:
#endregion
#region Command: Builde Command generate response command
public ICommand? SubmitQuestionCommand { get; }
//stop current chat
public ICommand? StopCurrentChatCommand { get; }
//new chat
public ICommand? NewSessionCommand { get; }
//scroll to MarkdownViewer end command
public ICommand ScrollToEndCommand { get; }
#endregion
#endregion
#region Constructor : Initialize
public ChatMdViewModel()
{
// initialize object
markdownViewer = new MarkdownViewer();
_cancellationTokenSource = new CancellationTokenSource();
//generate command
SubmitQuestionCommand = new ParameterlessCommand(async () => OnSubmitQuestion());
StopCurrentChatCommand = new ParameterlessCommand(OnStopCurrentChat);
NewSessionCommand = new ParameterlessCommand(OnNewSessionCommand);
//markdown reletive command
ScrollToEndCommand = new ScrollViewerCommand(OnScrollToEnd);
OnLoadRecord();
}
#endregion
#region other method
#region other
//setting Ollama
public void SetOllama(ShareOllamaObject ollama)
{
_ollama = ollama;
}
//check chat state
private bool CheckChatState()
{
if (_ollama.Ollama == null || _ollama.OllamaEnabled == false)
{
MarkdownContent += "server not open...";
return false;
}
if (_ollama.Ollama.SelectedModel == null)
{
MarkdownContent += "model not select...";
return false;
}
if (string.IsNullOrWhiteSpace(InputText))
{
MarkdownContent += "text is null ...";
return false;
}
return true;
}
//trigger sroll to end
private void OnScrollToEnd(object parameter)
{
var scrollViewer = parameter as ScrollViewer;
if (scrollViewer != null && _isAutoScrolling)
{
scrollViewer.ScrollToEnd();
TextWidth = scrollViewer.Width;
}
}
#endregion
#region Mardown command binding method
//loaded history record
public void OnLoadRecord()
{
OutText(File.ReadAllText($"{Environment.CurrentDirectory}//Data//" +
$"{DateTime.Today.ToString("yyyyMMdd")}//{DateTime.Today.ToString("yyyyMMdd")}_0.txt"));
}
#endregion
#endregion
#region command method
/// <summary>
/// Submit question: Submit problem to the AI and get the output result
/// </summary>
private async void OnSubmitQuestion()
{
try
{
// Checks whether the string is empty, empty, or contains only whitespace characters
if (CheckChatState())
{
_isAutoScrolling = true; //enable auto scroll
//ToggleExtensions();
string input = InputText;
InputText = string.Empty;
string output = string.Empty;
OutText($"{Environment.NewLine}");
OutText($"## 【User】{Environment.NewLine}");
OutText($">{input}{Environment.NewLine}");
OutText($"## 【AI】{Environment.NewLine}");
//
output += ($"{Environment.NewLine}");
output += ($"## 【User】{Environment.NewLine}");
output += ($">{input}{Environment.NewLine}");
output += ($"## 【AI】{Environment.NewLine}");
if (input.Equals("/clearContext"))
{
chat = new Chat(_ollama.Ollama);
_ollama.RecordIndex++;
return;
}
#region Start answer :Mode two => chat mode
if (chat == null)
{
chat = new Chat(_ollama.Ollama);
_ollama.RecordIndex++;
}
_cancellationTokenSource = new CancellationTokenSource();
await foreach (var answerToken in chat.SendAsync(input, _cancellationTokenSource.Token))
{
OutText(answerToken);
output += (answerToken);
await Task.Delay(20);
Debug.Print(answerToken);
}
OutText($"{Environment.NewLine}");
_ollama.WriteDataToFileAsync(output);
#endregion
}
}
catch (Exception ex)
{
OutText($"Error: {ex.Message}{Environment.NewLine}");
}
_isAutoScrolling = false;
}
/// <summary>
/// New build chat.
/// </summary>
private void OnNewSessionCommand()
{
OnStopCurrentChat();
if (chat != null)
{
chat.SendAsync("/clearContext");
if (_ollama != null)
chat = new Chat(_ollama.Ollama);
}
OutText($"{string.Empty}{Environment.NewLine}");
}
/// <summary>
/// stop chat.
/// </summary>
private void OnStopCurrentChat()
{
_cancellationTokenSource?.Cancel();
Task.Delay(100);
OutText($"{string.Empty}{Environment.NewLine}");
MarkdownContent = string.Empty;
}
/// <summary>
/// output Text to Markdown.
/// </summary>
/// <param name="text"></param>
public void OutText(string text)
{
MarkdownContent += text;
}
#endregion
#region Method that trigger a property changed event.
/// <summary>
/// OnPropertyChangedTrigger a property changed event.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

182
ViewModels/MainViewModel.cs Normal file
View File

@@ -0,0 +1,182 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using YwxApp.AiChat.Commands;
using YwxApp.AiChat.Views;
namespace YwxApp.AiChat.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
#region Field | Property | Collection | Command
#region Field
private object _currentView; //The current view object.
private string _currentTime; //The current time.
private string _currentModel; //The current model name.
private DispatcherTimer _timer; //Time label timer.
private ShareOllamaObject _ollamaObject; //OllamaAPI object.
#endregion
#region Property
public object CurrentView
{
get => _currentView;
set
{
_currentView = value;
OnPropertyChanged();
}
}
public string CurrentTime
{
get => _currentTime;
set
{
_currentTime = value;
OnPropertyChanged();
}
}
public string CurrentModel
{
get => _currentModel;
set
{
_currentModel = value;
OnPropertyChanged();
}
}
#endregion
#region Collection
private ObservableCollection<UserControl> _viewList;
private ObservableCollection<UserControl> ViewList
{
get => _viewList;
set
{
_viewList = value;
OnPropertyChanged();
}
}
#endregion
#region Command
public ICommand SwitchToViewCommand { get; }
public ICommand ClosingWindowCommand { get; }
#endregion
#endregion
#region Constructor
public MainViewModel()
{
//Initialize Ollama object.
_ollamaObject = new ShareOllamaObject();
//bind command method
SwitchToViewCommand = new ObjectPassingCommand(OnSwitchToView);
ClosingWindowCommand = new EventsCommand<CancelEventArgs>(OnClosingWindow);
//create view
_viewList = new ObservableCollection<UserControl>();
ViewList.Add(new SettingView(_ollamaObject));
ViewList.Add(new ChatMdView(_ollamaObject));
//Set the default display of subview 1.
CurrentModel = _ollamaObject.Ollama.SelectedModel;
InitializeTimer();
CurrentView = ViewList[0];
}
#region The window close event
/// <summary>
///trigger close event
/// </summary>
private void OnClosingWindow(CancelEventArgs e)
{
if (MessageBox.Show("确定要关闭程序吗?", "确认关闭", MessageBoxButton.YesNo) == MessageBoxResult.No)
e.Cancel = true;
else ClearingResources();
}
/// <summary>
/// Clear the resource.
/// </summary>
private void ClearingResources()
{
ShareOllamaObject.CloseProcess("ollama_llama_server");
Debug.Print($"{ShareOllamaObject.GetProgramName()}:关闭成功...");
}
#endregion
#endregion
#region Other mothod
//Initialize time label timer //Each one second update once
private void InitializeTimer()
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += Timer_Tick;
_timer.Start();
}
//update current time
private void Timer_Tick(object sender, EventArgs e)
{
CurrentTime = DateTime.Now.ToString("HH:mm:ss");
CurrentModel = _ollamaObject.Ollama.SelectedModel;
}
#endregion
#region Command method
#region View switch
//set the view
public void OnSwitchToView(object operationItem)
{
var viewObj = ViewList.FirstOrDefault(viewObj => viewObj.GetType().Name.Equals(operationItem));
if (viewObj == null)
{
var newViewObj = new UserControl();
switch (operationItem)
{
case "ChatMdView":
newViewObj = new ChatMdView(_ollamaObject);
break;
case "SettingView":
newViewObj = new SettingView(_ollamaObject);
break;
default:
break;
}
ViewList.Add(newViewObj);
CurrentView = newViewObj;
}
else
{
CurrentView = viewObj;
}
}
#endregion
#endregion
#region Property changed event
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

View File

@@ -0,0 +1,216 @@
using Microsoft.Win32;
using OllamaSharp;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using YwxApp.AiChat.Commands;
using YwxApp.AiChat.Models;
namespace YwxApp.AiChat.ViewModels
{
/// <summary>
/// 0、Current class:
/// </summary>
public class SettingViewModel : INotifyPropertyChanged
{
#region Field | Property | Collection | Command
#region Field
private string _selectedModel; //select model
private string _modelInfo; //model info
private SolidColorBrush _labelBackgroundColor; //color style
private readonly ShareOllamaObject _ollama; //OllamaAPI object.
#endregion
#region Property
public string OllamaAppPath
{
get { return _ollama.OllamaAppPath; }
set { _ollama.OllamaAppPath = value; OnPropertyChanged(); }
}
public string SelectedModel
{
get => _selectedModel;
set
{
if (_selectedModel != value)
{
_selectedModel = value;
ResetModelName();
}
OnPropertyChanged();
}
}
public string ModelInformation
{
get => _modelInfo;
set
{
_modelInfo = value;
OnPropertyChanged();
}
}
public SolidColorBrush LabelBackgroundColor
{
get => _labelBackgroundColor;
set
{
if (_labelBackgroundColor != value)
{
_labelBackgroundColor = value;
OnPropertyChanged();
}
}
}
#endregion
#region Collection
public ObservableCollection<string> ModelList
{
get { return _ollama.ModelList; }
set { _ollama.ModelList = value; OnPropertyChanged(); }
}
#endregion
#region Command
public ICommand OpenFileDialogCommand { get; } //select Ollama application file path command.
public ICommand GetModelListCommand { get; } //get model list command.
public ICommand ModelListUpdateCommand { get; } //model list update command.
public ICommand StartOllamaServerCommand { get; } //start ollam server command.
#endregion
#endregion
#region Constructor
public SettingViewModel(ShareOllamaObject ollama)
{
_ollama = ollama;
Task task = OnGetModelList();
OpenFileDialogCommand = new ParameterlessCommand(() => OnSelectOllamaAppPathDialog());
GetModelListCommand = new ParameterlessCommand(async () => await OnGetModelList());
ModelListUpdateCommand = new ParameterlessCommand(async () => await OnModelListUpdate());
StartOllamaServerCommand = new ParameterlessCommand(async () => OnStartOllamaServer());
SetConnected();
}
#endregion
#region other method
///set ollama model server application object.
public void SetOllamaApiClient(OllamaApiClient ollama)
{
_ollama.Ollama = ollama;
}
// set the connection states color
public void SetConnected()
{
if (_ollama.OllamaEnabled)
{
LabelBackgroundColor = Brushes.Green;
}
else
{
LabelBackgroundColor = Brushes.Red;
}
}
/// <summary>
/// reset the model
/// </summary>
private void ResetModelName()
{
_ollama.OllamaEnabled = false;
_ollama.Ollama.SelectedModel = SelectedModel;
ModelInformationChanged();
_ollama.OllamaEnabled = true;
}
/// <summary>
/// model info changed
/// </summary>
public void ModelInformationChanged()
{
string modelName = SelectedModel.Split(':')[0].ToLower();
string modelInfoPath = $"{Environment.CurrentDirectory}\\model introduction\\{modelName}.txt";
string info = string.Empty;
if (File.Exists(modelInfoPath))
{
info = File.ReadAllText(modelInfoPath);
}
//MessageBox.Show(modelInfoPath);
switch (modelName)
{
case ModelDescription.Llama32:
ModelInformation = info;
break;
case ModelDescription.CodeGemma:
ModelInformation = info;
break;
default:
ModelInformation = "";
break;
}
}
#endregion
#region command trigger method
private void OnStartOllamaServer()
{
if (!_ollama.OllamaEnabled)
{
_ollama.StartOllama(OllamaAppPath, SelectedModel);
}
}
private void OnSelectOllamaAppPathDialog()
{
OpenFileDialog openFileDialog = new();
if (openFileDialog.ShowDialog() == true)
{
OllamaAppPath = openFileDialog.FileName;
}
}
/// <summary>
/// get the model list
/// </summary>
private async Task OnGetModelList()
{
try
{
//ModelList.Clear();
ModelList = (ObservableCollection<string>)_ollama.GetModelList();
Debug.Print($"ModelList count: {ModelList.Count}");
SelectedModel = _ollama.Ollama.SelectedModel;
var modelName = ModelList.FirstOrDefault(name => name.Equals(SelectedModel));
if (ModelList.Count > 0 && modelName != null)
{
SelectedModel = ModelList[ModelList.Count - 1];
}
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// update the model list
/// </summary>
private async Task OnModelListUpdate()
{
MessageBox.Show($"List Update");
}
#endregion
#region property changed event
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

View File

@@ -0,0 +1,298 @@
using OllamaSharp;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows;
namespace YwxApp.AiChat.ViewModels
{
/// <summary>
/// 0、Current class:
/// </summary>
public class ShareOllamaObject
{
#region Field | Property | Collection | Command
#region Field
private bool _ollamaEnabled = false; //ollama connected state
private string _ollamaAppPath; //ollama app path.
private int recordIndex = 0; //current record index.
private string _currentPath; //current record;
private Chat chat; //build interactive chat model object.
private OllamaApiClient _ollama; //OllamaAPI object.
#endregion
#region Property
public string OllamaAppPath
{
get { return _ollamaAppPath; }
set { _ollamaAppPath = value; }
}
public bool OllamaEnabled
{
get { return _ollamaEnabled; }
set { _ollamaEnabled = value; }
}
public OllamaApiClient Ollama
{
get { return _ollama; }
set { _ollama = value; }
}
public Chat Chat
{
get { return chat; }
set { chat = value; }
}
public string CurrentPath
{
get => _currentPath;
}
public int RecordIndex
{
get => recordIndex;
set
{
recordIndex = value;
_currentPath = $"{Environment.CurrentDirectory}//Data//{DateTime.Today.ToString("yyyyMMdd")}" +
$"//{DateTime.Today.ToString("yyyyMMdd")}_{recordIndex}.txt";
}
}
#endregion
#region Collection
public ObservableCollection<string> ModelList { get; set; }
#endregion
#endregion
#region Constructor
public ShareOllamaObject()
{
RecordIndex = 0;
WriteDataToFileAsync("");
Init(OllamaAppPath, "llama3.2:9b");
}
#endregion
#region other method
/// <summary>
/// initialite method
/// </summary>
private void Init(string appPath, string modelName)
{
OllamaAppPath = appPath;
try
{
// 设置默认设备为GPU
Environment.SetEnvironmentVariable("OLLAMA_DEFAULT_DEVICE", "gpu");
//判断路径是否存在
if (OllamaAppPath == string.Empty || OllamaAppPath == null) OllamaAppPath = @"ollama app.exe";
//路径存在获取应用名
if (File.Exists(OllamaAppPath)) OllamaAppPath = Path.GetFileName(OllamaAppPath);
//获取环境Ollama环境变量用于找到 ollama app.exe
var filePath = FindExeInPath(OllamaAppPath);
//如果路径存在启动Ollama
if (File.Exists(filePath)) CheckStartProcess(OllamaAppPath);
//连接Ollama并设置初始模型
_ollama = new OllamaApiClient(new Uri("http://localhost:11434"));
//获取本地可用的模型列表
ModelList = (ObservableCollection<string>)GetModelList();
var tmepModelName = ModelList.FirstOrDefault(name => name.ToLower().Contains("llama3.2"));
if (tmepModelName != null) _ollama.SelectedModel = tmepModelName;
else if (ModelList.Count > 0) _ollama.SelectedModel = ModelList[ModelList.Count - 1];
if (ModelList.FirstOrDefault(name => name.Equals(modelName)) != null) _ollama.SelectedModel = modelName;
//Ollama服务启用成功
OllamaEnabled = true;
}
catch (Exception)
{
OllamaEnabled = false;
}
}
/// <summary>
/// update the model selected by Ollama
/// </summary>
public void UpdataSelectedModel(string model)
{
Ollama.SelectedModel = model;
OllamaEnabled = true;
}
/// <summary>
/// Start Ollama app and relevant server.
/// </summary>
public async void StartOllama(string appPath, string modelName)
{
Init(appPath, modelName); await Task.Delay(1);
}
/// <summary>
/// get model list
/// </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;
}
#endregion
#region starting or closeing method of Ollama(server).
/// <summary>
/// Finds whether the specified application name is configured in the system environment.
/// If it exists, return the full path, otherwise return null
/// </summary>
public static string FindExeInPath(string exeName)
{
// get environment variable "Path" value
var pathVariable = Environment.GetEnvironmentVariable("PATH");
// Split string
string[] paths = pathVariable.Split(Path.PathSeparator);
foreach (string path in paths)
{
string fullPath = Path.Combine(path, exeName);
if (File.Exists(fullPath))
{
return fullPath;
}
}
return null;
}
/// <summary>
///Startup program Specifies a program, enters a program name, and determines whether the program is running.
/// If it is running, exit directly, otherwise run the program according to the input path.
/// </summary>
public static void CheckStartProcess(string processPath)
{
string processName = Path.GetFileName(processPath);
CheckStartProcess(processName, processPath);
}
/// <summary>
/// Startup program Specifies a program, enters a program name, and determines whether the program is running.
/// If it is running, exit directly, otherwise run the program according to the input path.
/// </summary>
public static void CheckStartProcess(string processName, string processPath)
{
// Check whather the program is running
if (!IsProcessRunning(processName))
{
Console.WriteLine($"{processName} is not running. Starting the process...");
StartProcess(processPath);
}
else Console.WriteLine($"{processName} is already running.");
}
/// <summary>
/// Enter the program path to start the program
/// </summary>
public static void StartProcess(string processPath)
{
try
{
Process.Start(processPath);
Console.WriteLine("Process started successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error starting process: {ex.Message}");
}
}
/// <summary>
/// Check whather the process is running
/// </summary>
public static bool IsProcessRunning(string processName)
{
Process[] processes = Process.GetProcessesByName(processName);
return processes.Length > 0;
}
/// <summary>
/// close the process with the specify name.
/// </summary>
/// <param name="processName"></param>
public static void CloseProcess(string processName)
{
try
{
foreach (var process in Process.GetProcessesByName(processName))
{
process.Kill();
process.WaitForExit();
Application.Current.Shutdown();
}
}
catch (Exception ex)
{
MessageBox.Show($"无法关闭【{processName}】进程: {ex.Message}");
}
}
/// <summary>
/// get current process name
/// </summary>
public static string GetProgramName()
{
Assembly assembly = Assembly.GetExecutingAssembly();
return assembly.GetName().Name;
}
#endregion
#region File save
/// <summary>
/// Save record
/// </summary>
public void WriteDataToFileAsync(string data, int retryCount = 5, int delayMilliseconds = 500)
{
//Get the directory where the file located.
string directoryPath = Path.GetDirectoryName(CurrentPath);
// if directory exists't ,create directory(include all must directory).
if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
for (int i = 0; i < retryCount; i++)
{
try
{
using (FileStream fs = new FileStream(CurrentPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
using (StreamWriter writer = new StreamWriter(fs, Encoding.UTF8))
{
writer.WriteAsync(data);
}
return; // successful writed exit the loop.
}
catch (IOException ex)
{
if (i == retryCount - 1)
{
throw; //If the maximum number of retries is reached , a exception is thrown
}
Task.Delay(delayMilliseconds); // Wait a while and try again
}
catch (Exception ex)
{
throw; //other exception is thrown
}
}
}
#endregion
}
}