【C#】WPF+OllamaSharpe实现离线AI对话

在这里插入图片描述

  • 二、程序

    • 2.1 项目结构

      • 项目结构如下图,目前对WPF的MVVM模型只是初步初探,所有只是做了简单的模块区分。

        1. Models
        • 在此创建一些实现 ICommand 的类(这个目录应该只生命一些对象模型的,模型中只创建基本属性,但是我目前没有做区分。先简单实现功能,后面可能会优化。)
        1. Resources
        • 在此存放图像资源…
        1. Resources
        • ViewModels:在此创建视图模型,视图对应的模型(前后端分离思想?),如SettingViewModel用于跟SetingView进行数据绑定(属性、命令)。
        1. Views
        • 在此创建视图->UI交互界面。
  • 2.2 项目代码

    • MainWindow.xaml

      <Window x:Class="MAIModel.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:local="clr-namespace:MAIModel"
              xmlns:viewmodels="clr-namespace:MAIModel.ViewModels" 
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
              xmlns:behaviors="clr-namespace:MAIModel.Commands"
              mc:Ignorable="d"
              WindowStartupLocation="CenterScreen"
              Title="ChatAI" Height="600" Width="800"
              Icon="/Resources/app-logo128.ico"
              MinHeight="600" MinWidth="800">
          <!--Bind context-->
          <Window.DataContext>
              <viewmodels:MainViewModel/>
          </Window.DataContext>
          <!--Reference  style resource-->
          <Window.Resources>
              <ResourceDictionary>
                  <!--resource dictionary : add control style-->
                  <ResourceDictionary.MergedDictionaries>
                      <ResourceDictionary Source="Views/Style/ButtonStyle.xaml"/>
                  </ResourceDictionary.MergedDictionaries>
              </ResourceDictionary>
          </Window.Resources>
          <!-- Add close behevior event-->
          <i:Interaction.Behaviors>
              <behaviors:ClosingWindowBehavior Command="{Binding  ClosingWindowCommand}" />
          </i:Interaction.Behaviors>
          <!--Front-end display content-->
          <Grid>
              <!--defined column-->
              <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="200"/>
                  <ColumnDefinition Width="*"/>
                  <ColumnDefinition Width="20"/>
              </Grid.ColumnDefinitions>
              <!--defined row-->
              <Grid.RowDefinitions>
                  <RowDefinition Height="10"/>
                  <RowDefinition Height="*"/>
                  <RowDefinition Height="25"/>
              </Grid.RowDefinitions>
      
              <!-- Row 1 , Column 1: set the background color-->
              <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
                  <!-- Row 1 -->
                  <Rectangle >
                      <Rectangle.Fill>
                          <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                              <GradientStop Color="#916CE5" Offset="0.5" />
                              <GradientStop Color="#FFFFFF" Offset="1.5" />
                          </LinearGradientBrush>
                      </Rectangle.Fill>
                  </Rectangle>
              </Grid>
              <!-- Row 2,Column 11、Set the background color of function bar(Rectangular area).
                  2、Set the function bar buttons : icon + text + other style
              -->
              <Grid Grid.Row="1"  Grid.Column="0"  >
                  <!--Row 2,Column 1: Backgroud-->
                  <Rectangle  Grid.Row="1" Grid.Column="0">
                      <Rectangle.Fill>
                          <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                              <GradientStop Color="#9ABAFF" Offset="0.8" />
                              <GradientStop Color="#9ABFAF" Offset="0.3" />
                          </LinearGradientBrush>
                      </Rectangle.Fill>
                  </Rectangle>
                  <!--Row 2,Column 1: Function button setting-->
                  <StackPanel Margin="0 0 0 0"  Grid.Row="1" Grid.Column="0">
                      <Button Command="{Binding SwitchToViewCommand}" CommandParameter="SettingView"
                      Style="{StaticResource IconButtonStyle}">
                          <StackPanel Orientation="Horizontal">
                              <Image Source="/Resources/setting64.png" Margin="5" />
                              <TextBlock Text="设置" VerticalAlignment="Center"/>
                          </StackPanel>
                      </Button>
                      <Button 
                      Command="{Binding SwitchToViewCommand}" CommandParameter="ChatMdView"
                      Style="{StaticResource IconButtonStyle}">
                          <StackPanel Orientation="Horizontal">
                              <Image Source="/Resources/chat64.png" Margin="5"/>
                              <TextBlock Text="会话" VerticalAlignment="Center"/>
                          </StackPanel>
                      </Button>
                  </StackPanel>
              </Grid>
              <Grid Grid.Row="1" Grid.Column="1" Margin="5">
                  <!-- Row 2,Column 2:Subview display area ,used to display switched subview.-->
                  <ContentControl
              Content="{Binding CurrentView}"
              HorizontalAlignment="Stretch" 
              VerticalAlignment="Stretch" 
              HorizontalContentAlignment="Stretch" 
              VerticalContentAlignment="Stretch"/>
              </Grid>
              <!--  Row 2,Column 21、Background color.
                  2、 Use the Lable to display current model and time.
              -->
              <Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3">
                  <Rectangle>
                      <Rectangle.Fill>
                          <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                              <GradientStop Color="#FAAFA9"  Offset="0.1" />
                              <GradientStop Color="#A4D3A2" Offset="0.9" />
                          </LinearGradientBrush>
                      </Rectangle.Fill>
                  </Rectangle >
                  <WrapPanel  Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" 
          VerticalAlignment="Center" HorizontalAlignment="Right">
                      <Label Content="{Binding CurrentModel}" Width="auto"  FontSize="12" Margin="5 0 5 0"/>
                      <Label Content="{Binding CurrentTime}" Background="#00F0BD"
         Width="auto"  FontSize="12" Margin="5 0 5 0"/>
                  </WrapPanel>
              </Grid>
          </Grid>
      </Window>
      
  • SettingView.xaml

    <UserControl  x:Class="MAIModel.Views.SettingView"
            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:local ="clr-namespace:MAIModel.ViewModels"
            mc:Ignorable="d" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <UserControl.Resources>
            <ResourceDictionary>
                <!--Resource dictionary : add the control style-->
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="Style/ButtonStyle.xaml"/>
                    <ResourceDictionary Source="Style/TextBoxStyle.xaml"/>
                    <ResourceDictionary Source="Style/LabelStyle.xaml"/>
                    <ResourceDictionary Source="Style/ComboBoxStyle.xaml"/>
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
        </UserControl.Resources>
    
    
        <Grid Background="#FFFFFF"  HorizontalAlignment="Stretch">
            <Grid.RowDefinitions>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            
            <!-- The first line -->
            <WrapPanel Grid.Row="0" Margin="5"  VerticalAlignment="Center" HorizontalAlignment="Left">
                <Label Content="Ollama路径:" Margin="5" HorizontalAlignment="Left"  VerticalAlignment="Center" />
                <TextBox x:Name="Tbx_OllamaAppPath"  FontSize="12" 
                          Text="{Binding OllamaAppPath}"
                          Style="{StaticResource SearchBoxStyle}" Margin="5" />
            </WrapPanel>
            
            <!--The second line-->
            <WrapPanel Grid.Row="1" Margin="5"  VerticalAlignment="Center" HorizontalAlignment="Left">
                <Label Content="Ollama:" VerticalAlignment="Center" Margin="5" />
                <Label Name="Label_State" Style="{StaticResource RoundLabelStyle}"  />
                <Button Content="打开"   Style="{StaticResource RoundCornerButtonStyle}" 
                        Command="{Binding StartOllamaServerCommand}"/>
            </WrapPanel>
            
            <!--The third line-->
            <WrapPanel Grid.Row="2" Margin="5"  VerticalAlignment="Center" HorizontalAlignment="Left">
                <Label Content="模型:" VerticalAlignment="Center" Margin="5" />
                <ComboBox x:Name="Cbx_ModelList" Style="{StaticResource RoundComboBoxStyle}" 
                  ItemsSource="{Binding ModelList}"
                  SelectedItem="{Binding SelectedModel}">
                </ComboBox>
                <Button Content="刷新"    Margin="5" Grid.Row="1" 
                Style="{StaticResource RoundCornerButtonStyle}" 
                Command="{Binding ModelListUpdateCommand}"/>
            </WrapPanel>
            <TextBox  x:Name="ModelDesciption" Grid.Row="3" IsReadOnly="True" 
                      TextWrapping="WrapWithOverflow" Text="{Binding ModelInformation,Mode=OneWay}"/>
        </Grid>
    </UserControl>
    
    
    
    ------------------------------------------- SettingView  ------------------------------------------- 
    using MAIModel.ViewModels;
    using System.Windows.Controls;
    
    namespace MAIModel.Views
    {
        public partial class SettingView : UserControl
        {
            SettingViewModel _viewModel;
            public SettingView(ShareOllamaObject ollama)
            {
                InitializeComponent();
                _viewModel = new SettingViewModel(ollama);
                this.DataContext = _viewModel;
            }
        }
    }
    
  • ChatMdView.xaml

    <UserControl x:Class="MAIModel.Views.ChatMdView"
                 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:i="http://schemas.microsoft.com/expression/2010/interactivity"
                 xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
                 xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800">
        <UserControl.Resources >
            <ResourceDictionary>
                <!--Resource dictionary:Add control style.-->
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="Style/ButtonStyle.xaml"/>
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
        </UserControl.Resources>
        <Grid Background="#0F000F">
           
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="200" />
                <RowDefinition Height="50" />
            </Grid.RowDefinitions>
               
            <!--First line: Display output text to "Markdown" container-->
            <Grid Grid.Row="0">
                <ScrollViewer Background="#FFFFFF" x:Name="MarkDownScrollViewer">
                    <!--Bind event command to the ScrollViewer-->
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="ScrollChanged">
                            <i:InvokeCommandAction Command = "{Binding ScrollToEndCommand}"
                                CommandParameter="{Binding ElementName=MarkDownScrollViewer}" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                    <!--scrollviewer internal container-->
                    <markdig:MarkdownViewer x:Name="MarkdownOutputBox" Markdown="{Binding MarkdownContent}" />
                </ScrollViewer>
            </Grid>
    
            <!-- the second line  -->
            <Grid Grid.Row="1">
                <TextBox x:Name="InputBox"
                 Text="{Binding InputText , Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                 Grid.Row="1"  Margin="5" AcceptsReturn="True" 
                 VerticalScrollBarVisibility="Auto">
                    <!--key binding of "Enter"-->
                    <TextBox.InputBindings>
                        <KeyBinding Command="{Binding SubmitQuestionCommand}" Key="Enter"/>
                    </TextBox.InputBindings>
                </TextBox>
            </Grid>
            
            <!-- The third line: submit button  -->
            <Grid Grid.Row="2">
                <WrapPanel Grid.Row="2"  HorizontalAlignment="Right">
    
                    <Button x:Name="BtnNewChat" Content="新建会话" 
                         HorizontalAlignment="Right"
                         Style="{StaticResource RoundCornerButtonStyle}"
                         Command="{Binding NewSessionCommand}" 
                         Width="100" 
                         Height="30"/>
                    <Button x:Name="BtnSubmit"  Content="提交" 
                        HorizontalAlignment="Right"
                        Style="{StaticResource RoundCornerButtonStyle}"
                        Command="{Binding SubmitQuestionCommand}" 
                        Width="100" 
                        Height="30"/>
                </WrapPanel>
            </Grid>
            
        </Grid>
    </UserControl>
    
    ----------------------------------------- ChatMdView  -----------------------------------------
    using MAIModel.ViewModels;
    using System.Windows.Controls;
    
    namespace MAIModel.Views
    {
        public partial class ChatMdView : UserControl
        {
            ChatMdViewModel viewModel;
            public ChatMdView(ShareOllamaObject shareOllama)
            {
                InitializeComponent();
                viewModel = new ChatMdViewModel();
                viewModel.SetOllama(shareOllama);
                this.DataContext = viewModel;
            }
        }
    }
    
  • ShareOllamaObject

    using OllamaSharp;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Text;
    using System.Windows;
    
    namespace MAIModel.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
        }
    }
    
  • MainViewModel

    using MAIModel.Commands;
    using MAIModel.Views;
    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;
    namespace MAIModel.ViewModels
    {
        /// <summary>
        /// </summary>
        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
        }
    }
    
  • SettingViewModel

    using MAIModel.Commands;
    using MAIModel.Models;
    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;
    
    namespace MAIModel.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 OpenFileDialog();
                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
        }
    }
    
  • ChatMdViewModel

    using MAIModel.Commands;
    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;
    
    namespace MAIModel.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>
            /// OnPropertyChanged:Trigger a property changed event.
            /// </summary>
            public event PropertyChangedEventHandler? PropertyChanged;
            protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
            #endregion
        }
    }
    
  • ModelDescription

    /// <summary>
    /// select switch  display model description.
    /// </summary>
    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";
    }
    
  • ClosingWindowBehavior

    	using System.ComponentModel;
    	using System.Windows.Input;
    	using System.Windows.Interactivity;
    	using System.Windows;
    	namespace MAIModel.Commands
    	{
    	    public class ClosingWindowBehavior : Behavior<Window>
    	    {
    	        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);
    	            }
    	        }
    	    }
    	}
    
  • EventsCommand

    using System;
    using System.Windows.Input;
    /// <summary>
    /// close window events
    /// </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; }
        }
    }
    

  • ObjectPassingCommand

    using System.Windows.Input;
    namespace MAIModel.Commands
    {
        /// <summary>
        /// object parameter passing.
        /// </summary>
        public class ObjectPassingCommand : ICommand
        {
            public Action<object> execute;
            public ObjectPassingCommand(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 MAIModel.Commands
    {
        /// <summary>
        /// relay  command 
        /// </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();
            }
        }
    }
    
  • ScrollViewerCommand

    using System.Windows.Input;
    namespace MAIModel.Commands
    {
        /// <summary>
        /// Scroll command : The argument object passed by this constructor of this class is ScrollViewer
        /// </summary>
        class ScrollViewerCommand : ICommand
        {
            private readonly Action<object> _execute;
            private readonly Predicate<object> _canExecute;
            public ScrollViewerCommand(Action<object> execute, Predicate<object> canExecute = null)
            {
                _execute = execute ?? throw new ArgumentNullException(nameof(execute));
                _canExecute = canExecute;
            }
            public bool CanExecute(object parameter)
            {
                return _canExecute == null || _canExecute(parameter);
            }
            public void Execute(object parameter)
            {
                _execute(parameter);
            }
            public event EventHandler CanExecuteChanged
            {
                add { CommandManager.RequerySuggested += value; }
                remove { CommandManager.RequerySuggested -= value; }
            }
        }
    }
    

  • 三、总结

    • 1、通过此项目学习了WPF。
    • 2、通过此项目了解了基本的MVVM模式,WPF的数据绑定,属性变更,以及如何通过实现ICommand接口进行命令触发。
    • 3、简单实现了使用C#和OllamaAPI实现AI交互界面。
    • 4、简单调用了Markdig库 将交互数据以md格式显示。

  • 最后

    • 如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!
    • 如有疑问,欢迎评论区留言。
    • 也可以关注微信公众号 [编程笔记in] ,共同学习!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程笔记in

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值