引入AI支持(火山引擎)

This commit is contained in:
lihanbo 2025-02-28 08:41:23 +08:00
parent 07ab6fa3cc
commit a6d1c966d8
10 changed files with 797 additions and 6 deletions

74
AI/AiHelper.cs Normal file
View File

@ -0,0 +1,74 @@
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using OpenAI;
namespace ExcelHelper.AI;
class AiHelper
{
/// <summary>
/// 火山引擎
/// </summary>
private const string API_KEY = "";
private const string API_ENDPOINT = "https://ark.cn-beijing.volces.com/api/v3/";
private readonly IChatClient _client;
private bool UseTool = false;
public AiHelper(string modelName, bool useTools = false)
{
UseTool = useTools;
if (useTools)
{
//TODO: add function call
}
else
{
var client = new OpenAIClient(new ApiKeyCredential(API_KEY), new OpenAIClientOptions
{
Endpoint = new Uri(API_ENDPOINT),
});
_client = client.AsChatClient(modelName);
}
}
public IChatClient Client => _client;
public async Task<string> GetChatResponseAsync(string question)
{
var response = await _client.GetResponseAsync(question);
return response.Message.ToString();
}
public async IAsyncEnumerable<string> GetStreamingResponseAsync(string question)
{
// 获取所有的工具函数
//var toolsList = AgentExecutor.GetDefaultTools();
ChatOptions chatOptions = new ChatOptions
{
// TODO: Models config
TopK= 10,
TopP = 0.85f,
Temperature = 0.7f,
};
if (UseTool)
{
//TODO: add function call
}
await foreach (var response in _client.GetStreamingResponseAsync(question, chatOptions))
{
yield return response.Text;
}
}
}

31
AI/PromptUtil.cs Normal file
View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ExcelHelper.AI
{
class PromptUtil
{
public static string UsePrompt(string msg,string excelPrompt)
{
return $@"
Excel公式大师Excel公式相关的问题
1.
2.
3. 2~3
4.
5. 使
{DateTime.Now}
excel文件列信息为{excelPrompt}
{msg}
";
}
}
}

View File

@ -2,13 +2,18 @@
x:Class="ExcelHelper.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ExcelHelper">
xmlns:local="clr-namespace:ExcelHelper"
xmlns:local1="clr-namespace:ExcelHelper.Converter">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

68
Converter/Converters.cs Normal file
View File

@ -0,0 +1,68 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace ExcelHelper.Converter;
public class BoolToColorConverter : IValueConverter
{
public Brush UserMessageColor { get; set; } = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#DCF8C6"));
public Brush AIMessageColor { get; set; } = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ECECEC"));
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isUser)
{
return isUser ? UserMessageColor : AIMessageColor;
}
return AIMessageColor;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class BoolToAlignmentConverter : IValueConverter
{
public HorizontalAlignment TrueValue { get; set; } = HorizontalAlignment.Right;
public HorizontalAlignment FalseValue { get; set; } = HorizontalAlignment.Left;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? TrueValue : FalseValue;
}
return FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return true;
}
}

View File

@ -46,14 +46,18 @@
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="CommunityToolkit.Mvvm">
<Version>8.3.2</Version>
<Version>8.4.0</Version>
</PackageReference>
<PackageReference Include="DuckDB.NET.Bindings.Full" Version="1.1.3" />
<PackageReference Include="DuckDB.NET.Data.Full" Version="1.1.3" />
<PackageReference Include="HandyControl">
<Version>3.5.1</Version>
</PackageReference>
<PackageReference Include="MdXaml" Version="1.27.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25114.11" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3116-prerelease" />
<PackageReference Include="MiniExcel">
<Version>1.34.2</Version>
</PackageReference>
@ -69,4 +73,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0-rc.2.24473.5" />
<PackageReference Include="TinyPinyin" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Web\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,131 @@
<UserControl
x:Class="ExcelHelper.Views.Components.AiMessageControll"
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:local="clr-namespace:ExcelHelper.Views.Components"
xmlns:local1="clr-namespace:ExcelHelper.Converter"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml"
x:Name="userControl"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<!-- 将布尔值转换为颜色 (用户消息和AI消息使用不同颜色) -->
<local1:BoolToColorConverter
x:Key="BoolToColorConverter"
AIMessageColor="#ECECEC"
UserMessageColor="#DCF8C6" />
<!-- 将布尔值转换为对齐方式 (用户消息靠右AI消息靠左) -->
<local1:BoolToAlignmentConverter
x:Key="BoolToAlignmentConverter"
FalseValue="Left"
TrueValue="Right" />
<!-- 布尔值取反转换器 -->
<local1:InverseBoolConverter x:Key="InverseBoolConverter" />
<!-- 布尔值到可见性转换器 -->
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 消息显示区 -->
<ScrollViewer
Grid.Row="0"
Margin="5"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Messages, UpdateSourceTrigger=PropertyChanged, ElementName=userControl}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Margin="5"
Padding="5"
HorizontalAlignment="{Binding IsUser, Converter={StaticResource BoolToAlignmentConverter}}"
Background="{Binding IsUser, Converter={StaticResource BoolToColorConverter}}"
CornerRadius="10">
<StackPanel>
<mdxam:MarkdownScrollViewer
x:Name="Markdownview"
Width="Auto"
FontSize="12"
Markdown="{Binding Content}"
PreviewMouseWheel="Markdownview_PreviewMouseWheel" />
<TextBlock
Margin="0,5,0,0"
HorizontalAlignment="Right"
FontSize="10"
Foreground="#99000000"
Text="{Binding Timestamp, StringFormat='{}{0:HH:mm}'}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- 输入区 -->
<Grid Grid.Row="1" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
BorderBrush="#CCCCCC"
BorderThickness="1"
CornerRadius="5">
<TextBox
x:Name="MessageInput"
MinHeight="40"
MaxHeight="120"
Padding="8"
AcceptsReturn="True"
BorderThickness="0"
IsEnabled="{Binding IsWaiting, Converter={StaticResource InverseBoolConverter}, UpdateSourceTrigger=PropertyChanged, ElementName=userControl}"
KeyDown="MessageInput_KeyDown"
Text="{Binding CurrentMessage, UpdateSourceTrigger=PropertyChanged, ElementName=userControl}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Border>
<Button
Grid.Column="1"
Height="40"
Margin="5,0,0,0"
Padding="15,0"
Click="SendButton_Click"
Content="发送"
IsEnabled="{Binding IsWaiting, Converter={StaticResource InverseBoolConverter}, UpdateSourceTrigger=PropertyChanged, ElementName=userControl}" />
</Grid>
<!-- 等待指示器 -->
<Border
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="#80FFFFFF"
Visibility="{Binding IsWaiting, Converter={StaticResource BoolToVisibilityConverter}, UpdateSourceTrigger=PropertyChanged, ElementName=userControl}">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="AI正在思考中..." />
<ProgressBar
Width="100"
Height="15"
Margin="10,0,0,0"
IsIndeterminate="True" />
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,184 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Media;
namespace ExcelHelper.Views.Components
{
/// <summary>
/// AiMessageControll.xaml 的交互逻辑
/// </summary>
public partial class AiMessageControll : UserControl
{
// 消息集合
public ObservableCollection<ChatMessage> Messages
{
get
{
return (ObservableCollection<ChatMessage>)GetValue(MessagesProperty);
}
set
{
SetValue(MessagesProperty, value);
}
}
public static readonly DependencyProperty MessagesProperty =
DependencyProperty.Register("Messages", typeof(ObservableCollection<ChatMessage>), typeof(AiMessageControll),
new PropertyMetadata(new ObservableCollection<ChatMessage>()));
// 当前输入的消息
public string CurrentMessage
{
get
{
return (string)GetValue(CurrentMessageProperty);
}
set
{
SetValue(CurrentMessageProperty, value);
}
}
public static readonly DependencyProperty CurrentMessageProperty =
DependencyProperty.Register("CurrentMessage", typeof(string), typeof(AiMessageControll), new PropertyMetadata(string.Empty));
// 发送命令
public ICommand SendCommand
{
get
{
return (ICommand)GetValue(SendCommandProperty);
}
set
{
SetValue(SendCommandProperty, value);
}
}
public static readonly DependencyProperty SendCommandProperty =
DependencyProperty.Register("SendCommand", typeof(ICommand), typeof(AiMessageControll), new PropertyMetadata(null));
// 是否正在等待AI响应
public bool IsWaiting
{
get
{
return (bool)GetValue(IsWaitingProperty);
}
set
{
SetValue(IsWaitingProperty, value);
}
}
public static readonly DependencyProperty IsWaitingProperty =
DependencyProperty.Register("IsWaiting", typeof(bool), typeof(AiMessageControll), new PropertyMetadata(false));
public AiMessageControll()
{
InitializeComponent();
//DataContext = this;
}
private void SendButton_Click(object sender, RoutedEventArgs e)
{
if (SendCommand != null && SendCommand.CanExecute(CurrentMessage))
{
SendCommand.Execute(CurrentMessage);
}
}
private void MessageInput_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && !string.IsNullOrEmpty(CurrentMessage) && !IsWaiting)
{
if (SendCommand != null && SendCommand.CanExecute(CurrentMessage))
{
SendCommand.Execute(CurrentMessage);
}
e.Handled = true;
}
}
private void Markdownview_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// 将滚动事件传递给外部的 ScrollViewer
var scrollViewer = FindParent<ScrollViewer>((DependencyObject)sender);
if (scrollViewer != null)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta);
e.Handled = true;
}
}
private T FindParent<T>(DependencyObject child) where T : DependencyObject
{
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
if (parentObject == null) return null;
if (parentObject is T parent)
{
return parent;
}
else
{
return FindParent<T>(parentObject);
}
}
}
// 聊天消息类
public class ChatMessage : INotifyPropertyChanged
{
private string _content;
public string Content
{
get
{
return _content;
}
set
{
_content = value;
OnPropertyChanged(nameof(Content));
}
}
private bool _isUser;
public bool IsUser
{
get
{
return _isUser;
}
set
{
_isUser = value;
OnPropertyChanged(nameof(IsUser));
}
}
private DateTime _timestamp = DateTime.Now;
public DateTime Timestamp
{
get
{
return _timestamp;
}
set
{
_timestamp = value;
OnPropertyChanged(nameof(Timestamp));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -2,6 +2,7 @@
x:Class="ExcelHelper.Views.Pages.ImportExcelPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:components="clr-namespace:ExcelHelper.Views.Components"
xmlns:converter="clr-namespace:ExcelHelper.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
@ -52,6 +53,10 @@
Width="370"
MinWidth="370"
MaxWidth="600" />
<ColumnDefinition
Width="370"
MinWidth="370"
MaxWidth="600" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="DropFileMask"
@ -68,7 +73,7 @@
Panel.ZIndex="10"
Content="{Binding}"
ContentTemplate="{StaticResource LoadingMask}"
Visibility="{Binding IsLoading, Converter={StaticResource Boolean2VisibilityConverter}}" />
Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}" />
<hc:TabControl
Grid.Column="0"
Margin="5,5,7,5"
@ -202,6 +207,17 @@
</hc:SimpleStackPanel>
</hc:SimpleStackPanel>
</hc:Card>
<GridSplitter
Grid.Column="1"
Width="2"
Margin="0,5" />
<hc:Card Grid.Column="2">
<!-- Fix: Added Mode=OneWay -->
<components:AiMessageControll
CurrentMessage="{Binding CurrentMessage}"
IsWaiting="{Binding IsAiLoading}"
Messages="{Binding Messages}"
SendCommand="{Binding SendMessageToAiCommand}" />
</hc:Card>
</Grid>
</Page>

View File

@ -1,4 +1,4 @@
using System.Windows.Controls;
using System.Windows.Controls;
using CommunityToolkit.Mvvm.Messaging;
using ExcelHelper.Message;
using ExcelHelper.Views.ViewModels;
@ -73,4 +73,6 @@ public partial class ImportExcelPage : Page, IView, IRecipient<UpdateDataGridCol
}
}
}
}

View File

@ -7,9 +7,11 @@ using System.Windows.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ExcelHelper.AI;
using ExcelHelper.Message;
using ExcelHelper.Model;
using ExcelHelper.Utils;
using ExcelHelper.Views.Components;
using MiniExcelLibs;
using MiniExcelLibs.OpenXml;
using SqlSugar;
@ -20,6 +22,8 @@ namespace ExcelHelper.Views.ViewModels;
public partial class ImportViewModel : ObservableRecipient, IViewModel
{
private AiHelper AiHelper = new AiHelper("deepseek-v3-241226", false);
public Task FileDrop(string[] files)
{
IsLoading = true;
@ -100,7 +104,7 @@ public partial class ImportViewModel : ObservableRecipient, IViewModel
: MiniExcel.QueryRange(path, sheetName: SelectedSheetName, useHeaderRow: UseHeaderRow, startCell: StartCell, endCell: EndCell, configuration: config);
}
private void GenColumns(IEnumerable<string> columns)
{
ExcelColumns.Clear();
@ -250,6 +254,267 @@ public partial class ImportViewModel : ObservableRecipient, IViewModel
};
}
[RelayCommand]
private async Task SendMessageToAi(string message)
{
if (!string.IsNullOrEmpty(message))
{
Messages.Add(new ChatMessage
{
Content = message,
IsUser = true,
Timestamp = DateTime.Now
});
IsAiLoading = true;
var aiChat = new ChatMessage {
Timestamp = DateTime.Now,
IsUser = false,
Content = ""
};
Messages.Add(aiChat);
#if DEBUG
var results = TEST_AI_CONTENT;
foreach(var result in results)
{
aiChat.Content += result;
//await Task.Delay(1);
}
#else
var promptedMsg = PromptUtil.UsePrompt(message,PromptString);
await foreach(var result in AiHelper.GetStreamingResponseAsync(promptedMsg))
{
aiChat.Content += result;
}
#endif
IsAiLoading = false;
}
}
private const string TEST_AI_CONTENT = @"
# Welcome to Leanote! Leanote!
## 1.
**** **
~~~~
:
> Leanote官方的话, Leanote, ...
:
1. Vim
2. Emacs
:
- 1
- 2
## 2.
:
![leanote](http://leanote.com/images/logo/leanote_icon_blue.png)
:
[这是去往Leanote官方博客的链接](http://leanote.leanote.com)
## 3.
, 5
```
# h1
## h2
### h3
#### h4
##### h4
###### h5
```
## 4.
:
function get(key) {
return m[key];
}
:
``` javascript
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
var a = 1, b = 1;
var tmp;
while (--n >= 0) {
tmp = a;
a += b;
b = tmp;
}
return a;
}
document.write(fib(10));
```
```python
class Employee:
empCount = 0
def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
```
# 5. Markdown
Markdown :
*
*
* Html
*
*
*
* MathJax
## 5.1
Item | Value
-------- | ---
Computer | \$1600
Phone | \$12
Pipe | \$1
, Item列左对齐, Value列右对齐, Qty列居中对齐
| Item | Value | Qty |
| :------- | ----: | :---: |
| Computer | \$1600 | 5 |
| Phone | \$12 | 12 |
| Pipe | \$1 | 234 |
## 5.2
1
: 1
2
:
## 5.3 Html
Markdown Html Html
<table>
<tr>
<th rowspan=""2""></th>
<th></th>
<th></th>
<th></th>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
<table>
<tr>
<th rowspan=""2""></th>
<th></th>
<th></th>
<th></th>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
****, , img标签, :
<img src=""http://leanote.com/images/logo/leanote_icon_blue.png"" width=""50px"" />
## 5.4
Leanote[^footnote]
[^footnote]: Leanote是一款强大的开源云笔记产品.
## 5.5
`[TOC]` , :
[TOC]
## 5.6
```sequence
Alice->Bob: Hello Bob, how are you?
Note right of Bob: Bob thinks
Bob-->Alice: I am good thanks!
```
:
```flow
st=>start: Start
e=>end
op=>operation: My Operation
cond=>condition: Yes or No?
st->op->cond
cond(yes)->e
cond(no)->op
```
> **:** :
> - [](http://bramp.github.io/js-sequence-diagrams/)
> - [](http://adrai.github.io/flowchart.js)
## 5.7 MathJax
$
$E=mc^2$
$$
$$\sum_{i=1}^n a_i=0$$
$$f(x_1,x_x,\ldots,x_n) = x_1^2 + x_2^2 + \cdots + x_n^2 $$
$$\sum^{j-1}_{k=0}{\widehat{\gamma}_{kj} z_k}$$
:
$$
\begin{eqnarray}
\vec\nabla \times (\vec\nabla f) & = & 0 \cdots\cdots梯度场必是无旋场\\
\vec\nabla \cdot(\vec\nabla \times \vec F) & = & 0\cdots\cdots旋度场必是无散场\\
\vec\nabla \cdot (\vec\nabla f) & = & {\vec\nabla}^2f\\
\vec\nabla \times(\vec\nabla \times \vec F) & = & \vec\nabla(\vec\nabla \cdot \vec F) - {\vec\nabla}^2 \vec F\\
\end{eqnarray}
$$
访 [MathJax](http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference) 参考更多使用方法。
";
#region Props
[ObservableProperty]
private IEnumerable<dynamic> _excelData;
@ -263,12 +528,20 @@ public partial class ImportViewModel : ObservableRecipient, IViewModel
[ObservableProperty]
private ObservableCollection<TableColumnModel> _tableColumns = [];
[ObservableProperty]
private ObservableCollection<ChatMessage> _messages = [];
[ObservableProperty]
private string _currentMessage;
/// <summary>
/// 是否正在加载
/// </summary>
[ObservableProperty]
private bool _isLoading = false;
[ObservableProperty]
private bool _isAiLoading = false;
/// <summary>
/// 使用首行作为表头
/// </summary>