Compare commits

..

24 Commits
0.6.4 ... 0.6.6

Author SHA1 Message Date
zyxucp
f03649146f 调整聊天气泡尾部样式以优化外观
修改了`.chat-bubble.received::after`和`.chat-bubble.sent::after`伪元素的定位方式,将`bottom`替换为`top`,并调整了宽度从`16px`到`18px`。更新了`clip-path`属性以改进尾部形状,同时调整了`left`和`right`的偏移值以优化位置。
2025-10-05 16:11:24 +08:00
zyxucp
15fd59571f 优化 Chat 和 ChatView 样式及布局
重构了 `Chat.razor` 和 `ChatView.razor` 的样式:
- 替换内联样式为模块化 CSS 类(如 `chat-card`)。
- 优化卡片布局,新增容器类 `chat-card__content`。
- 改进滚动条样式,调整宽度、颜色和圆角。
- 为 `#chat` 添加模糊背景伪元素和阴影效果。
- 删除冗余样式规则,提升代码可维护性。
2025-10-05 16:03:18 +08:00
zyxucp
f8399887ce 优化 ChatView 组件的样式和功能
- 为 `<Image>` 组件新增 `Preview="false"` 属性,禁用头像图片预览。
- 调整输入框结构,新增 `<div class="input-bar__field">` 容器,优化布局。
- 更新 `.avatar` 样式,确保头像图片比例正确并裁剪为圆形。
- 优化 `.input-bar` 和 `.ant-input` 样式,提升输入框的视觉效果和交互体验。
- 新增 `.input-bar__field` 样式,增强输入框容器的布局和样式控制。
2025-10-05 15:40:56 +08:00
zyxucp
97548b0d2b 优化聊天界面样式与功能支持
更新聊天气泡样式,增加圆角、阴影及悬停动画,分离发送与接收消息的样式,改进消息元信息布局。
支持深色模式,优化滚动条样式,增强小屏设备的响应式设计。
调整文件上传组件样式,更新颜色变量,替换背景为单色设计,提升层次感。
改进Markdown与代码块样式,增加`.think`类样式调整,统一消息布局。
新增消息气泡弹出动画,优化输入栏样式,整体提升用户体验与视觉效果。
2025-10-05 15:31:49 +08:00
xuzeyu
d6b2c3a08b Update README.md 2025-10-04 23:34:00 +08:00
xuzeyu
3342378feb Update README.md 2025-09-02 15:23:03 +08:00
zyxucp
16303d7d92 增强函数管理功能和界面优化
在 `FunDto.cs` 中添加 `Parameters` 属性及默认值,新增 `FunParameterDto` 类以描述函数参数信息。
在 `FunList.razor` 中优化函数列表显示,使用卡片组件展示按钮和参数信息。
更新 `FunList.razor.cs` 中的依赖注入属性,确保初始化为 `default!`,并简化添加函数逻辑。
在文件上传验证中添加 `_message` 的空值检查,避免空引用异常。
2025-09-01 20:57:12 +08:00
zyxucp
49b67ce3eb 优化 ChatHistory 组件的样式和功能
在 `ChatHistory.razor` 中添加总条数显示,调整多个列的宽度。更新卡片和输入框样式,增强视觉效果。新增消息气泡样式,优化用户信息展示。修改 `IChats_Repositories` 的注入方式,简化 `InitData` 方法返回类型,并确保搜索时重置页码。
2025-09-01 20:49:27 +08:00
zyxucp
b8f8688676 Merge branch 'main' of https://github.com/AIDotNet/AntSK 2025-09-01 20:41:09 +08:00
zyxucp
60eec4ad03 优化 ChatView.razor 的样式和结构
- 增加 scrollDiv 的内边距,提升视觉效果。
- 修改消息文本显示方式,添加时间显示。
- 更新用户头像样式,统一管理。
- 改进输入框和发送按钮的样式。
- 增加 CSS 变量,改善主题和气泡样式。
- 添加气泡尾部样式,使其更具聊天气泡效果。
- 引入 .message-meta 类显示消息时间。
- 增强 Markdown 和代码块的样式支持。
- 其他小的样式调整,提升用户体验。
2025-09-01 20:41:06 +08:00
zyxucp
8f6341dd6a Update README.md 2025-07-28 00:18:52 +08:00
zyxucp
cb50062f3d Merge branch 'main' of https://github.com/AIDotNet/AntSK 2025-07-25 11:26:09 +08:00
zyxucp
293c94fbf2 update 忽略 2025-07-25 11:25:50 +08:00
zyxucp
fe8c026d13 Update README.md 2025-07-16 21:37:54 +08:00
zyxucp
b3c435be01 update 群二维码 2025-07-02 22:34:36 +08:00
zyxucp
36f7ba7931 update 样式 2025-06-22 22:54:05 +08:00
zyxucp
9461ab0aa5 样式优化 2025-06-22 22:37:14 +08:00
zyxucp
6d3f16450b readme
readme
2025-06-22 12:32:07 +08:00
zyxucp
a4d0eaad77 update readme 2025-06-22 12:03:17 +08:00
zyxucp
ecbb36bcc6 update gzh 2025-06-10 17:23:09 +08:00
zyxucp
80a5688f46 update md 2025-06-10 17:21:42 +08:00
zyxucp
65cda7dba5 update md 2025-06-10 17:19:58 +08:00
zyxucp
bfd1bd7ff1 update md 2025-06-10 17:15:26 +08:00
zyxucp
cf0d7acf8b update md 2025-06-10 17:13:34 +08:00
13 changed files with 1283 additions and 203 deletions

3
.gitignore vendored
View File

@@ -338,4 +338,5 @@ ASALocalRun/
/src/AntSK/appsettings.Development.json
/src/AntSK.db
/src/AntSK/llama_models
/src/AntSK/AntSK.xml
/src/AntSK/AntSK.xml
/src/.codebuddy/db/vectra

View File

@@ -211,7 +211,7 @@ This project follows the Apache 2.0 agreement, in addition to the following addi
3. You pre-install or integrate the software into hardware devices or products and bundle it for sale.
4. You are engaging in large-scale procurement for government or educational institutions, especially involving security, data privacy, or other sensitive requirements.
3. If you need authorization, you can contact WeChat: **xuzeyu91**
3. If you need authorization, you can contact WeChat: **13469996907**
If you plan to use AntSK in commercial projects, you need to ensure that you follow the following steps:

387
README.md
View File

@@ -2,6 +2,30 @@
# AntSK
## 使用.Net9 + Blazor+SemanticKernel 打造的AI知识库/智能体
![GitHub stars](https://img.shields.io/github/stars/AIDotNet/AntSK?style=social)
![GitHub forks](https://img.shields.io/github/forks/AIDotNet/AntSK?style=social)
![GitHub license](https://img.shields.io/github/license/AIDotNet/AntSK)
![.NET version](https://img.shields.io/badge/.NET-9.0-blue)
AntSK 是一个基于 .NET 9 和 Blazor 技术栈构建的企业级AI知识库和智能体平台集成了 Semantic Kernel 和 Kernel Memory提供完整的AI应用开发解决方案。
## 📋 目录
- [⭐ 核心功能](#核心功能)
- [🏗️ 技术架构](#技术架构)
- [🔄 系统工作流程](#系统工作流程)
- [🛠️ 技术栈](#技术栈)
- [📁 项目结构](#项目结构)
- [🚀 特色功能](#特色功能)
- [⛪ 应用场景](#应用场景)
- [✏️ 功能示例](#功能示例)
- [❓ 如何开始](#如何开始)
- [🔧 开发指南](#开发指南)
- [📊 性能优化建议](#性能优化建议)
- [💕 贡献者](#贡献者)
- [🚨 使用协议](#使用协议)
- [☎️ 联系我](#联系我)
## ⭐核心功能
- **语义内核 (Semantic Kernel)**:采用领先的自然语言处理技术,准确理解、处理和响应复杂的语义查询,为用户提供精确的信息检索和推荐服务。
@@ -27,7 +51,210 @@
- **国产信创**AntSK支持国产模型和国产数据库可以在信创条件下运行
- **模型微调**规划中基于llamafactory进行模型微调
## 🏗️ 技术架构
```mermaid
graph TB
subgraph "用户界面层"
UI[Blazor前端界面]
API[Web API接口]
end
subgraph "应用服务层"
Chat[聊天服务]
KMS[知识库服务]
Plugin[插件服务]
Model[模型管理服务]
Auth[认证服务]
end
subgraph "领域核心层"
SK[Semantic Kernel]
KM[Kernel Memory]
Embedding[向量嵌入]
Function[函数调用]
end
subgraph "基础设施层"
DB[(数据库)]
Vector[(向量数据库)]
File[文件存储]
OCR[OCR服务]
SD[StableDiffusion]
end
subgraph "AI模型层"
OpenAI[OpenAI]
Local[本地模型]
LlamaFactory[LlamaFactory]
Ollama[Ollama]
Spark[讯飞星火]
end
subgraph "插件系统"
NetPlugin[.NET插件]
APIPlugin[API插件]
FuncPlugin[函数插件]
end
UI --> Chat
UI --> KMS
UI --> Plugin
UI --> Model
API --> Auth
Chat --> SK
KMS --> KM
Plugin --> Function
SK --> OpenAI
SK --> Local
SK --> LlamaFactory
SK --> Ollama
SK --> Spark
KM --> Vector
KM --> Embedding
Chat --> NetPlugin
Chat --> APIPlugin
Chat --> FuncPlugin
KMS --> DB
KMS --> File
Model --> DB
OCR --> SD
style SK fill:#e1f5fe
style KM fill:#e8f5e8
style UI fill:#fff3e0
style API fill:#fff3e0
```
## 🔄 系统工作流程
```mermaid
graph TD
A[用户输入] --> B{输入类型}
B -->|文档上传| C[文档解析]
B -->|聊天对话| D[对话处理]
B -->|API调用| E[API处理]
C --> F[文档分块]
F --> G[向量化处理]
G --> H[存储到知识库]
D --> I{是否需要知识库}
I -->|是| J[知识库检索]
I -->|否| K[直接调用LLM]
J --> L[向量搜索]
L --> M[相关性排序]
M --> N[构建Prompt]
K --> O[LLM推理]
N --> O
O --> P{是否需要插件}
P -->|是| Q[插件调用]
P -->|否| R[生成回复]
Q --> S[执行函数]
S --> T[合并结果]
T --> R
E --> U[权限验证]
U --> V[业务逻辑]
V --> W[返回结果]
R --> X[用户界面展示]
W --> X
style A fill:#e1f5fe
style O fill:#e8f5e8
style H fill:#fff3e0
style X fill:#f3e5f5
```
## 🛠️ 技术栈
### 后端技术
- **.NET 9**: 最新的 .NET 框架,提供高性能和现代化开发体验
- **Blazor Server**: 基于服务器端渲染的现代Web UI框架
- **Semantic Kernel**: 微软开源的AI编排框架
- **Kernel Memory**: 知识库和向量存储管理
- **SqlSugar**: 高性能 ORM 框架,支持多种数据库
- **AutoMapper**: 对象映射框架
### AI & ML 技术
- **OpenAI GPT**: 支持 GPT-3.5/GPT-4 系列模型
- **Azure OpenAI**: 企业级 OpenAI 服务
- **讯飞星火**: 科大讯飞大语言模型
- **阿里云积**: 阿里云大语言模型
- **LlamaFactory**: 本地模型微调和推理
- **Ollama**: 本地模型运行环境
- **Stable Diffusion**: 文生图模型
- **BGE Embedding**: 中文向量嵌入模型
- **BGE Rerank**: 重排序模型
### 存储技术
- **PostgreSQL**: 主数据库存储
- **SQLite**: 轻量级数据库支持
- **Qdrant**: 向量数据库
- **Redis**: 缓存和向量存储
- **Disk/Memory**: 本地存储方案
### 前端技术
- **Ant Design Blazor**: 企业级UI组件库
- **Chart.js**: 数据可视化
- **Prism.js**: 代码高亮
## 📁 项目结构
```
AntSK/
├── src/
│ ├── AntSK/ # 主应用Blazor Server
│ │ ├── Components/ # 自定义组件
│ │ ├── Controllers/ # Web API控制器
│ │ ├── Pages/ # Blazor页面
│ │ │ ├── ChatPage/ # 聊天相关页面
│ │ │ ├── KmsPage/ # 知识库管理页面
│ │ │ ├── Plugin/ # 插件管理页面
│ │ │ ├── Setting/ # 系统设置页面
│ │ │ └── User/ # 用户管理页面
│ │ ├── Services/ # 应用服务
│ │ └── wwwroot/ # 静态资源
│ ├── AntSK.Domain/ # 领域层
│ │ ├── Domain/ # 领域模型和接口
│ │ ├── Repositories/ # 数据仓储
│ │ ├── Services/ # 领域服务
│ │ └── Common/ # 通用组件
│ ├── AntSK.LLM/ # LLM集成层
│ │ ├── SparkDesk/ # 讯飞星火集成
│ │ ├── StableDiffusion/ # SD文生图集成
│ │ └── Mock/ # 模拟服务
│ ├── AntSK.LLamaFactory/ # LlamaFactory集成
│ ├── AntSK.OCR/ # OCR服务
│ ├── AntSK.BackgroundTask/ # 后台任务处理
│ └── AntSK.ServiceDefaults/ # 服务默认配置
├── docs/ # 文档
└── docker-compose.yml # Docker部署文件
```
### 核心模块说明
| 模块 | 功能描述 |
|------|---------|
| **AntSK** | 主应用程序包含Blazor UI和Web API |
| **AntSK.Domain** | 领域层,包含业务逻辑、数据模型和仓储接口 |
| **AntSK.LLM** | 大语言模型集成层支持多种AI模型 |
| **AntSK.LLamaFactory** | LlamaFactory集成支持本地模型微调和推理 |
| **AntSK.OCR** | 光学字符识别服务 |
| **AntSK.BackgroundTask** | 后台任务处理,如知识库导入 |
## ⛪应用场景
@@ -63,9 +290,59 @@ AntSK 适用于多种业务场景,例如:
[在线文档http://antsk.cn](http://antsk.cn)
## 🚀 特色功能
### 🤖 多模型支持
- **云端模型**: OpenAI GPT、Azure OpenAI、讯飞星火、阿里云积灵等
- **本地模型**: 支持 Ollama 和Llamafactory运行离线模型
- **LlamaFactory**: 支持主流开源模型的微调和推理
- **Ollama**: 本地模型管理和运行
- **一键切换**: 支持在不同模型间无缝切换
### 📚 智能知识库
- **多格式支持**: Word、PDF、Excel、TXT、Markdown、JSON、PPT
- **向量化存储**: BGE-embedding 中文优化向量模型
- **智能检索**: BGE-rerank 重排序提升检索精度
- **实时同步**: 知识库内容实时更新和同步
### 🔌 开放插件系统
- **.NET 插件**: 支持 DLL 格式的原生插件
- **API 插件**: 通过 HTTP API 集成外部服务
- **函数插件**: 基于 Semantic Kernel 的函数调用
- **热插拔**: 插件动态加载,无需重启系统
### 🎨 文生图能力
- **Stable Diffusion**: 集成本地 SD 模型
- **多种后端**: 支持 CPU、CUDA、ROCm 等不同计算后端
- **参数调节**: 丰富的生成参数配置
- **批量生成**: 支持批量图片生成
### 🔍 OCR 文字识别
- **图片转文字**: 支持多种图片格式的文字提取
- **多语言支持**: 中英文等多语言识别
- **高精度**: 优化的 OCR 引擎,识别准确率高
## ❓如何开始?
在这里我使用的是Postgres 作为数据存储和向量存储因为Semantic Kernel和Kernel Memory都支持他当然你也可以换成其他的。
### 🛠️ 环境要求
- **.NET 9 SDK**: [下载地址](https://dotnet.microsoft.com/zh-cn/download/dotnet/9.0)
- **Docker** (可选): 用于容器化部署
- **Python 3.8+** (可选): 使用 LlamaFactory 时需要
### 💾 数据库支持
AntSK 支持多种数据库,通过 SqlSugar ORM 实现:
- **PostgreSQL** (推荐): 同时支持关系型数据和向量存储
- **SQLite**: 轻量级,适合开发和测试
- **MySQL**: 广泛使用的开源数据库
- **SQL Server**: 微软企业级数据库
- **Oracle**: 企业级数据库解决方案
### 🔧 向量数据库选择
- **PostgreSQL**: 使用 pgvector 扩展
- **Qdrant**: 专业向量数据库
- **Redis**: 内存向量存储
- **Disk**: 本地文件存储
- **Memory**: 内存存储 (不持久化)
模型默认支持openai、azure openai、讯飞星火、阿里云积、 和llama支持的gguf本地模型 以及llamafactory的本地模型,如果需要使用其他模型可以使用one-api进行集成。
@@ -73,8 +350,8 @@ AntSK 适用于多种业务场景,例如:
需要配置如下的配置文件
## 为了方便体验,我已经把打包好的程序放进了网盘,你只需要安装.net8环境即可运行。
[.net8环境 ](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0)
## 为了方便体验,我已经把打包好的程序放进了网盘,你只需要安装.net9环境即可运行。
[.net9环境 ](https://dotnet.microsoft.com/zh-cn/download/dotnet/9.0)
[我用夸克网盘分享了「AntSK」](https://pan.quark.cn/s/63ea02e1683e)
@@ -109,7 +386,7 @@ version: '3.8'
services:
antsk:
container_name: antsk
image: registry.cn-hangzhou.aliyuncs.com/AIDotNet/antsk:v0.6.0
image: registry.cn-hangzhou.aliyuncs.com/AIDotNet/antsk:v0.6.5
ports:
- 5000:5000
networks:
@@ -198,6 +475,74 @@ dotnet AntSK.dll
DB我使用的是CodeFirst模式只要配置好数据库链接表结构是自动创建的
## 🔧 开发指南
### 本地开发环境搭建
1. **克隆项目**
```bash
git clone https://github.com/AIDotNet/AntSK.git
cd AntSK
```
2. **安装依赖**
```bash
# 确保已安装 .NET 9 SDK
dotnet restore
```
3. **配置数据库**
- 修改 `src/AntSK/appsettings.json` 中的数据库连接字符串
- 首次运行会自动创建数据库表结构 (CodeFirst 模式)
4. **启动项目**
```bash
cd src/AntSK
dotnet run
```
访问 `https://localhost:5001``http://localhost:5000`
### 插件开发
#### .NET 插件开发
```csharp
[AntSKFunction("插件描述")]
public class MyPlugin
{
[AntSKFunction("函数描述")]
public async Task<string> MyFunction(string input)
{
// 您的业务逻辑
return "处理结果";
}
}
```
#### API 插件开发
创建符合 OpenAPI 规范的 HTTP 接口AntSK 会自动解析并集成。
### 自定义模型集成
1. **实现 IChatCompletion 接口**
```csharp
public class CustomChatCompletion : IChatCompletion
{
public async Task<IReadOnlyList<ChatMessage>> GetChatMessageContentsAsync(
ChatHistory chatHistory,
PromptExecutionSettings? executionSettings = null,
Kernel? kernel = null,
CancellationToken cancellationToken = default)
{
// 实现您的模型调用逻辑
}
}
```
2. **注册服务**
```csharp
services.AddSingleton<IChatCompletion, CustomChatCompletion>();
```
## ✔使用llamafactory
```
1、首先需要确保你的环境已经安装了python和pip如果使用镜像例如p0.2.4版本已经包含了 python全套环境则无需此步骤
@@ -209,6 +554,23 @@ DB我使用的是CodeFirst模式只要配置好数据库链接表结构是
7、点击保存然后就可以开始聊天了
8、很多人会问 LLamaSharp与llamafactory有什么区别其实这两者LLamaSharp是llama.cpp的 dotnet实现但是只支持本地gguf模型 而llamafactory 支持的模型种类更多但使用的是python的实现其主要差异在这里另外llamafactory具有模型微调的能力这也是我们下一步需要重点集成的部分。
```
## 📊 性能优化建议
### 硬件配置推荐
| 用途 | CPU | 内存 | 存储 | GPU |
|------|-----|------|------|-----|
| 开发测试 | 4核+ | 8GB+ | SSD 50GB+ | 可选 |
| 小型部署 | 8核+ | 16GB+ | SSD 100GB+ | 可选 |
| 生产环境 | 16核+ | 32GB+ | SSD 500GB+ | RTX 3080+ |
| 大规模部署 | 32核+ | 64GB+ | SSD 1TB+ | RTX 4090+ |
### 性能调优
- **数据库连接池**: 根据并发量调整连接池大小
- **向量维度**: 根据精度需求选择合适的向量维度
- **缓存策略**: 合理使用 Redis 缓存热点数据
- **模型选择**: 根据场景选择合适的模型大小

## 💕 贡献者
@@ -224,14 +586,14 @@ DB我使用的是CodeFirst模式只要配置好数据库链接表结构是
除以下附加条款外该项目遵循Apache 2.0协议
1. **免费商用**:用户在不修改代码的情况下,可以免费用于商业目的。
1. **免费商用**:用户在不修改应用名称、logo、版权信息的情况下,可以免费用于商业目的。
2. **商业授权**:如果您满足以下任意条件之一,需取得商业授权:
1. 对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)
1. 修改应用名称、logo、版权信息等
2. 为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。
3. 预装或集成到硬件设备或产品中进行捆绑销售。
4. 政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
3. 如果您需要授权,可以联系微信:xuzeyu91
3. 如果您需要授权,可以联系微信:**13469996907**
如果您打算在商业项目中使用AntSK您需要确保遵守以下步骤
@@ -241,13 +603,4 @@ DB我使用的是CodeFirst模式只要配置好数据库链接表结构是
3. 满足以上要求
## 💕 特别感谢
助力企业级AI应用开发推荐使用 [AntBlazor](https://antblazor.com)
## ☎️联系我
如有任何问题或建议请通过以下方式关注我的公众号《许泽宇的技术分享》发消息与我联系我们也有AIDotnet交流群可以发送进群等消息然后我会拉你进交流群
另外您也可以通过邮箱与我联系antskpro@qq.com
![公众号](https://github.com/AIDotNet/AntSK/blob/main/images/gzh.jpg)

View File

@@ -3,9 +3,9 @@ version: '3.8'
services:
antsk:
container_name: antsk
image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:v0.6.3
image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:v0.6.4
# 如果需要pytorch环境需要使用下面这个镜像镜像比较大
# image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:p0.6.3
# image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:p0.6.4
ports:
- 5000:5000
networks:

View File

@@ -32,9 +32,9 @@ services:
- ./pg/data:/var/lib/postgresql/data
antsk:
container_name: antsk
image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:v0.6.3
image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:v0.6.4
# 如果需要pytorch环境需要使用下面这个镜像镜像比较大
# image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:p0.6.3
# image: registry.cn-hangzhou.aliyuncs.com/xuzeyu91/antsk:p0.6.4
ports:
- 5000:5000
networks:

View File

@@ -3,7 +3,7 @@ sidebar_position: 1
---
# AntSK功能介绍
## 基于.Net8+AntBlazor+SemanticKernel 打造的AI知识库/智能体
## 基于.Net9+AntBlazor+SemanticKernel 打造的AI知识库/智能体
## 核心功能

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -8,11 +8,21 @@ namespace AntSK.Domain.Domain.Model.Fun
{
public class FunDto
{
public string Name { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; }
public string Description { get; set; } = string.Empty;
public FunType FunType { get; set; }
// 函数参数信息(用于前端展示)
public List<FunParameterDto> Parameters { get; set; } = new();
}
public class FunParameterDto
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
public enum FunType

View File

@@ -10,7 +10,7 @@
<GridRow Gutter="(16, 16)">
<GridCol Span="14">
<Card Style="height:75vh;overflow: auto;">
<Card Class="chat-card">
<TitleTemplate>
<Icon Type="setting" /> 选择应用
<Select DataSource="@_list"
@@ -23,19 +23,21 @@
<a href="@( NavigationManager.BaseUri + "openchat/" + AppId)" target="_blank">分享使用</a>
</TitleTemplate>
<Body>
@if (!string.IsNullOrEmpty(AppId))
{
<Watermark Content="AntSK">
<ChatView AppId="@AppId" ShowTitle=false OnRelevantSources="OnRelevantSources"></ChatView>
</Watermark>
}
<div class="chat-card__content">
@if (!string.IsNullOrEmpty(AppId))
{
<Watermark Content="AntSK" Class="chat-card__watermark">
<ChatView AppId="@AppId" ShowTitle=false OnRelevantSources="OnRelevantSources"></ChatView>
</Watermark>
}
</div>
</Body>
</Card>
</GridCol>
<GridCol Span="10">
<Card Style="height: 75vh;overflow: auto;">
<TitleTemplate>
<Icon Type="search" /> 调试结果
<Icon Type="search" /> 知识溯源
</TitleTemplate>
<Extra>
@@ -62,13 +64,45 @@
</GridRow>
<style>
#chat {
height: calc(75vh - 120px);
.chat-card {
height: 75vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
margin: 0;
overflow: visible;
min-height: 0;
}
.chat-card .ant-card-body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
padding: 5px;
box-sizing: border-box;
}
.chat-card__content {
position: relative;
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
min-height: 0;
}
.chat-card__watermark {
flex: 1 1 auto;
display: flex;
width: 100%;
height: 100%;
min-height: 0;
}
.chat-card__watermark > * {
flex: 1 1 auto;
min-width: 0;
height: 100%;
}
body {

View File

@@ -11,45 +11,54 @@
{
<PageHeader Class="site-page-header" Title="@app.Name" Subtitle="@app.Describe" />
}
<div id="scrollDiv" style="flex:1; width:100%; overflow-y:auto; overflow-x:hidden;padding:10px;">
<div id="scrollDiv" class="chat-scroll">
<Virtualize Items="@(MessageList.OrderBy(o => o.CreateTime).ToList())" Context="item">
@if (item.IsSend)
{
<GridRow>
<GridRow Class="message-row" Gutter="(8,8)">
<GridCol Span="23">
<div class="chat-bubble sent">
<Popover Title="@item.CreateTime.ToString()">
<Unbound>
<Flex Vertical RefBack="context">
@if (item.FileName != null)
{
<p class="message-file">
<Upload DefaultFileList="[new(){ FileName= item.FileName }]" />
</p>
}
<p>@(item.Context)</p>
</Flex>
</Unbound>
</Popover>
<div class="message-shell sent">
<div class="chat-bubble sent">
<Popover Title="@item.CreateTime.ToString()">
<Unbound>
<Flex Vertical RefBack="context" Class="bubble-body">
@if (item.FileName != null)
{
<p class="message-file">
<Upload DefaultFileList="[new(){ FileName= item.FileName }]" />
</p>
}
<p class="bubble-text">@(item.Context)</p>
</Flex>
</Unbound>
</Popover>
</div>
<div class="message-meta meta-right">
<span>@item.CreateTime.ToString("HH:mm")</span>
<Icon Class="meta-action" Type="copy" Theme="outline" OnClick="async () =>await OnCopyAsync(item)" />
</div>
</div>
<Icon Style="float:right;margin-top:10px;" Type="copy" Theme="outline" OnClick="async () =>await OnCopyAsync(item)" />
</GridCol>
<GridCol Span="1">
<Image Width="25px" Height="25px" Style="margin-top:10px;" Src="./assets/KDpgvguMpGfqaHPjicRK.svg" />
<GridCol Span="1" Class="avatar-col">
<Image Class="avatar" Width="32px" Height="32px" Src="./assets/KDpgvguMpGfqaHPjicRK.svg" Preview="false" />
</GridCol>
</GridRow>
}
else
{
<GridRow>
<GridCol Span="1">
<Image Width="25px" Height="25px" Style="margin-top:10px;" Src="./assets/method-draw-image.svg" />
<GridRow Class="message-row" Gutter="(8,8)">
<GridCol Span="1" Class="avatar-col">
<Image Class="avatar" Width="32px" Height="32px" Src="./assets/method-draw-image.svg" Preview="false" />
</GridCol>
<GridCol Span="23">
<div class="chat-bubble received">
@((MarkupString)(item.Context))
<div class="message-shell received">
<div class="chat-bubble received">
@((MarkupString)(item.Context))
</div>
<div class="message-meta meta-left">
<span>@item.CreateTime.ToString("HH:mm")</span>
</div>
</div>
</GridCol>
</GridRow>
}
@@ -62,9 +71,10 @@
<Upload DefaultFileList="fileList" OnRemove="HandleFileRemove" />
</Flex>
}
<Flex Justify="end">
<AntDesign.Input @bind-Value="@(_messageInput)" DebounceMilliseconds="@(-1)" Placeholder="输入消息回车发送" OnPressEnter="@(async () => await OnSendAsync())" Disabled="@Sendding"></AntDesign.Input>
<Flex Class="input-bar" Justify="end" Align="center">
<div class="input-bar__field">
<AntDesign.Input @bind-Value="@(_messageInput)" DebounceMilliseconds="@(-1)" Placeholder="输入消息回车发送" OnPressEnter="@(async () => await OnSendAsync())" Disabled="@Sendding"></AntDesign.Input>
</div>
@if (app.EmbeddingModelID != null)
{
<Upload Action="@("api/File/UploadFile")"
@@ -77,65 +87,376 @@
</Upload>
}
<Button Icon="clear" Type="@(ButtonType.Link)" OnClick="@(async () => await OnClearAsync())" Disabled="@Sendding"></Button>
<Button Icon="send" Type="@(ButtonType.Link)" OnClick="@(async () => await OnSendAsync())" Disabled="@Sendding"></Button>
<Button Icon="send" Type="@(ButtonType.Primary)" OnClick="@(async () => await OnSendAsync())" Disabled="@Sendding"></Button>
</Flex>
</div>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
justify-content: center;
align-items: flex-start;
height: 100vh;
:root {
--surface-base: #ffffff;
--surface-elevated: #f5f7fb;
--border-subtle: rgba(15, 23, 42, 0.07);
--shadow-soft: 0 16px 28px rgba(15, 23, 42, 0.08);
--primary-color: var(--ant-primary-color, #1677ff);
--primary-soft: rgba(22, 119, 255, 0.12);
--bubble-recv: #ffffff;
--bubble-border-sent: rgba(22, 119, 255, 0.25);
--text-primary: #1f2329;
--text-secondary: #7a808a;
}
.chat-container {
width: 350px;
border: 1px solid #ccc;
border-radius: 5px;
overflow: hidden;
@@supports (color: color-mix(in srgb, white 50%, black 50%)) {
:root {
--bubble-sent: color-mix(in srgb, var(--primary-color) 28%, white);
}
}
#chat {
position: relative;
isolation: isolate;
z-index: 0;
display: flex;
flex-direction: column;
background-color: #fff;
padding-bottom: 15px;
gap: 8px;
flex: 1;
height: 100%;
width: 100%;
min-height: min(65vh, 100%);
max-height: 100%;
background: var(--surface-base);
border-radius: 16px 16px 16px 16px;
padding: 12px 12px 0 12px;
box-sizing: border-box;
border: 1px solid var(--border-subtle);
}
#chat::after {
content: "";
position: absolute;
left: 32px;
right: 32px;
bottom: -18px;
height: 40px;
background: rgba(15, 23, 42, 0.22);
filter: blur(24px);
border-radius: 50%;
pointer-events: none;
z-index: -1;
}
.chat-scroll {
flex: 1;
width: 100%;
min-height: 0;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px 20px;
background: var(--surface-base);
border-radius: 16px;
box-sizing: border-box;
scroll-behavior: smooth;
scrollbar-gutter: stable both-edges;
}
.chat-scroll::-webkit-scrollbar {
width: 6px;
}
.chat-scroll::-webkit-scrollbar-thumb {
background: rgba(22, 119, 255, 0.24);
border-radius: 8px;
}
.chat-scroll::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.05);
border-radius: 8px;
}
.message-row {
margin-bottom: 4px;
}
.message-shell {
display: flex;
flex-direction: column;
gap: 6px;
}
.message-shell.sent {
align-items: flex-end;
}
.message-shell.received {
align-items: flex-start;
}
.chat-bubble {
padding: 10px;
margin: 10px;
margin-bottom: 0;
border-radius: 5px;
max-width: 70%;
display: inline-flex;
flex-direction: column;
gap: 6px;
padding: 14px 16px;
border-radius: 16px;
max-width: 78%;
position: relative;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
border: none;
word-break: break-word;
line-height: 1.65;
animation: pop .2s ease-out;
transition: transform .2s ease, box-shadow .2s ease;
}
.received {
background-color: #f0f0f0;
align-self: flex-start;
float: left;
.chat-bubble:hover {
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.12);
}
.sent {
background-color: #daf8cb;
align-self: flex-end;
float: right;
.chat-bubble .bubble-text {
margin: 0;
white-space: pre-wrap;
color: var(--text-primary);
}
.ant-card-body {
height: 90% !important;
.chat-bubble .bubble-body {
gap: 10px;
}
.chat-bubble.received {
background: var(--bubble-recv);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06), inset 0 0 0 1px rgba(15, 23, 42, 0.05);
}
.chat-bubble.sent {
background: var(--bubble-sent);
color: #0c1a33;
box-shadow: 0 12px 26px rgba(22, 119, 255, 0.18);
}
.chat-bubble.sent .bubble-text {
color: #102347;
}
/* bubble tails */
.chat-bubble.received::after,
.chat-bubble.sent::after {
content: "";
position: absolute;
top: 16px;
bottom: auto;
width: 18px;
height: 18px;
transform: none;
border-radius: 0;
box-shadow: none !important;
filter: none !important;
}
.chat-bubble.received::after {
left: -14px;
background: var(--bubble-recv);
clip-path: polygon(100% 0, 0 0, 100% 100%);
}
.chat-bubble.sent::after {
right: -14px;
background: var(--bubble-sent);
clip-path: polygon(0 0, 100% 0, 0 100%);
}
.message-meta {
font-size: 12px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.meta-right {
justify-content: flex-end;
}
.meta-left {
justify-content: flex-start;
padding-left: 4px;
}
.meta-action {
color: var(--text-secondary);
cursor: pointer;
transition: color .2s ease;
}
.meta-action:hover {
color: var(--primary-color);
}
.avatar-col {
display: flex;
justify-content: center;
}
.avatar {
width: 32px;
height: 32px;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.12) inset, 0 6px 16px rgba(15, 23, 42, 0.12);
}
.input-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
background: var(--surface-base);
position: sticky;
bottom: 0;
box-shadow: 0 -12px 24px rgba(15, 23, 42, 0.06);
border-radius: 16px;
}
.input-bar .ant-input {
border-radius: 999px;
padding: 10px 16px;
}
.input-bar__field {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
padding: 2px;
border-radius: 999px;
background: var(--surface-elevated);
border: 1px solid var(--border-subtle);
}
.input-bar__field .ant-input {
width: 100%;
border: none;
background: transparent;
box-shadow: none;
height: 100%;
}
/* markdown & code inside bubbles */
.chat-bubble pre {
background: #0b1021;
color: #e6e6e6;
padding: 12px;
border-radius: 12px;
overflow: auto;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.chat-bubble code {
background: rgba(27, 31, 35, 0.08);
padding: .15rem .35rem;
border-radius: 6px;
}
.chat-bubble pre code {
background: transparent;
padding: 0;
}
.chat-bubble table {
width: 100%;
border-collapse: collapse;
}
.chat-bubble table td,
.chat-bubble table th {
border: 1px solid rgba(15, 23, 42, 0.08);
padding: 6px 8px;
}
.think {
color: gray;
color: rgba(22, 119, 255, 0.9);
font-style: italic;
text-align: left !important;
display: block;
margin-top: 5px;
margin-left: 8px;
padding-left: 8px;
border-left: 2px solid #7F7FFF;
padding-left: 10px;
border-left: 3px solid rgba(22, 119, 255, 0.4);
}
.message-file {
margin: 0;
border-radius: 10px;
overflow: hidden;
}
.message-file .ant-upload-list-item {
background: rgba(22, 119, 255, 0.08);
border: none !important;
}
@@media (max-width: 992px) {
#chat {
border-radius: 12px;
padding: 10px 10px 0;
}
.chat-bubble {
max-width: 88%;
}
.input-bar {
flex-wrap: wrap;
}
}
@@media (prefers-color-scheme: dark) {
:root {
--surface-base: #111827;
--surface-elevated: #1f2937;
--border-subtle: rgba(148, 163, 184, 0.16);
--shadow-soft: 0 18px 34px rgba(0, 0, 0, 0.45);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--bubble-recv: #1f2937;
}
#chat {
background: var(--surface-elevated);
box-shadow: 0 28px 48px -24px rgba(0, 0, 0, 0.68);
}
#chat::after {
background: rgba(15, 23, 42, 0.55);
}
.chat-scroll {
background: transparent;
}
.chat-bubble {
border: 1px solid rgba(148, 163, 184, 0.16);
}
.chat-bubble.received::after {
background: var(--bubble-recv);
border: none;
}
.message-file .ant-upload-list-item {
background: rgba(22, 119, 255, 0.16);
}
.input-bar {
background: var(--surface-elevated);
}
}
@@keyframes pop {
from { transform: translateY(6px); opacity: .55; }
to { transform: translateY(0); opacity: 1; }
}
</style>

View File

@@ -3,7 +3,6 @@
@using AntSK.Domain.Domain.Model.Enum
@using AntSK.Domain.Domain.Model.Fun
@page "/plugins/funlist"
@inject NavigationManager NavigationManager
@using AntSK.Services.Auth
@inherits AuthComponentBase
@@ -26,16 +25,20 @@
<ListItem NoFlex>
@if (string.IsNullOrEmpty(context.Name))
{
<Button Type="dashed" class="newButton" @onclick="AddFun">
<Icon Type="plus" Theme="outline" /> 创建函数
</Button>
<Button Type="dashed" class="newButton" @onclick="ClearFun">
<Icon Type="clear" Theme="outline" /> 清空导入函数
</Button>
<Card Hoverable Bordered Class="card" Style="height:160px; display:flex; align-items:center; justify-content:center;">
<div style="display:flex; gap:12px; flex-wrap:wrap; justify-content:center;">
<Button Type="dashed" class="newButton" @onclick="AddFun">
<Icon Type="plus" Theme="outline" /> 创建函数
</Button>
<Button Type="dashed" class="newButton" @onclick="ClearFun">
<Icon Type="clear" Theme="outline" /> 清空导入函数
</Button>
</div>
</Card>
}
else
{
<Card Hoverable Bordered Class="card" Style="max-height:247px;">
<Card Hoverable Bordered Class="card" Style="height:160px; overflow:hidden;">
<CardMeta>
<AvatarTemplate>
@@ -44,10 +47,29 @@
<a>@context.Name</a>
</TitleTemplate>
<DescriptionTemplate>
<Paragraph class="item" Ellipsis>
<Paragraph class="item" Ellipsis Style="margin-bottom:8px;">
<!--todo: Ellipsis not working-->
@context.Description
</Paragraph>
@if (context.Parameters?.Count > 0)
{
<div style="margin-top:8px; max-height:140px; overflow:auto; padding-right:4px;">
<span style="font-weight:600;">参数:</span>
<ul style="margin: 6px 0 0 18px; padding:0;">
@foreach (var p in context.Parameters)
{
<li style="list-style: disc;">
<span style="color:#3b3b3b">@p.Name</span>
<span style="color:#999"> (@p.Type)</span>
@if (!string.IsNullOrWhiteSpace(p.Description))
{
<span style="color:#666"> - @p.Description</span>
}
</li>
}
</ul>
</div>
}
</DescriptionTemplate>
</CardMeta>
</Card>

View File

@@ -17,18 +17,18 @@ namespace AntSK.Pages.FunPage
{
private FunDto[] _data = { };
[Inject]
FunctionService _functionService { get; set; }
[Inject]
IServiceProvider _serviceProvider { get; set; }
[Inject]
IConfirmService _confirmService { get; set; }
[Inject]
IFuns_Repositories _funs_Repositories { get; set; }
[Inject]
FunctionService _functionService { get; set; } = default!;
[Inject]
IServiceProvider _serviceProvider { get; set; } = default!;
[Inject]
IConfirmService _confirmService { get; set; } = default!;
[Inject]
IFuns_Repositories _funs_Repositories { get; set; } = default!;
[Inject]
protected MessageService? _message { get; set; }
[Inject] protected ILogger<FunDto> _logger { get; set; }
[Inject] protected ILogger<FunDto> _logger { get; set; } = default!;
bool _fileVisible = false;
bool _fileConfirmLoading = false;
@@ -56,7 +56,19 @@ namespace AntSK.Pages.FunPage
foreach (var func in funList)
{
var methodInfo = _functionService.MethodInfos[func.Key];
list.Add(new FunDto() { Name = func.Key, Description = methodInfo.Description });
var paramDtos = methodInfo.Parameters?.Select(p => new FunParameterDto
{
Name = p.ParameterName ?? string.Empty,
Type = (p.ParameterType?.IsClass ?? false) ? "object" : (p.ParameterType?.Name ?? "string"),
Description = p.Description ?? string.Empty
}).ToList() ?? new List<FunParameterDto>();
list.Add(new FunDto()
{
Name = func.Key,
Description = methodInfo.Description,
Parameters = paramDtos
});
}
_data = list.ToArray();
await InvokeAsync(StateHasChanged);
@@ -72,9 +84,7 @@ namespace AntSK.Pages.FunPage
await InitData(searchKey);
}
private async Task AddFun() {
_fileVisible = true;
}
private Task AddFun() { _fileVisible = true; return Task.CompletedTask; }
private async Task ClearFun()
{
var content = "清空自定义函数将会删除全部导入函数并且需要程序重启后下次生效如不是DLL冲突等原因不建议清空是否要清空";
@@ -97,7 +107,10 @@ namespace AntSK.Pages.FunPage
_funs_Repositories.Insert(new Funs() { Id = Guid.NewGuid().ToString(), Path = file.FilePath });
_functionService.FuncLoad(file.FilePath);
}
_message.Info("上传成功");
if (_message is not null)
{
await _message.Info("上传成功");
}
await InitData("");
_fileVisible = false;
}
@@ -119,12 +132,12 @@ namespace AntSK.Pages.FunPage
{
if (file.Ext != ".dll")
{
_message.Error("请上传dll文件!");
_message?.Error("请上传dll文件!");
}
var IsLt500K = file.Size < 1024 * 1024 * 100;
if (!IsLt500K)
{
_message.Error("文件需不大于100MB!");
_message?.Error("文件需不大于100MB!");
}
return IsLt500K;

View File

@@ -1,4 +1,4 @@
@namespace AntSK.Pages.Setting.AIModel
@namespace AntSK.Pages.Setting.ChatHistory
@page "/setting/chathistory"
@using AntSK.Services.Auth
@inherits AuthComponentBase
@@ -9,53 +9,359 @@
@using AntSK.Domain.Common.Map
@attribute [Authorize(Roles = "AntSKAdmin")]
<Table @ref="table"
TItem="ChatsDto"
DataSource="@chatDtoList"
Total="_total"
@bind-PageIndex="_pageIndex"
@bind-PageSize="_pageSize"
OnChange="OnChange"
Size="TableSize.Small"
RowKey="x=>x.Id">
<TitleTemplate>
<GridRow>
<GridCol Span="4">
<Title Level="3">聊天记录</Title>
</GridCol>
<GridCol Span="8" Offset="12">
<Search Placeholder="搜索" @bind-Value="searchString" OnSearch="Search" />
</GridCol>
</GridRow>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c=>c.Id" />
<PropertyColumn Title="用户名" Property="c=>c.UserName" />
<PropertyColumn Title="应用名称" Property="c=>c.AppName" />
<PropertyColumn Title="发送/接收" Property="c=>c.SendReveice" />
<ActionColumn Title="消息内容" Width="30%">
<Space>
<SpaceItem>
@((MarkupString)(context.Context))
</SpaceItem>
</Space>
</ActionColumn>
<PropertyColumn Title="时间" Property="c=>c.CreateTime" Format="yyyy-MM-dd HH:mm:ss" />
</ColumnDefinitions>
</Table>
<div class="chat-history-container">
<Card>
<TitleTemplate>
<div class="page-header">
<div class="header-left">
<Icon Type="message" Theme="outline" />
<Title Level="2" style="margin-bottom: 0; margin-left: 8px;">聊天记录</Title>
</div>
<div class="header-right">
<Search
Placeholder="搜索用户名或消息内容"
@bind-Value="searchString"
OnSearch="Search"
EnterButton="true"
Size="@InputSize.Large"
style="width: 300px;" />
<span class="total-text">共 @_total 条</span>
</div>
</div>
</TitleTemplate>
<Body>
<Table @ref="table"
TItem="ChatsDto"
DataSource="@chatDtoList"
Total="_total"
@bind-PageIndex="_pageIndex"
@bind-PageSize="_pageSize"
OnChange="OnChange"
Size="TableSize.Middle"
RowKey="x=>x.Id"
Bordered="true"
Loading="_loading">
<ColumnDefinitions>
<PropertyColumn Property="c=>c.Id" Width="80px">
<TitleTemplate>
<strong>ID</strong>
</TitleTemplate>
</PropertyColumn>
<PropertyColumn Title="用户" Property="c=>c.UserName" Width="160px">
<TitleTemplate>
<strong><Icon Type="user" /> 用户</strong>
</TitleTemplate>
</PropertyColumn>
<PropertyColumn Title="应用" Property="c=>c.AppName" Width="150px">
<TitleTemplate>
<strong><Icon Type="appstore" /> 应用</strong>
</TitleTemplate>
</PropertyColumn>
<PropertyColumn Title="类型" Property="c=>c.SendReveice" Width="120px">
<TitleTemplate>
<strong><Icon Type="swap" /> 类型</strong>
</TitleTemplate>
</PropertyColumn>
<PropertyColumn Title="消息内容" Property="c=>c.Context" Width="40%">
<TitleTemplate>
<strong><Icon Type="message" /> 消息内容</strong>
</TitleTemplate>
</PropertyColumn>
<PropertyColumn Title="时间" Property="c=>c.CreateTime" Format="yyyy-MM-dd HH:mm:ss" Width="200px">
<TitleTemplate>
<strong><Icon Type="clock-circle" /> 时间</strong>
</TitleTemplate>
</PropertyColumn>
</ColumnDefinitions>
</Table>
</Body>
</Card>
</div>
<style>
.chat-history-container {
padding: 24px;
min-height: 100vh;
}
.ant-card {
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 12px 0 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.header-left {
display: flex;
align-items: center;
}
.header-left .anticon {
font-size: 26px;
color: #1677ff;
margin-right: 12px;
}
.header-left .ant-typography {
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
margin: 0;
}
.header-right {
display: flex;
align-items: center;
}
.total-text {
margin-left: 12px;
color: rgba(0,0,0,.45);
font-size: 12px;
}
.ant-input-search-large {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.12);
transition: all 0.3s ease;
}
.ant-input-affix-wrapper-focused {
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12);
border-color: #1677ff !important;
}
.ant-table {
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.ant-table-thead > tr > th {
background-color: #fafafa;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
color: rgba(0,0,0,.85);
letter-spacing: .2px;
}
.ant-table-tbody > tr {
transition: all 0.3s ease;
}
.ant-table-tbody > tr:hover > td {
background-color: #f5f5f5;
}
.ant-table-tbody > tr > td {
padding: 12px 16px;
vertical-align: top;
}
.message-content {
max-width: 600px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal;
}
.message-content .ant-typography {
margin: 0;
line-height: 1.6;
}
.bubble {
background: linear-gradient(180deg, #fafafa 0%, #fff 100%);
padding: 12px 14px;
border-radius: 10px;
border: 1px solid #f0f0f0;
position: relative;
}
.bubble::before {
content: '';
position: absolute;
left: -6px;
top: 14px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #f0f0f0;
}
.bubble::after {
content: '';
position: absolute;
left: -5px;
top: 14px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #fff;
}
.time-cell {
display: inline-flex;
align-items: center;
gap: 8px;
background: #fafafa;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #e8e8e8;
}
.time-cell .date {
font-size: 13px;
color: #595959;
font-weight: 600;
margin-bottom: 4px;
}
.time-cell .time {
font-size: 11px;
color: #8c8c8c;
font-family: 'Courier New', monospace;
}
.time-text {
font-size: 12px;
color: #595959;
letter-spacing: 0.2px;
}
.ant-tag {
margin: 0;
border-radius: 6px;
font-weight: 500;
padding: 4px 10px;
border: 1px solid rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
.type-tag {
box-shadow: 0 2px 6px rgba(0,0,0,.06);
}
.app-tag {
background: #f0f5ff;
color: #2f54eb;
border-color: #adc6ff;
}
.user-cell {
display: inline-flex;
align-items: center;
gap: 8px;
}
.user-name {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
}
.avatar-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: #e6f4ff;
color: #1677ff;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px;
box-sizing: border-box;
}
/* 响应式设计 */
@@media (max-width: 1200px) {
.message-content {
max-width: 420px;
}
}
@@media (max-width: 768px) {
.chat-history-container {
padding: 12px;
}
.page-header {
flex-direction: column;
gap: 16px;
padding: 16px 0;
}
.header-left .ant-typography {
font-size: 20px;
}
.header-right {
width: 100%;
}
.header-right .ant-input-search {
width: 100% !important;
}
.message-content {
max-width: 100%;
}
.user-name { max-width: 160px; }
}
@@media (max-width: 480px) {
.message-content {
max-width: 100%;
}
.time-cell {
padding: 4px;
}
.time-cell .date {
font-size: 11px;
}
.time-cell .time {
font-size: 10px;
}
.user-name { max-width: 120px; }
}
</style>
@using System.Text.Json;
@code {
[Inject] IChats_Repositories _chats_Repositories { get; set; }
[Inject] IChats_Repositories _chats_Repositories { get; set; } = default!;
ChatsDto[] chatDtoList;
ITable table;
ChatsDto[] chatDtoList = Array.Empty<ChatsDto>();
ITable? table;
bool _loading = false;
int _pageIndex = 1;
int _pageSize = 10;
int _total = 0;
string searchString;
string searchString = string.Empty;
protected override async Task OnInitializedAsync()
{
@@ -64,58 +370,78 @@
public async Task OnChange(QueryModel<ChatsDto> queryModel)
{
_loading = true;
StateHasChanged();
await InitData();
_loading = false;
StateHasChanged();
}
private async Task InitData()
private Task InitData()
{
if (string.IsNullOrEmpty(searchString))
try
{
_total = _chats_Repositories.Count(p => true);
var chatList = _chats_Repositories.GetDB().Queryable<Chats, Apps>((c, a) => new object[] {
SqlSugar.JoinType.Left,c.AppId==a.Id
}).Select((c, a) => new ChatsDto
{
Id = c.Id,
UserName = c.UserName,
AppId = c.AppId,
IsSend = c.IsSend,
SendReveice = c.IsSend ? "发送" : "接收",
Context = c.Context,
CreateTime = c.CreateTime,
AppName = a.Name
}).ToPageList(_pageIndex, _pageSize);
chatDtoList = chatList.ToArray();
}
else
{
_total = _chats_Repositories.Count(p => p.UserName.Contains(searchString) || p.Context.Contains(searchString));
var chatList = _chats_Repositories.GetDB().Queryable<Chats, Apps>((c, a) => new object[] {
SqlSugar.JoinType.Left,c.AppId==a.Id
}).Select((c, a) => new ChatsDto
if (string.IsNullOrEmpty(searchString))
{
Id = c.Id,
UserName = c.UserName,
AppId = c.AppId,
IsSend = c.IsSend,
SendReveice = c.IsSend ? "发送" : "接收",
Context = c.Context,
CreateTime = c.CreateTime,
AppName = a.Name
}).Where(c => c.UserName.Contains(searchString) || c.Context.Contains(searchString)).ToPageList(_pageIndex, _pageSize);
chatDtoList = chatList.ToArray();
_total = _chats_Repositories.Count(p => true);
var chatList = _chats_Repositories.GetDB().Queryable<Chats, Apps>((c, a) => new object[] {
SqlSugar.JoinType.Left, c.AppId == a.Id
}).Select((c, a) => new ChatsDto
{
Id = c.Id,
UserName = c.UserName,
AppId = c.AppId,
IsSend = c.IsSend,
SendReveice = c.IsSend ? "发送" : "接收",
Context = c.Context,
CreateTime = c.CreateTime,
AppName = a.Name ?? "未知应用"
}).OrderByDescending(c => c.CreateTime).ToPageList(_pageIndex, _pageSize);
chatDtoList = chatList.ToArray();
}
else
{
_total = _chats_Repositories.Count(p => p.UserName.Contains(searchString) || p.Context.Contains(searchString));
var chatList = _chats_Repositories.GetDB().Queryable<Chats, Apps>((c, a) => new object[] {
SqlSugar.JoinType.Left, c.AppId == a.Id
}).Select((c, a) => new ChatsDto
{
Id = c.Id,
UserName = c.UserName,
AppId = c.AppId,
IsSend = c.IsSend,
SendReveice = c.IsSend ? "发送" : "接收",
Context = c.Context,
CreateTime = c.CreateTime,
AppName = a.Name ?? "未知应用"
}).Where(c => c.UserName.Contains(searchString) || c.Context.Contains(searchString))
.OrderByDescending(c => c.CreateTime).ToPageList(_pageIndex, _pageSize);
chatDtoList = chatList.ToArray();
}
}
catch
{
// 处理异常,可以添加日志记录
chatDtoList = Array.Empty<ChatsDto>();
}
return Task.CompletedTask;
}
private async Task Search(string searchKey)
{
_pageIndex = 1; // 重置到第一页
_loading = true;
StateHasChanged();
await InitData();
_loading = false;
StateHasChanged();
}
public class ChatsDto : Chats
{
public string AppName { get; set; }
public string SendReveice { get; set; }
public string AppName { get; set; } = string.Empty;
public string SendReveice { get; set; } = string.Empty;
}
}
}