添加项目文件。

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

9
App.xaml Normal file
View File

@@ -0,0 +1,9 @@
<Application x:Class="YwxApp.AiChat.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YwxApp.AiChat"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

13
App.xaml.cs Normal file
View File

@@ -0,0 +1,13 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace YwxApp.AiChat;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}

10
AssemblyInfo.cs Normal file
View File

@@ -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)
)]

View File

@@ -0,0 +1,54 @@

using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
namespace YwxApp.AiChat.Commands
{
/// <summary>
/// Close Window Behavior
/// </summary>
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);
}
}
}
}

29
Commands/EventsCommand.cs Normal file
View File

@@ -0,0 +1,29 @@
using System.Windows.Input;
namespace YwxApp.AiChat.Commands
{
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; }
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Windows.Input;
namespace YwxApp.AiChat.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);
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Windows.Input;
namespace YwxApp.AiChat.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();
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Windows.Input;
namespace YwxApp.AiChat.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; }
}
}
}

View File

@@ -0,0 +1 @@


88
MainWindow.xaml Normal file
View File

@@ -0,0 +1,88 @@
<Window x:Class="YwxApp.AiChat.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:behaviors="clr-namespace:YwxApp.AiChat.Commands"
xmlns:ViewModels="clr-namespace:YwxApp.AiChat.ViewModels"
mc:Ignorable="d"
WindowStartupLocation="CenterScreen"
Title="ChatAI" Height="600" Width="800" MinHeight="600" MinWidth="800">
<Window.DataContext>
<ViewModels:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/Style/ButtonStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<i:Interaction.Behaviors>
<behaviors:ClosingWindowBehavior Command="{Binding ClosingWindowCommand}" />
</i:Interaction.Behaviors>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="*"/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
<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>
<Grid Grid.Row="1" Grid.Column="0" >
<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>
<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">
<ContentControl Content="{Binding CurrentView}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
</Grid>
<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>

19
MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,19 @@
using System.Windows;
using YwxApp.AiChat.Services;
using YwxApp.AiChat.ViewModels;
namespace YwxApp.AiChat;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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());
}
}

30
Models/ChatMessage.cs Normal file
View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YwxApp.AiChat.Models
{
/// <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";
}
}

14
Services/IChatService.cs Normal file
View File

@@ -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<string> GetResponseAsync(string prompt);
void Configure(string apiKey, string modelName = "gpt-3.5-turbo");
}
}

95
Services/OllamaService.cs Normal file
View File

@@ -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<string> 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<OllamaResponse>();
// 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<OllamaResponse>();
var result = await response.Content.ReadAsStringAsync();
MessageBox.Show(result);
return "No response";
}
public class ChatRequest
{
[JsonPropertyName("messages")]
public List<Message> Messages { get; set; } = new List<Message>();
[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; }
}
}
}

54
Services/OpenAIService.cs Normal file
View File

@@ -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<string> 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<OpenAIResponse>();
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; }
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Windows.Input;
namespace YwxApp.AiChat.Utilities
{
public class AsyncRelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
public AsyncRelayCommand(Func<Task> execute, Func<bool> 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();
}
}

View File

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

182
ViewModels/MainViewModel.cs Normal file
View File

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

View File

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

View File

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

74
Views/ChatMdView.xaml Normal file
View File

@@ -0,0 +1,74 @@
<UserControl x:Class="YwxApp.AiChat.Views.ChatMdView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources >
<ResourceDictionary>
<!--Resource dictionaryAdd 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>

37
Views/ChatMdView.xaml.cs Normal file
View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using YwxApp.AiChat.ViewModels;
namespace YwxApp.AiChat.Views
{
/// <summary>
/// ChatMdView.xaml 的交互逻辑
/// </summary>
public partial class ChatMdView : UserControl
{
public ChatMdView()
{
InitializeComponent();
}
ChatMdViewModel viewModel;
public ChatMdView(ShareOllamaObject shareOllama)
{
InitializeComponent();
viewModel = new ChatMdViewModel();
viewModel.SetOllama(shareOllama);
this.DataContext = viewModel;
}
}
}

58
Views/SettingView.xaml Normal file
View File

@@ -0,0 +1,58 @@
<UserControl x:Class="YwxApp.AiChat.Views.SettingView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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>

20
Views/SettingView.xaml.cs Normal file
View File

@@ -0,0 +1,20 @@
using System.Windows.Controls;
using YwxApp.AiChat.ViewModels;
namespace YwxApp.AiChat.Views
{
/// <summary>
/// SettingView.xaml 的交互逻辑
/// </summary>
public partial class SettingView : UserControl
{
SettingViewModel _viewModel;
public SettingView(ShareOllamaObject ollama)
{
InitializeComponent();
_viewModel = new SettingViewModel(ollama);
this.DataContext = _viewModel;
}
}
}

View File

@@ -0,0 +1,33 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="IconButtonStyle">
<Setter Property="Button.Background" Value="Transparent"/>
<Setter Property="Button.BorderBrush" Value="Transparent"/>
<Setter Property="Button.BorderThickness" Value="0"/>
<Setter Property="Button.Foreground" Value="White"/>
<Setter Property="Button.FontSize" Value="12"/>
<Setter Property="Button.FontWeight" Value="Bold"/>
<Setter Property="Button.Padding" Value="5"/>
<Setter Property="Button.Margin" Value="5"/>
<Setter Property="Button.Width" Value="150"/>
<Setter Property="Button.Height" Value="50"/>
<Setter Property="Button.HorizontalAlignment" Value="Left"/>
<Setter Property="Button.VerticalAlignment" Value="Center"/>
<Setter Property="Button.Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RoundCornerButtonStyle">
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,9 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="SearchBoxStyle">
</Style>
<Style x:Key="RoundComboBoxStyle">
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,6 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="RoundLabelStyle">
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</ResourceDictionary>

23
YwxApp.AiChat.csproj Normal file
View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig.Wpf" Version="0.5.0.1" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="OllamaSharp" Version="5.1.4" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

25
YwxApp.AiChat.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35825.156 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YwxApp.AiChat", "YwxApp.AiChat.csproj", "{E8688B66-249C-4189-A7A4-B57B7D8A8B86}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E8688B66-249C-4189-A7A4-B57B7D8A8B86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8688B66-249C-4189-A7A4-B57B7D8A8B86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8688B66-249C-4189-A7A4-B57B7D8A8B86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8688B66-249C-4189-A7A4-B57B7D8A8B86}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2C58468-159F-4EEA-8C4C-5BB5BF2071E9}
EndGlobalSection
EndGlobal

10
appsettings.json Normal file
View File

@@ -0,0 +1,10 @@
{
"OpenAIConfig": {
"ApiKey": "your-api-key",
"Model": "gpt-3.5-turbo"
},
"OllamaConfig": {
"BaseUrl": "http://192.168.1.3:11434",
"Model": "deepseek-r1:1.5b"
}
}