diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..e7e5ec2 --- /dev/null +++ b/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..632f4a7 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,13 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace YwxApp.AiChat; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ +} + diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Commands/ClosingWindowBehavior.cs b/Commands/ClosingWindowBehavior.cs new file mode 100644 index 0000000..eaa2dd7 --- /dev/null +++ b/Commands/ClosingWindowBehavior.cs @@ -0,0 +1,54 @@ + +using System.ComponentModel; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interactivity; +namespace YwxApp.AiChat.Commands +{ + + + /// + /// Close Window Behavior + /// + public class ClosingWindowBehavior : Behavior + { + public static readonly DependencyProperty CommandProperty = + DependencyProperty.Register("Command", typeof(ICommand), typeof(ClosingWindowBehavior), new PropertyMetadata(null)); + + public static readonly DependencyProperty CommandParameterProperty = + DependencyProperty.Register("CommandParameter", typeof(object), typeof(ClosingWindowBehavior), new PropertyMetadata(null)); + + public ICommand Command + { + get { return (ICommand)GetValue(CommandProperty); } + set { SetValue(CommandProperty, value); } + } + + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.Closing += OnClosing; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject.Closing -= OnClosing; + } + + private void OnClosing(object sender, CancelEventArgs e) + { + if (Command != null && Command.CanExecute(CommandParameter)) + { + Command.Execute(e); + } + } + } + } + diff --git a/Commands/EventsCommand.cs b/Commands/EventsCommand.cs new file mode 100644 index 0000000..45a538b --- /dev/null +++ b/Commands/EventsCommand.cs @@ -0,0 +1,29 @@ +using System.Windows.Input; + +namespace YwxApp.AiChat.Commands +{ + + public class EventsCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + public EventsCommand(Action execute, Func 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; } + } + } +} \ No newline at end of file diff --git a/Commands/ObjectPassingCommand.cs b/Commands/ObjectPassingCommand.cs new file mode 100644 index 0000000..16350d9 --- /dev/null +++ b/Commands/ObjectPassingCommand.cs @@ -0,0 +1,29 @@ +using System.Windows.Input; + +namespace YwxApp.AiChat.Commands +{ + + /// + /// object parameter passing. + /// + public class ObjectPassingCommand : ICommand + { + public Action execute; + public ObjectPassingCommand(Action execute) + { + this.execute = execute; + } + public event EventHandler? CanExecuteChanged; + + public bool CanExecute(object? parameter) + { + return CanExecuteChanged != null; + } + + public void Execute(object? parameter) + { + execute?.Invoke(parameter); + } + } +} + diff --git a/Commands/ParameterlessCommand.cs b/Commands/ParameterlessCommand.cs new file mode 100644 index 0000000..5681094 --- /dev/null +++ b/Commands/ParameterlessCommand.cs @@ -0,0 +1,29 @@ +using System.Windows.Input; + +namespace YwxApp.AiChat.Commands +{ + + /// + /// relay command + /// + 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(); + } + } +} + diff --git a/Commands/ScrollViewerCommand.cs b/Commands/ScrollViewerCommand.cs new file mode 100644 index 0000000..62b62a9 --- /dev/null +++ b/Commands/ScrollViewerCommand.cs @@ -0,0 +1,37 @@ +using System.Windows.Input; + +namespace YwxApp.AiChat.Commands +{ + + /// + /// Scroll command : The argument object passed by this constructor of this class is ScrollViewer + /// + class ScrollViewerCommand : ICommand + { + private readonly Action _execute; + private readonly Predicate _canExecute; + + public ScrollViewerCommand(Action execute, Predicate 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; } + } + } +} + diff --git a/Data/20250312/20250312_0.txt b/Data/20250312/20250312_0.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/Data/20250312/20250312_0.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..c49f80f --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs new file mode 100644 index 0000000..9d59d60 --- /dev/null +++ b/MainWindow.xaml.cs @@ -0,0 +1,19 @@ +using System.Windows; +using YwxApp.AiChat.Services; +using YwxApp.AiChat.ViewModels; + +namespace YwxApp.AiChat; + +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + //OllamaService ollamaService = new(); + //ollamaService.Configure("http://192.168.1.3:11434", "deepseek-r1:1.5b"); + //this.DataContext = new ChatViewModel(new OllamaService()); + } +} \ No newline at end of file diff --git a/Models/ChatMessage.cs b/Models/ChatMessage.cs new file mode 100644 index 0000000..bc79fae --- /dev/null +++ b/Models/ChatMessage.cs @@ -0,0 +1,30 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace YwxApp.AiChat.Models +{ + public class ChatMessage : INotifyPropertyChanged + { + public enum MessageRole { User, Assistant } + + private MessageRole _role; + public MessageRole Role + { + get => _role; + set { _role = value; OnPropertyChanged(); } + } + + private string _content; + public string Content + { + get => _content; + set { _content = value; OnPropertyChanged(); } + } + + public event PropertyChangedEventHandler? PropertyChanged; + protected virtual void OnPropertyChanged([CallerMemberName] string? name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + } +} diff --git a/Models/ModelDescription.cs b/Models/ModelDescription.cs new file mode 100644 index 0000000..3d3cbad --- /dev/null +++ b/Models/ModelDescription.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YwxApp.AiChat.Models +{ + /// + /// select switch display model description. + /// + public class ModelDescription + { + public const string CodeGemma = "codegemma"; + public const string Llama32 = "llama3.2"; + //model list(description) + public const string Codellama34b = "codellama:34b"; + public const string Llava13b = "llava:13b"; + public const string CommandRLatest = "command-r:latest"; + public const string Wizardlm2Latest = "wizardlm2:latest"; + public const string Qwen25CoderLatest = "qwen2.5-coder:latest"; + public const string Qwen25_14b = "qwen2.5:14b"; + public const string SamanthaMistralLatest = "samantha-mistral:latest"; + public const string MistralSmallLatest = "mistral-small:latest"; + public const string Gemma29b = "gemma2:9b"; + public const string NemotronMiniLatest = "nemotron-mini:latest"; + public const string Phi35Latest = "phi3.5:latest"; + public const string Llama32VisionLatest = "llama3.2-vision:latest"; + public const string Llama31_8b = "llama3.1:8b"; + public const string Gemma22b = "gemma2:2b"; + public const string Qwen27b = "qwen2:7b"; + public const string Qwen20_5b = "qwen2:0.5b"; + public const string Llama31_70b = "llama3.1:70b"; + public const string Llama31Latest = "llama3.1:latest"; + public const string Llama32Latest = "llama3.2:latest"; + public const string Llama32_3b = "llama3.2:3b"; + } + + +} diff --git a/Services/IChatService.cs b/Services/IChatService.cs new file mode 100644 index 0000000..2d48128 --- /dev/null +++ b/Services/IChatService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YwxApp.AiChat.Services +{ + public interface IChatService + { + Task GetResponseAsync(string prompt); + void Configure(string apiKey, string modelName = "gpt-3.5-turbo"); + } +} diff --git a/Services/OllamaService.cs b/Services/OllamaService.cs new file mode 100644 index 0000000..4c14283 --- /dev/null +++ b/Services/OllamaService.cs @@ -0,0 +1,95 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Windows; +using static YwxApp.AiChat.Services.OpenAIService; + +namespace YwxApp.AiChat.Services +{ + public class OllamaService : IChatService + { + private readonly HttpClient _httpClient; + private string _modelName; + + public OllamaService() + { + _httpClient = new HttpClient(); + } + + public void Configure(string apiKey, string modelName) + { + //_httpClient.BaseAddress = new Uri(apiKey); + _modelName = modelName; + } + + public async Task GetResponseAsync(string prompt) + { + //var request = new + //{ + // model = "deepseek-r1:1.5b", + // prompt = prompt, + // stream = false + //}; + + // var response = await _httpClient.PostAsJsonAsync("http://192.168.1.3:11434/api/generate", request); + // var result = await response.Content.ReadFromJsonAsync(); + + // return result?.Response ?? "No response"; + + var request = new ChatRequest + { + Messages ={ + new Message { Role = "user", Content = "你是谁" } + } + }; + + string json = JsonSerializer.Serialize(request, new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + var response = await _httpClient.PostAsJsonAsync("http://192.168.1.3:11434/api/chat", request); + // var response = await _httpClient.PostAsJsonAsync("http://192.168.1.3:11434/api/generate", request); + // var result = await response.Content.ReadFromJsonAsync(); + var result = await response.Content.ReadAsStringAsync(); + MessageBox.Show(result); + + return "No response"; + } + public class ChatRequest + { + [JsonPropertyName("messages")] + public List Messages { get; set; } = new List(); + + [JsonPropertyName("model")] + public string? Model { get; set; } = "deepseek-r1:1.5b"; // 默认值 + + [JsonPropertyName("stream")] + public bool Stream { get; set; } = false; + + [JsonPropertyName("temperature")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double Temperature { get; set; } = 0.01; + + [JsonPropertyName("max_tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MaxTokens { get; set; } = 1024; + } + + public class Message + { + [JsonPropertyName("role")] + public string Role { get; set; } = "user"; + + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; + } + + private class OllamaResponse + { + public string Response { get; set; } + } + } +} \ No newline at end of file diff --git a/Services/OpenAIService.cs b/Services/OpenAIService.cs new file mode 100644 index 0000000..ecaa50d --- /dev/null +++ b/Services/OpenAIService.cs @@ -0,0 +1,54 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace YwxApp.AiChat.Services +{ + public class OpenAIService : IChatService + { + private readonly HttpClient _httpClient; + private string _modelName = "gpt-3.5-turbo"; + + public OpenAIService() + { + _httpClient = new HttpClient(); + _httpClient.BaseAddress = new Uri("https://api.openai.com/v1/"); + } + + public void Configure(string apiKey, string modelName = "gpt-3.5-turbo") + { + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", apiKey); + _modelName = modelName; + } + + public async Task GetResponseAsync(string prompt) + { + var requestBody = new + { + model = _modelName, + messages = new[] { new { role = "user", content = prompt } } + }; + + var response = await _httpClient.PostAsJsonAsync("chat/completions", requestBody); + var responseContent = await response.Content.ReadFromJsonAsync(); + + return responseContent?.Choices[0].Message.Content ?? "No response"; + } + + public class Message + { + public string Content { get; set; } + } + public class Choice + { + public Message Message { get; set; } + + } + private class OpenAIResponse + { + public Choice[] Choices { get; set; } + + } + } +} diff --git a/Utilities/AsyncRelayCommand.cs b/Utilities/AsyncRelayCommand.cs new file mode 100644 index 0000000..47a24a2 --- /dev/null +++ b/Utilities/AsyncRelayCommand.cs @@ -0,0 +1,26 @@ +using System.Windows.Input; + +namespace YwxApp.AiChat.Utilities +{ + public class AsyncRelayCommand : ICommand + { + private readonly Func _execute; + private readonly Func _canExecute; + + public AsyncRelayCommand(Func execute, Func canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + public event EventHandler CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; + + public async void Execute(object parameter) => await _execute(); + } +} diff --git a/ViewModels/ChatMdViewModel.cs b/ViewModels/ChatMdViewModel.cs new file mode 100644 index 0000000..b2f91ad --- /dev/null +++ b/ViewModels/ChatMdViewModel.cs @@ -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 +{ + /// + /// 0、Current class: + /// + 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 + /// + /// Submit question: Submit problem to the AI and get the output result + /// + 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; + } + + /// + /// New build chat. + /// + private void OnNewSessionCommand() + { + OnStopCurrentChat(); + if (chat != null) + { + chat.SendAsync("/clearContext"); + if (_ollama != null) + chat = new Chat(_ollama.Ollama); + } + OutText($"{string.Empty}{Environment.NewLine}"); + } + /// + /// stop chat. + /// + private void OnStopCurrentChat() + { + _cancellationTokenSource?.Cancel(); + Task.Delay(100); + OutText($"{string.Empty}{Environment.NewLine}"); + MarkdownContent = string.Empty; + } + /// + /// output Text to Markdown. + /// + /// + public void OutText(string text) + { + MarkdownContent += text; + } + #endregion + + #region Method that trigger a property changed event. + /// + /// OnPropertyChanged:Trigger a property changed event. + /// + public event PropertyChangedEventHandler? PropertyChanged; + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + #endregion + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..b731681 --- /dev/null +++ b/ViewModels/MainViewModel.cs @@ -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 _viewList; + private ObservableCollection 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(OnClosingWindow); + + //create view + _viewList = new ObservableCollection(); + 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 + /// + ///trigger close event + /// + private void OnClosingWindow(CancelEventArgs e) + { + if (MessageBox.Show("确定要关闭程序吗?", "确认关闭", MessageBoxButton.YesNo) == MessageBoxResult.No) + e.Cancel = true; + else ClearingResources(); + } + /// + /// Clear the resource. + /// + 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 + } + + +} + + diff --git a/ViewModels/SettingViewModel.cs b/ViewModels/SettingViewModel.cs new file mode 100644 index 0000000..4d306b7 --- /dev/null +++ b/ViewModels/SettingViewModel.cs @@ -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 +{ + + + + /// + /// 0、Current class: + /// + 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 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; + } + } + /// + /// reset the model + /// + private void ResetModelName() + { + _ollama.OllamaEnabled = false; + _ollama.Ollama.SelectedModel = SelectedModel; + ModelInformationChanged(); + _ollama.OllamaEnabled = true; + } + /// + /// model info changed + /// + 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; + } + } + /// + /// get the model list + /// + private async Task OnGetModelList() + { + try + { + //ModelList.Clear(); + ModelList = (ObservableCollection)_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); + } + } + /// + /// update the model list + /// + 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 + } +} diff --git a/ViewModels/ShareOllamaObject.cs b/ViewModels/ShareOllamaObject.cs new file mode 100644 index 0000000..f82d34a --- /dev/null +++ b/ViewModels/ShareOllamaObject.cs @@ -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 +{ + + + /// + /// 0、Current class: + /// + 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 ModelList { get; set; } + #endregion + + #endregion + + #region Constructor + public ShareOllamaObject() + { + RecordIndex = 0; + WriteDataToFileAsync(""); + Init(OllamaAppPath, "llama3.2:9b"); + } + #endregion + + #region other method + /// + /// initialite method + /// + 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)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; + } + } + /// + /// update the model selected by Ollama + /// + public void UpdataSelectedModel(string model) + { + Ollama.SelectedModel = model; + OllamaEnabled = true; + } + + /// + /// Start Ollama app and relevant server. + /// + public async void StartOllama(string appPath, string modelName) + { + Init(appPath, modelName); await Task.Delay(1); + } + /// + /// get model list + /// + public Collection GetModelList() + { + var models = _ollama.ListLocalModelsAsync(); + var modelList = new ObservableCollection(); + foreach (var model in models.Result) + { + modelList.Add(model.Name); + } + return modelList; + } + #endregion + + #region starting or closeing method of Ollama(server). + /// + /// Finds whether the specified application name is configured in the system environment. + /// If it exists, return the full path, otherwise return null + /// + 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; + } + + /// + ///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. + /// + public static void CheckStartProcess(string processPath) + { + string processName = Path.GetFileName(processPath); + CheckStartProcess(processName, processPath); + } + + /// + /// 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. + /// + 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."); + } + + + /// + /// Enter the program path to start the program + /// + 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}"); + } + } + + /// + /// Check whather the process is running + /// + public static bool IsProcessRunning(string processName) + { + Process[] processes = Process.GetProcessesByName(processName); + return processes.Length > 0; + } + + /// + /// close the process with the specify name. + /// + /// + 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}"); + } + } + /// + /// get current process name + /// + public static string GetProgramName() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + return assembly.GetName().Name; + } + #endregion + + #region File save + /// + /// Save record + /// + 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 + } +} \ No newline at end of file diff --git a/Views/ChatMdView.xaml b/Views/ChatMdView.xaml new file mode 100644 index 0000000..16e9d0d --- /dev/null +++ b/Views/ChatMdView.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +