This commit is contained in:
Archer
2023-10-30 13:26:42 +08:00
committed by GitHub
parent 008d0af010
commit 60ee160131
216 changed files with 4429 additions and 2229 deletions

View File

@@ -47,7 +47,7 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
1. 强大的可视化编排,轻松构建 AI 应用
- [x] 提供简易模式,无需操作编排
- [x] 用户对话前引导, 全局字符串变量
- [x] 用户对话前引导全局字符串变量
- [x] 知识库搜索
- [x] 多 LLM 模型对话
- [x] 文本内容提取成结构化数据
@@ -56,12 +56,12 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
- [x] 对话下一步指引
- [ ] 对话多路线选择
- [x] 源文件引用追踪
- [ ] 自定义文件阅读器
- [x] 模块封装,实现多级复用
2. 丰富的知识库预处理
- [x] 多库复用,混用
- [x] chunk 记录修改和删除
- [x] 支持 手动输入, 直接分段, QA 拆分导入
- [x] 支持 url 读取、 CSV 批量导入
- [x] 支持手动输入直接分段QA 拆分导入
- [x] 支持 url 读取、CSV 批量导入
- [x] 支持知识库单独设置向量模型
- [x] 源文件存储
- [ ] 文件学习 Agent
@@ -71,7 +71,7 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
- [x] 完整上下文呈现
- [x] 完整模块中间值呈现
4. OpenAPI
- [x] completions 接口对齐 GPT 接口
- [x] completions 接口 (对齐 GPT 接口)
- [ ] 知识库 CRUD
5. 运营功能
- [x] 免登录分享窗口
@@ -80,7 +80,7 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
## 👨‍💻 开发
项目技术栈: NextJs + TS + ChakraUI + Mongo + PostgresVector 插件
项目技术栈NextJs + TS + ChakraUI + Mongo + Postgres (Vector 插件)
- **⚡ 快速部署**
@@ -95,7 +95,8 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
* [系统配置文件说明](https://doc.fastgpt.in/docs/development/configuration/)
* [多模型配置](https://doc.fastgpt.in/docs/installation/one-api/)
* [版本更新/升级介绍](https://doc.fastgpt.in/docs/installation/upgrading)
* [API 文档](https://doc.fastgpt.in/docs/development/openapi/)
* [OpenAPI API 文档](https://doc.fastgpt.in/docs/development/openapi/)
* [知识库结构详解](https://doc.fastgpt.in/docs/use-cases/datasetengine/)
## 🏘️ 社区交流群
@@ -105,10 +106,10 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
## 💪 相关项目
- [Laf: 3 分钟快速接入三方应用](https://github.com/labring/laf)
- [Sealos: 快速部署集群应用](https://github.com/labring/sealos)
- [One API: 多模型管理,支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
- [TuShan: 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
- [Laf3 分钟快速接入三方应用](https://github.com/labring/laf)
- [Sealos快速部署集群应用](https://github.com/labring/sealos)
- [One API多模型管理,支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
- [TuShan5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
## 👀 其他
@@ -129,6 +130,6 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
本仓库遵循 [FastGPT Open Source License](./LICENSE) 开源协议。
1. 允许作为后台服务直接商用,但不允许提供 SaaS 服务。
2. 需保留相关版权信息。
2. 未经商业授权,任何形式的商用服务均需保留相关版权信息。
3. 完整请查看 [FastGPT Open Source License](./LICENSE)
4. 联系方式yujinlong@sealos.io, [点击查看定价策略](https://doc.fastgpt.run/docs/commercial)
4. 联系方式yujinlong@sealos.io[点击查看商业版定价策略](https://doc.fastgpt.run/docs/commercial)

View File

@@ -54,9 +54,9 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
- [x] Extend with HTTP
- [ ] Embed Laf for on-the-fly HTTP module crafting
- [x] Directions for the next dialogue steps
- [ ] Multiple dialogue paths selection
- [x] Tracking source file references
- [ ] Custom file reader
- [ ] Modules are packaged into plug-ins to achieve reuse
2. Extensive knowledge base preprocessing

1
docSite/.zhlintignore Normal file
View File

@@ -0,0 +1 @@
*.html

6
docSite/.zhlintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"preset": "default",
"rules": {
"adjustedFullWidthPunctuation": ""
}
}

View File

@@ -3,7 +3,7 @@
## 本地运行
1. 安装 go 语言环境。
2. 安装 hugo。 [二进制下载](https://github.com/gohugoio/hugo/releases/tag/v0.117.0),注意需要安装 extended 版本。
2. 安装 hugo。[二进制下载](https://github.com/gohugoio/hugo/releases/tag/v0.117.0),注意需要安装 extended 版本。
3. cd docSite
4. hugo serve
5. 访问 http://localhost:1313

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

View File

@@ -4,7 +4,7 @@ description: 'FastGPT V4.5.1 更新'
icon: 'upgrade'
draft: false
toc: true
weight: 839
weight: 838
---
## 执行初始化 API

View File

@@ -0,0 +1,15 @@
---
title: 'V4.5.2'
description: 'FastGPT V4.5.2 更新'
icon: 'upgrade'
draft: false
toc: true
weight: 837
---
## 功能介绍
### Fast GPT V4.5.2
1. 新增 - 模块插件,允许自行组装插件进行模块复用。
2. 优化 - 知识库引用提示。

View File

@@ -39,16 +39,18 @@ weight: 310
```
{{% alert icon="🍅" context="success" %}}
Tips: 可以通过点击上下文按键查看完整的
Tips: 可以通过点击上下文按键查看完整的上下文组成,便于调试。
{{% /alert %}}
## 引用模板和提示词设计
引用模板和引用提示词通常是成对出现,引用提示词依赖引用模板。
FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变量)的格式存储,在转义成字符串时候会根据**引用模板**来进行格式化。知识库包含 3 个变量: q, a, file_id, index, source可以通过 {{q}} {{a}} {{file_id}} {{index}} {{source}} 按需引入。下面一个模板例子:
FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变量)的格式存储,在转义成字符串时候会根据**引用模板**来进行格式化。知识库包含多个可用变量: q, a, sourceId数据的ID, index(第n个数据), source(数据的集合名、文件名)score(距离得分0-1) 可以通过 {{q}} {{a}} {{sourceId}} {{index}} {{source}} {{score}} 按需引入。下面一个模板例子:
**引用模板**
可以通过 [知识库结构讲解](/docs/use-cases/datasetEngine/) 了解详细的知识库的结构。
### 引用模板
```
{instruction:"{{q}}",output:"{{a}}",source:"{{source}}"}
@@ -62,7 +64,7 @@ FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变
{instruction:"电影《铃芽之旅》的编剧是谁22",output:"新海诚是本片的编剧。",source:"手动输入"}
```
**引用提示词**
### 引用提示词
引用模板需要和引用提示词一起使用,提示词中可以写引用模板的格式说明以及对话的要求等。可以使用 {{quote}} 来使用 **引用模板**,使用 {{question}} 来引入问题。例如:
@@ -91,4 +93,42 @@ FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变
2. 使用背景知识回答问题。
3. 背景知识无法回答问题时,你可以礼貌的的回答用户问题。
我的问题是:"{{question}}"
```
```
### 总结
引用模板规定了搜索出来的内容如何组成一句话,其由 q,a,index,source 多个变量组成。
引用提示词由`引用模板``提示词`组成,提示词通常是对引用模板的一个描述,加上对模型的要求。
## 引用模板和提示词设计 示例
### 通用模板与问答模板对比
我们通过一组`你是谁`的手动数据,对通用模板与问答模板的效果进行对比。此处特意打了个搞笑的答案,通用模板下 GPT35 就变得不那么听话了,而问答模板下 GPT35 依然能够回答正确。这是由于结构化的提示词,在大语言模型中具有更强的引导作用。
{{% alert icon="🍅" context="success" %}}
Tips: 建议根据不同的场景每种知识库仅选择1类数据类型这样有利于充分发挥提示词的作用。
{{% /alert %}}
| 通用模板配置及效果 | 问答模板配置及效果 |
| --- | --- |
| ![](/imgs/datasetprompt1.png) | ![](/imgs/datasetprompt2.png) |
| ![](/imgs/datasetprompt3.png) | ![](/imgs/datasetprompt5.png) |
| ![](/imgs/datasetprompt4.png) | ![](/imgs/datasetprompt6.png) |
### 严格模板
使用非严格模板,我们随便询问一个不在知识库中的内容,模型通常会根据其自身知识进行回答。
| 非严格模板效果 | 选择严格模板 | 严格模板效果 |
| --- | --- | --- |
| ![](/imgs/datasetprompt7.png) | ![](/imgs/datasetprompt8.png) |![](/imgs/datasetprompt9.png) |
### 提示词设计思路
1. 使用序号进行不同要求描述。
2. 使用首先、然后、最后等词语进行描述。
3. 列举不同场景的要求时尽量完整不要遗漏。例如背景知识完全可以回答、背景知识可以回答一部分、背景知识与问题无关3种场景都说明清楚。
4. 巧用结构化提示,例如在问答模板中,利用了`instruction``output`,清楚的告诉模型,`output`是一个预期的答案。
5. 标点符号正确且完整。

View File

@@ -0,0 +1,83 @@
---
title: "知识库结构讲解"
description: "本节会介绍 FastGPT 知识库结构设计,理解其 QA 的存储格式和检索格式,以便更好的构建知识库。这篇介绍主要以使用为主,详细原理不多介绍。"
icon: "dataset"
draft: false
toc: true
weight: 311
---
# 理解向量
FastGPT 采用了 RAG 中的 Embedding 方案构建知识库,要使用好 FastGPT 需要简单的理解`Embedding`向量是如何工作的及其特点。
人类的文字、图片、视频等媒介是无法直接被计算机理解的,要想让计算机理解两段文字是否有相似性、相关性,通常需要将它们转成计算机可以理解的语言,向量是其中的一种方式。
向量可以简单理解为一个数字数组,两个向量之间可以通过数学公式得出一个`距离`,距离越小代表两个向量的相似度越大。从而映射到文字、图片、视频等媒介上,可以用来判断两个媒介之间的相似度。向量搜索便是利用了这个原理。
而由于文字是有多种类型,并且拥有成千上万种组合方式,因此在转成向量进行相似度匹配时,很难保障其精确性。在向量方案构建的知识库中,通常使用`topk`召回的方式,也就是查找前`k`个最相似的内容,丢给大模型去做更进一步的`语义判断``逻辑推理``归纳总结`,从而实现知识库问答。因此,在知识库问答中,向量搜索的环节是最为重要的。
影响向量搜索精度的因素非常多,主要包括:向量模型的质量、数据的质量(长度,完整性,多样性)、检索器的精度(速度与精度之间的取舍)。与数据质量对应的就是检索词的质量。
检索器的精度比较容易解决,向量模型的训练略复杂,因此数据和检索词质量优化成了一个重要的环节。
# FastGPT 中向量的结构设计
FastGPT 采用了 `PostgresSQL``PG Vector` 插件作为向量检索器,索引为`HNSW`。且`PostgresSQL`仅用于向量检索,`MongoDB`用于其他数据的存取。
`PostgresSQL`的表中,设置一个 `index` 字段用于存储向量、一个 `q` 字段用于存储向量对应的内容,以及一个 `a` 字段用于检索映射。之所以取字段为 `qa` 是由于一些历史缘故,无需完全解为 “问答对” 的格式。在实际使用过程中,可以利用`q``a`的组合,对检索后的内容做进一步的声明,提高大模型的理解力(注意,这里不直接提高搜索精度)。
目前,提高向量搜索的精度,主要可以通过几种途径:
1. 精简`q`的内容,减少向量内容的长度:当`q`的内容更少,更准确时,检索精度自然会提高。但与此同时,会牺牲一定的检索范围,适合答案较为严格的场景。
2. 更好分词分段:当一段话的结构和语义是完整的,并且是单一的,精度也会提高。因此,许多系统都会优化分词器,尽可能的保障每组数据的完整性。
3. 多样性文本:为一段内容增加关键词、摘要、相似问题等描述性信息,可以使得该内容的向量具有更大的检索覆盖范围。
4. 优化检索词:在实际使用过程中,用户的问题通常是模糊的或是缺失的,并不一定是完整清晰的问题。因此优化用户的问题(检索词)很大程度上也可以提高精度。
5. 微调向量模型:由于市面上直接使用的向量模型都是通用型模型,在特定领域的检索精度并不高,因此微调向量模型可以很大程度上提高专业领域的检索效果。
# FastGPT 构建知识库方案
在 FastGPT 中,整个知识库由库、集合和数据 3 部分组成。集合可以简单理解为一个`文件`。一个`库`中可以包含多个`集合`,一个`集合`中可以包含多组`数据`。最小的搜索单位是`库`,也就是说,知识库搜索时,是对整个`库`进行搜索,而集合仅是为了对数据进行分类管理,与搜索效果无关。(起码目前还是)
| 库 | 集合 | 数据 |
| --- | --- | --- |
| ![](/imgs/datasetEngine1.png) | ![](/imgs/datasetEngine2.png) | ![](/imgs/datasetEngine3.png) |
## 导入数据方案1 - 直接分段导入
选择文件导入时,可以选择直接分段方案。直接分段会利用`句子分词器`对文本进行一定长度拆分,最终分割中多组的`q`。如果使用了直接分段方案,我们建议在`应用`设置`引用提示词`时,使用`通用模板`即可,无需选择`问答模板`
| 交互 | 结果 |
| --- | --- |
| ![](/imgs/datasetEngine4.png) | ![](/imgs/datasetEngine5.png) |
## 导入数据方案2 - QA导入
选择文件导入时可以选择QA拆分方案。仍然需要使用到`句子分词器`对文本进行拆分,但长度比直接分段大很多。在导入后,会先调用`大模型`对分段进行学习,并给出一些`问题``答案`,最终问题和答案会一起被存储到`q`中。注意,新版的 FastGPT 为了提高搜索的范围,不再将问题和答案分别存储到 qa 中。
| 交互 | 结果 |
| --- | --- |
| ![](/imgs/datasetEngine6.png) | ![](/imgs/datasetEngine7.png) |
## 导入数据方案3 - 手动录入
在 FastGPT 中,你可以在任何一个`集合`中点击右上角的`插入`手动录入知识点,或者使用`标注`功能手动录入。被搜索的内容为`q`,补充内容(可选)为`a`
| | | |
| --- | --- | --- |
| ![](/imgs/datasetEngine8.png) | ![](/imgs/datasetEngine9.png) | ![](/imgs/datasetEngine10.png) |
## 导入数据方案4 - CSV录入
有些数据较为独特,可能需要单独的进行预处理分割后再导入 FastGPT此时可以选择 csv 导入,可批量的将处理好的数据导入。
![](/imgs/datasetEngine11.png)
## 导入数据方案5 - API导入
参考[FastGPT OpenAPI使用](/docs/development/openapi/#知识库添加数据)。
# QA的组合与引用提示词构建
参考[引用模板与引用提示词示例](/docs/use-cases/ai_settings/#示例)

View File

@@ -4,7 +4,7 @@ description: "通过与 OpenAI 兼容的 API 对接第三方应用"
icon: "model_training"
draft: false
toc: true
weight: 311
weight: 312
---
## 获取 API 秘钥

View File

@@ -86,7 +86,7 @@ weight: 142
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -210,14 +210,14 @@ weight: 142
"type": "target",
"label": "引用内容",
"description": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": false
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{

View File

@@ -132,7 +132,7 @@ export default async function (ctx: FunctionContext) {
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -179,7 +179,7 @@ export default async function (ctx: FunctionContext) {
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -410,14 +410,14 @@ export default async function (ctx: FunctionContext) {
"key": "quoteQA",
"type": "target",
"label": "引用内容",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": false
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{

View File

@@ -131,7 +131,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -174,7 +174,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -413,7 +413,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -630,7 +630,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
"label": "引用内容",
"description": "始终返回数组,如果希望搜索结果为空时执行额外操作,需要用到上面的两个输入以及目标模块的触发器",
"type": "source",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"targets": [
{
"moduleId": "bjfklc",
@@ -729,14 +729,14 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
"key": "quoteQA",
"type": "target",
"label": "引用内容",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": true
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -831,7 +831,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -874,7 +874,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -1006,7 +1006,7 @@ HTTP 模块允许你调用任意 POST 类型的 HTTP 接口,从而实验一些
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{

View File

@@ -83,7 +83,7 @@ weight: 144
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -189,7 +189,7 @@ weight: 144
"label": "引用内容",
"description": "始终返回数组,如果希望搜索结果为空时执行额外操作,需要用到上面的两个输入以及目标模块的触发器",
"type": "source",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"targets": [
{
"moduleId": "ol82hp",
@@ -291,14 +291,14 @@ weight: 144
"type": "target",
"label": "引用内容",
"description": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": true
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -389,7 +389,7 @@ weight: 144
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -432,7 +432,7 @@ weight: 144
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{

View File

@@ -245,7 +245,7 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -300,7 +300,7 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -427,7 +427,7 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -713,14 +713,14 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"type": "custom",
"label": "引用内容",
"description": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": false
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -871,14 +871,14 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"type": "custom",
"label": "引用内容",
"description": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": false
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -1085,14 +1085,14 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"type": "custom",
"label": "引用内容",
"description": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": false
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -1162,7 +1162,7 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
{
"key": "history",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"type": "source",
"targets": [
{
@@ -1205,7 +1205,7 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{
@@ -1452,14 +1452,14 @@ PS2配置中的问题分类还包含着“联网搜索”这个是另一
"type": "custom",
"label": "引用内容",
"description": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
"valueType": "kb_quote",
"valueType": "datasetQuote",
"connected": false
},
{
"key": "history",
"type": "target",
"label": "聊天记录",
"valueType": "chat_history",
"valueType": "chatHistory",
"connected": true
},
{

View File

@@ -4,18 +4,20 @@
"private": true,
"scripts": {
"prepare": "husky install",
"format": "prettier --config \"./.prettierrc.js\" --write \"./**/src/**/*.{ts,tsx,scss}\""
"format-code": "prettier --config \"./.prettierrc.js\" --write \"./**/src/**/*.{ts,tsx,scss}\"",
"format-doc": "zhlint --dir ./docSite *.md --fix"
},
"devDependencies": {
"husky": "^8.0.3",
"lint-staged": "^13.2.1",
"prettier": "^3.0.3",
"i18next": "^23.2.11",
"react-i18next": "^13.0.2",
"next-i18next": "^14.0.0"
"i18next": "^22.5.1",
"react-i18next": "^12.3.1",
"next-i18next": "^13.3.0"
},
"lint-staged": {
"./**/**/*.{ts,tsx,scss}": "npm run format"
"./**/**/*.{ts,tsx,scss}": "npm run format-code",
"./**/**/*.md": "npm run format-doc"
},
"engines": {
"node": ">=18.0.0"

View File

@@ -1,16 +1,12 @@
import { strIsLink } from '../string/tools';
export const fileImgs = [
{ suffix: 'pdf', src: '/imgs/files/pdf.svg' },
{ suffix: 'csv', src: '/imgs/files/csv.svg' },
{ suffix: '(doc|docs)', src: '/imgs/files/doc.svg' },
{ suffix: 'txt', src: '/imgs/files/txt.svg' },
{ suffix: 'md', src: '/imgs/files/markdown.svg' },
{ suffix: '.', src: '/imgs/files/file.svg' }
{ suffix: 'md', src: '/imgs/files/markdown.svg' }
// { suffix: '.', src: '/imgs/files/file.svg' }
];
export function getFileIcon(name = '') {
return (
fileImgs.find((item) => new RegExp(item.suffix, 'gi').test(name))?.src || '/imgs/files/file.svg'
);
export function getFileIcon(name = '', defaultImg = '/imgs/files/file.svg') {
return fileImgs.find((item) => new RegExp(item.suffix, 'gi').test(name))?.src || defaultImg;
}

View File

@@ -1,5 +1,6 @@
import { DatasetCollectionTypeEnum } from './constant';
import { getFileIcon } from '../../common/file/icon';
import { strIsLink } from '../../common/string/tools';
export function getCollectionIcon(
type: `${DatasetCollectionTypeEnum}` = DatasetCollectionTypeEnum.file,
@@ -7,9 +8,11 @@ export function getCollectionIcon(
) {
if (type === DatasetCollectionTypeEnum.folder) {
return '/imgs/files/folder.svg';
} else if (type === DatasetCollectionTypeEnum.link) {
}
if (type === DatasetCollectionTypeEnum.link) {
return '/imgs/files/link.svg';
} else if (type === DatasetCollectionTypeEnum.virtual) {
}
if (type === DatasetCollectionTypeEnum.virtual) {
if (name === '手动录入') {
return '/imgs/files/manual.svg';
} else if (name === '手动标注') {
@@ -19,3 +22,25 @@ export function getCollectionIcon(
}
return getFileIcon(name);
}
export function getSourceNameIcon({
sourceName,
sourceId
}: {
sourceName: string;
sourceId?: string;
}) {
if (strIsLink(sourceId)) {
return '/imgs/files/link.svg';
}
const fileIcon = getFileIcon(sourceName, '');
if (fileIcon) {
return fileIcon;
}
if (sourceName === '手动录入') {
return '/imgs/files/manual.svg';
} else if (sourceName === '手动标注') {
return '/imgs/files/mark.svg';
}
return '/imgs/files/collection.svg';
}

View File

@@ -0,0 +1,59 @@
export enum FlowNodeInputTypeEnum {
systemInput = 'systemInput', // history, userChatInput, variableInput
input = 'input', // one line input
textarea = 'textarea',
numberInput = 'numberInput',
select = 'select',
slider = 'slider',
custom = 'custom',
target = 'target', // data input
switch = 'switch',
chatInput = 'chatInput',
selectApp = 'selectApp',
// chat special input
aiSettings = 'aiSettings',
maxToken = 'maxToken',
selectChatModel = 'selectChatModel',
// dataset special input
selectDataset = 'selectDataset',
hidden = 'hidden'
}
export enum FlowNodeOutputTypeEnum {
answer = 'answer',
source = 'source',
hidden = 'hidden'
}
export enum FlowNodeTypeEnum {
empty = 'empty',
variable = 'variable',
userGuide = 'userGuide',
questionInput = 'questionInput',
historyNode = 'historyNode',
chatNode = 'chatNode',
datasetSearchNode = 'datasetSearchNode',
answerNode = 'answerNode',
classifyQuestion = 'classifyQuestion',
contentExtract = 'contentExtract',
httpRequest = 'httpRequest',
runApp = 'app',
pluginModule = 'pluginModule',
pluginInput = 'pluginInput',
pluginOutput = 'pluginOutput'
}
export enum FlowNodeSpecialInputKeyEnum {
'answerText' = 'text',
'agents' = 'agents', // cq agent key
'pluginId' = 'pluginId'
}
export enum FlowNodeValTypeEnum {
'string' = 'string',
'number' = 'number',
'boolean' = 'boolean',
'chatHistory' = 'chatHistory',
'datasetQuote' = 'datasetQuote',
'any' = 'any'
}

View File

@@ -0,0 +1,57 @@
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeValTypeEnum,
FlowNodeTypeEnum
} from './constant';
export type FlowNodeChangeProps = {
moduleId: string;
type:
| 'attr' // key: attr, value: new value
| 'updateInput' // key: update input key, value: new input value
| 'replaceInput' // key: old input key, value: new input value
| 'addInput' // key: null, value: new input value
| 'delInput' // key: delete input key, value: null
| 'updateOutput' // key: update output key, value: new output value
| 'replaceOutput' // key: old output key, value: new output value
| 'addOutput' // key: null, value: new output value
| 'delOutput'; // key: delete output key, value: null
key?: string;
value?: any;
index?: number;
};
export type FlowNodeInputItemType = {
key: string; // 字段名
value?: any;
valueType?: `${FlowNodeValTypeEnum}`;
type: `${FlowNodeInputTypeEnum}`;
label: string;
edit?: boolean;
connected?: boolean;
description?: string;
placeholder?: string;
max?: number;
min?: number;
step?: number;
required?: boolean;
list?: { label: string; value: any }[];
markList?: { label: string; value: any }[];
customData?: () => any;
valueCheck?: (value: any) => boolean;
};
export type FlowNodeOutputTargetItemType = {
moduleId: string;
key: string;
};
export type FlowNodeOutputItemType = {
key: string; // 字段名
label?: string;
edit?: boolean;
description?: string;
valueType?: `${FlowNodeValTypeEnum}`;
type?: `${FlowNodeOutputTypeEnum}`;
targets: FlowNodeOutputTargetItemType[];
};

44
packages/global/core/module/type.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
import { FlowNodeTypeEnum } from './node/constant';
import { FlowNodeInputItemType, FlowNodeOutputItemType } from './node/type';
export type FlowModuleTemplateType = {
id: string;
flowType: `${FlowNodeTypeEnum}`; // unique
logo?: string;
name: string;
description?: string;
intro?: string;
showStatus?: boolean; // chatting response step status
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
};
export type FlowModuleItemType = FlowModuleTemplateType & {
moduleId: string;
};
export type SystemModuleTemplateType = {
label: string;
list: FlowModuleTemplateType[];
}[];
export type ModuleItemType = {
name: string;
logo?: string;
intro?: string;
description?: string;
moduleId: string;
position?: {
x: number;
y: number;
};
flowType: `${FlowNodeTypeEnum}`;
showStatus?: boolean;
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
};
/* function type */
export type SelectAppItemType = {
id: string;
name: string;
logo: string;
};

View File

@@ -0,0 +1,47 @@
import {
FlowNodeInputTypeEnum,
FlowNodeSpecialInputKeyEnum,
FlowNodeTypeEnum
} from './node/constant';
import { FlowNodeInputItemType, FlowNodeOutputItemType } from './node/type';
import { ModuleItemType } from './type';
export function getPluginTemplatePluginIdInput(pluginId: string) {
return {
key: FlowNodeSpecialInputKeyEnum.pluginId,
type: FlowNodeInputTypeEnum.hidden,
label: 'pluginId',
value: pluginId,
connected: true
};
}
export function formatPluginIOModules(
pluginId: string,
modules: ModuleItemType[]
): {
inputs: FlowNodeInputItemType[];
outputs: FlowNodeOutputItemType[];
} {
const pluginInput = modules.find((module) => module.flowType === FlowNodeTypeEnum.pluginInput);
const customOutput = modules.find((module) => module.flowType === FlowNodeTypeEnum.pluginOutput);
return {
inputs: pluginInput
? [
getPluginTemplatePluginIdInput(pluginId),
...pluginInput.inputs.map((item) => ({
...item,
edit: false,
connected: false
}))
]
: [],
outputs: customOutput
? customOutput.outputs.map((item) => ({
...item,
edit: false
}))
: []
};
}

View File

@@ -0,0 +1,28 @@
import { ModuleItemType } from '../module/type';
export const defaultModules: ModuleItemType[] = [
{
moduleId: 'fph4s3',
name: '自定义输出',
flowType: 'pluginOutput',
showStatus: false,
position: {
x: 994.1266684738011,
y: -45.87689365278443
},
inputs: [],
outputs: []
},
{
moduleId: 'w09v30',
name: '自定义输入',
flowType: 'pluginInput',
showStatus: false,
position: {
x: 457.57860319995154,
y: -44.25099042468186
},
inputs: [],
outputs: []
}
];

View File

@@ -0,0 +1,21 @@
import type { ModuleItemType } from '../module/type.d';
export type CreateOnePluginParams = {
name: string;
avatar: string;
intro: string;
modules?: ModuleItemType[];
};
export type UpdatePluginParams = {
id: string;
name?: string;
avatar?: string;
intro?: string;
modules?: ModuleItemType[];
};
export type PluginListItemType = {
_id: string;
name: string;
avatar: string;
intro: string;
};

11
packages/global/core/plugin/type.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import type { ModuleItemType } from '../module/type.d';
export type PluginItemSchema = {
_id: string;
userId: string;
name: string;
avatar: string;
intro: string;
updateTime: Date;
modules: ModuleItemType[];
};

View File

@@ -0,0 +1,8 @@
export type PromotionRecordSchema = {
_id: string;
userId: string; // 收益人
objUId?: string; // 目标对象如果是withdraw则为空
type: 'register' | 'pay';
createTime: Date; // 记录时间
amount: number;
};

View File

@@ -0,0 +1,9 @@
export enum InformTypeEnum {
system = 'system'
}
export const InformTypeMap = {
[InformTypeEnum.system]: {
label: '系统通知'
}
};

View File

@@ -1,3 +1,5 @@
import { InformTypeEnum } from './constant';
export type UserModelSchema = {
_id: string;
username: string;
@@ -18,3 +20,13 @@ export type UserModelSchema = {
datasetMaxCount?: number;
};
};
export type UserInformSchema = {
_id: string;
userId: string;
time: Date;
type: `${InformTypeEnum}`;
title: string;
content: string;
read: boolean;
};

View File

@@ -0,0 +1,8 @@
export type PaySchema = {
_id: string;
userId: string;
createTime: Date;
price: number;
orderId: string;
status: 'SUCCESS' | 'REFUND' | 'NOTPAY' | 'CLOSED';
};

View File

@@ -0,0 +1 @@
export const imageBaseUrl = '/api/system/img/';

View File

@@ -0,0 +1,25 @@
import { imageBaseUrl } from './constant';
import { MongoImage } from './schema';
export function getMongoImgUrl(id: string) {
return `${imageBaseUrl}${id}`;
}
export async function uploadMongoImg({ base64Img, userId }: { base64Img: string; userId: string }) {
const base64Data = base64Img.split(',')[1];
const { _id } = await MongoImage.create({
userId,
binary: Buffer.from(base64Data, 'base64')
});
return getMongoImgUrl(String(_id));
}
export async function readMongoImg({ id }: { id: string }) {
const data = await MongoImage.findById(id);
if (!data) {
return Promise.reject('Image not found');
}
return data?.binary;
}

View File

@@ -1,4 +1,4 @@
import { connectionMongo, type Model } from '@fastgpt/service/common/mongo';
import { connectionMongo, type Model } from '../../mongo';
const { Schema, model, models } = connectionMongo;
const ImageSchema = new Schema({
@@ -12,5 +12,5 @@ const ImageSchema = new Schema({
}
});
export const Image: Model<{ userId: string; binary: Buffer }> =
export const MongoImage: Model<{ userId: string; binary: Buffer }> =
models['image'] || model('image', ImageSchema);

View File

@@ -63,6 +63,7 @@ const TrainingDataSchema = new Schema({
try {
TrainingDataSchema.index({ lockTime: 1 });
TrainingDataSchema.index({ userId: 1 });
TrainingDataSchema.index({ datasetCollectionId: 1 });
TrainingDataSchema.index({ expireAt: 1 }, { expireAfterSeconds: 7 * 24 * 60 });
} catch (error) {
console.log(error);

View File

@@ -0,0 +1,63 @@
import { CreateOnePluginParams, UpdatePluginParams } from '@fastgpt/global/core/plugin/controller';
import { MongoPlugin } from './schema';
import { FlowModuleTemplateType } from '@fastgpt/global/core/module/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { formatPluginIOModules } from '@fastgpt/global/core/module/utils';
export async function createOnePlugin(data: CreateOnePluginParams & { userId: string }) {
const { _id } = await MongoPlugin.create(data);
return _id;
}
export async function updateOnePlugin({
id,
userId,
...data
}: UpdatePluginParams & { userId: string }) {
await MongoPlugin.findOneAndUpdate({ _id: id, userId }, data);
}
export async function deleteOnePlugin({ id, userId }: { id: string; userId: string }) {
await MongoPlugin.findOneAndDelete({ _id: id, userId });
}
export async function getUserPlugins({ userId }: { userId: string }) {
return MongoPlugin.find({ userId }, 'name avatar intro');
}
export async function getOnePluginDetail({ id, userId }: { userId: string; id: string }) {
return MongoPlugin.findOne({ _id: id, userId });
}
/* plugin templates */
export async function getUserPlugins2Templates({
userId
}: {
userId: string;
}): Promise<FlowModuleTemplateType[]> {
const plugins = await MongoPlugin.find({ userId }).lean();
return plugins.map((plugin) => ({
id: String(plugin._id),
flowType: FlowNodeTypeEnum.pluginModule,
logo: plugin.avatar,
name: plugin.name,
description: plugin.intro,
intro: plugin.intro,
showStatus: false,
inputs: [],
outputs: []
}));
}
/* one plugin 2 module detail */
export async function getPluginModuleDetail({ id, userId }: { userId: string; id: string }) {
const plugin = await getOnePluginDetail({ id, userId });
if (!plugin) return Promise.reject('plugin not found');
return {
id: String(plugin._id),
flowType: FlowNodeTypeEnum.pluginModule,
logo: plugin.avatar,
name: plugin.name,
description: plugin.intro,
intro: plugin.intro,
showStatus: false,
...formatPluginIOModules(String(plugin._id), plugin.modules)
};
}

View File

@@ -0,0 +1,42 @@
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
import type { PluginItemSchema } from '@fastgpt/global/core/plugin/type.d';
export const ModuleCollectionName = 'plugins';
const PluginSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
name: {
type: String,
required: true
},
avatar: {
type: String,
default: '/icon/logo.svg'
},
intro: {
type: String,
default: ''
},
updateTime: {
type: Date,
default: () => new Date()
},
modules: {
type: Array,
default: []
}
});
try {
PluginSchema.index({ userId: 1 });
} catch (error) {
console.log(error);
}
export const MongoPlugin: Model<PluginItemSchema> =
models[ModuleCollectionName] || model(ModuleCollectionName, PluginSchema);

View File

@@ -1,6 +1,6 @@
import { connectionMongo, type Model } from '@fastgpt/service/common/mongo';
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { PromotionRecordSchema as PromotionRecordType } from '@/types/mongoSchema';
import { PromotionRecordSchema as PromotionRecordType } from '@fastgpt/global/support/activity/type.d';
const PromotionRecordSchema = new Schema({
userId: {
@@ -28,5 +28,5 @@ const PromotionRecordSchema = new Schema({
}
});
export const promotionRecord: Model<PromotionRecordType> =
export const MongoPromotionRecord: Model<PromotionRecordType> =
models['promotionRecord'] || model('promotionRecord', PromotionRecordSchema);

View File

@@ -0,0 +1,45 @@
import { MongoUserInform } from './schema';
import { MongoUser } from '../schema';
import { InformTypeEnum } from '@fastgpt/global/support/user/constant';
export type SendInformProps = {
type: `${InformTypeEnum}`;
title: string;
content: string;
};
export async function sendInform2AllUser({ type, title, content }: SendInformProps) {
const users = await MongoUser.find({}, '_id');
await MongoUserInform.insertMany(
users.map(({ _id }) => ({
type,
title,
content,
userId: _id
}))
);
}
export async function sendInform2OneUser({
type,
title,
content,
userId
}: SendInformProps & { userId: string }) {
const inform = await MongoUserInform.findOne({
type,
title,
content,
userId,
time: { $gte: new Date(Date.now() - 5 * 60 * 1000) }
});
if (inform) return;
await MongoUserInform.create({
type,
title,
content,
userId
});
}

View File

@@ -1,7 +1,7 @@
import { connectionMongo, type Model } from '@fastgpt/service/common/mongo';
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { informSchema } from '@/types/mongoSchema';
import { InformTypeMap } from '@/constants/user';
import type { UserInformSchema } from '@fastgpt/global/support/user/type.d';
import { InformTypeMap } from '@fastgpt/global/support/user/constant';
const InformSchema = new Schema({
userId: {
@@ -38,4 +38,5 @@ try {
console.log(error);
}
export const Inform: Model<informSchema> = models['inform'] || model('inform', InformSchema);
export const MongoUserInform: Model<UserInformSchema> =
models['inform'] || model('inform', InformSchema);

View File

@@ -1,6 +1,7 @@
import { connectionMongo, type Model } from '@fastgpt/service/common/mongo';
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { PaySchema as PayType } from '@/types/mongoSchema';
import { PaySchema as PayType } from '@fastgpt/global/support/wallet/type.d';
const PaySchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
@@ -27,4 +28,4 @@ const PaySchema = new Schema({
}
});
export const Pay: Model<PayType> = models['pay'] || model('pay', PaySchema);
export const MongoPay: Model<PayType> = models['pay'] || model('pay', PaySchema);

58
pnpm-lock.yaml generated
View File

@@ -12,20 +12,20 @@ importers:
specifier: ^8.0.3
version: registry.npmmirror.com/husky@8.0.3
i18next:
specifier: ^23.2.11
version: registry.npmmirror.com/i18next@23.6.0
specifier: ^22.5.1
version: registry.npmmirror.com/i18next@22.5.1
lint-staged:
specifier: ^13.2.1
version: registry.npmmirror.com/lint-staged@13.3.0
next-i18next:
specifier: ^14.0.0
version: registry.npmmirror.com/next-i18next@14.0.3(i18next@23.6.0)(next@13.5.2)(react-i18next@13.3.1)(react@18.2.0)
specifier: ^13.3.0
version: registry.npmmirror.com/next-i18next@13.3.0(i18next@22.5.1)(next@13.5.2)(react-i18next@12.3.1)(react@18.2.0)
prettier:
specifier: ^3.0.3
version: registry.npmmirror.com/prettier@3.0.3
react-i18next:
specifier: ^13.0.2
version: registry.npmmirror.com/react-i18next@13.3.1(i18next@23.6.0)(react-dom@18.2.0)(react@18.2.0)
specifier: ^12.3.1
version: registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
packages/global:
dependencies:
@@ -176,8 +176,8 @@ importers:
specifier: ^2.4.29
version: registry.npmmirror.com/hyperdown@2.4.29
i18next:
specifier: ^23.2.11
version: registry.npmmirror.com/i18next@23.6.0
specifier: ^22.5.1
version: registry.npmmirror.com/i18next@22.5.1
immer:
specifier: ^9.0.19
version: registry.npmmirror.com/immer@9.0.21
@@ -215,8 +215,8 @@ importers:
specifier: 13.5.2
version: registry.npmmirror.com/next@13.5.2(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.4)
next-i18next:
specifier: ^14.0.0
version: registry.npmmirror.com/next-i18next@14.0.3(i18next@23.6.0)(next@13.5.2)(react-i18next@13.3.1)(react@18.2.0)
specifier: ^13.3.0
version: registry.npmmirror.com/next-i18next@13.3.0(i18next@22.5.1)(next@13.5.2)(react-i18next@12.3.1)(react@18.2.0)
nprogress:
specifier: ^0.2.0
version: registry.npmmirror.com/nprogress@0.2.0
@@ -242,8 +242,8 @@ importers:
specifier: ^7.43.1
version: registry.npmmirror.com/react-hook-form@7.47.0(react@18.2.0)
react-i18next:
specifier: ^13.0.2
version: registry.npmmirror.com/react-i18next@13.3.1(i18next@23.6.0)(react-dom@18.2.0)(react@18.2.0)
specifier: ^12.3.1
version: registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
react-markdown:
specifier: ^8.0.7
version: registry.npmmirror.com/react-markdown@8.0.7(@types/react@18.0.28)(react@18.2.0)
@@ -7579,10 +7579,10 @@ packages:
name: i18next-fs-backend
version: 2.2.0
registry.npmmirror.com/i18next@23.6.0:
resolution: {integrity: sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/i18next/-/i18next-23.6.0.tgz}
registry.npmmirror.com/i18next@22.5.1:
resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/i18next/-/i18next-22.5.1.tgz}
name: i18next
version: 23.6.0
version: 22.5.1
dependencies:
'@babel/runtime': registry.npmmirror.com/@babel/runtime@7.23.2
@@ -9327,27 +9327,27 @@ packages:
version: 1.4.0
dev: true
registry.npmmirror.com/next-i18next@14.0.3(i18next@23.6.0)(next@13.5.2)(react-i18next@13.3.1)(react@18.2.0):
resolution: {integrity: sha512-FtnjRMfhlamk8YyeyWqd+pndNL+3er83iMZnH4M4mhiGA93l0+vtBUvuObgOAMHDJGLLB2SS2xOOZq69oiJh7A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/next-i18next/-/next-i18next-14.0.3.tgz}
id: registry.npmmirror.com/next-i18next/14.0.3
registry.npmmirror.com/next-i18next@13.3.0(i18next@22.5.1)(next@13.5.2)(react-i18next@12.3.1)(react@18.2.0):
resolution: {integrity: sha512-X4kgi51BCOoGdKbv87eZ8OU7ICQDg5IP+T5fNjqDY3os9ea0OKTY4YpAiVFiwcI9XimcUmSPbKO4a9jFUyYSgg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/next-i18next/-/next-i18next-13.3.0.tgz}
id: registry.npmmirror.com/next-i18next/13.3.0
name: next-i18next
version: 14.0.3
version: 13.3.0
engines: {node: '>=14'}
peerDependencies:
i18next: ^23.4.6
i18next: ^22.0.6
next: '>= 12.0.0'
react: '>= 17.0.2'
react-i18next: ^13.2.1
react-i18next: ^12.2.0
dependencies:
'@babel/runtime': registry.npmmirror.com/@babel/runtime@7.23.2
'@types/hoist-non-react-statics': registry.npmmirror.com/@types/hoist-non-react-statics@3.3.4
core-js: registry.npmmirror.com/core-js@3.33.1
hoist-non-react-statics: registry.npmmirror.com/hoist-non-react-statics@3.3.2
i18next: registry.npmmirror.com/i18next@23.6.0
i18next: registry.npmmirror.com/i18next@22.5.1
i18next-fs-backend: registry.npmmirror.com/i18next-fs-backend@2.2.0
next: registry.npmmirror.com/next@13.5.2(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.4)
react: registry.npmmirror.com/react@18.2.0
react-i18next: registry.npmmirror.com/react-i18next@13.3.1(i18next@23.6.0)(react-dom@18.2.0)(react@18.2.0)
react-i18next: registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
registry.npmmirror.com/next@13.5.2(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.4):
resolution: {integrity: sha512-vog4UhUaMYAzeqfiAAmgB/QWLW7p01/sg+2vn6bqc/CxHFYizMzLv6gjxKzl31EVFkfl/F+GbxlKizlkTE9RdA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/next/-/next-13.5.2.tgz}
@@ -10150,13 +10150,13 @@ packages:
react: registry.npmmirror.com/react@18.2.0
dev: false
registry.npmmirror.com/react-i18next@13.3.1(i18next@23.6.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-i18next/-/react-i18next-13.3.1.tgz}
id: registry.npmmirror.com/react-i18next/13.3.1
registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-i18next/-/react-i18next-12.3.1.tgz}
id: registry.npmmirror.com/react-i18next/12.3.1
name: react-i18next
version: 13.3.1
version: 12.3.1
peerDependencies:
i18next: '>= 23.2.3'
i18next: '>= 19.0.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
@@ -10168,7 +10168,7 @@ packages:
dependencies:
'@babel/runtime': registry.npmmirror.com/@babel/runtime@7.23.2
html-parse-stringify: registry.npmmirror.com/html-parse-stringify@3.0.1
i18next: registry.npmmirror.com/i18next@23.6.0
i18next: registry.npmmirror.com/i18next@22.5.1
react: registry.npmmirror.com/react@18.2.0
react-dom: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)

View File

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.5.1",
"version": "4.5.2",
"private": false,
"scripts": {
"dev": "next dev",
@@ -31,7 +31,7 @@
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"hyperdown": "^2.4.29",
"i18next": "^23.2.11",
"i18next": "^22.5.1",
"immer": "^9.0.19",
"js-cookie": "^3.0.5",
"js-tiktoken": "^1.0.7",
@@ -44,7 +44,7 @@
"multer": "1.4.5-lts.1",
"nanoid": "^4.0.1",
"next": "13.5.2",
"next-i18next": "^14.0.0",
"next-i18next": "^13.3.0",
"nprogress": "^0.2.0",
"papaparse": "^5.4.1",
"pg": "^8.10.0",
@@ -53,7 +53,7 @@
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-i18next": "^13.0.2",
"react-i18next": "^12.3.1",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.7.4",

View File

@@ -1,9 +1,9 @@
### Fast GPT V4.5.1
### Fast GPT V4.5.2
1. 新增 - 知识库目录结构,更方便进行分类
2. 新增 - 升级 PgVector 插件,引入 HNSW 索引,极大加快的知识库搜索速度
3. 新增 - AI对话模块增加【返回AI内容】选项可控制 AI 的内容不直接返回浏览器。
4. 优化 - TextSplitter采用递归拆解法。
5. [使用文档](https://doc.fastgpt.run/docs/intro/)
6. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
7. [点击查看商业版](https://doc.fastgpt.run/docs/commercial/)
1. 新增 - 模块插件,允许自行组装插件进行模块复用。
2. 优化 - 知识库引用提示
3. [知识库结构详解](https://doc.fastgpt.in/docs/use-cases/datasetengine/)
4. [知识库提示词详解](https://doc.fastgpt.in/docs/use-cases/ai_settings/#引用模板--引用提示词)
5. [使用文档](https://doc.fastgpt.in/docs/intro/)
6. [点击查看高级编排介绍文档](https://doc.fastgpt.in/docs/workflow)
7. [点击查看商业版](https://doc.fastgpt.in/docs/commercial/)

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698306337334"
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2550"
xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
<path
d="M362.496 661.504c0 23.552 18.944 42.496 42.496 42.496s42.496-18.944 42.496-42.496c0-23.552-18.944-42.496-42.496-42.496s-42.496 18.944-42.496 42.496zM576 661.504c0 23.552 18.944 42.496 42.496 42.496 23.552 0 42.496-18.944 42.496-42.496 0-23.552-18.944-42.496-42.496-42.496-23.552-0.512-42.496 18.944-42.496 42.496z"
p-id="2551" fill="#1296db"></path>
<path
d="M874.496 618.496v-21.504c2.048-95.744-41.984-186.368-118.272-244.224L860.16 190.464c34.304 8.192 68.608-13.312 76.8-48.128 8.192-34.304-13.312-68.608-48.128-76.8s-68.608 13.312-76.8 48.128c-4.608 18.944 0 38.4 11.776 53.76l-102.912 162.304c-64-35.328-136.192-53.248-209.408-52.224-73.216-1.536-145.408 16.384-209.408 52.224L199.68 167.424c21.504-27.648 16.896-68.096-11.264-89.6-27.648-21.504-68.096-16.896-89.6 11.264-22.016 27.648-16.896 67.584 11.264 89.6 15.36 11.776 34.816 16.384 53.76 11.776L267.776 353.28c-76.288 57.856-120.32 148.48-118.272 244.224v21.504H42.496v256h106.496v85.504h725.504v-85.504h106.496v-256h-106.496zM149.504 832H85.504v-170.496h64v170.496z m512-42.496H362.496c-70.656 0-128-57.344-128-128s57.344-128 128-128h298.496c70.656 0 128 57.344 128 128 0.512 70.656-56.832 128-127.488 128z m276.992 42.496h-64v-170.496h64v170.496z"
p-id="2552" fill="#3370ff"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -27,6 +27,7 @@
"Connection is invalid": "Connecting is invalid",
"Connection type is different": "Connection type is different",
"Copy Module Config": "Copy config",
"Dataset Quote Template": "Dataset Mode",
"Export Config Successful": "The configuration has been copied. Please check for important data",
"Export Configs": "Export Configs",
"Feedback Count": "User Feedback",
@@ -46,7 +47,20 @@
"Paste Config": "Paste Config",
"Variable Key Repeat Tip": "Variable Key Repeat",
"module": {
"Custom Title Tip": "The title name is displayed during the conversation"
"Combine Modules": "Combine Modules",
"Custom Title Tip": "The title name is displayed during the conversation",
"My Modules": "My Custom Modules",
"No Modules": "No module",
"System Module": "System Module",
"type": "{{type}}\n{{example}}",
"valueType": {
"any": "any",
"boolean": "boolean",
"chatHistory": "Chat History",
"datasetQuote": "Dataset Quote",
"number": "number",
"string": "string"
}
},
"modules": {
"Title is required": "Title is required"
@@ -72,6 +86,7 @@
"Mark Description Title": "Mark Description",
"New Chat": "New Chat",
"Question Guide Tips": "I guess what you're asking is",
"Quote": "Quote",
"Read Mark Description": "Read mark description",
"Read User Feedback": "Read user feedback",
"Select Mark Kb": "Select Dataset",
@@ -104,24 +119,32 @@
"module similarity": "Similarity",
"module temperature": "Temperature",
"module time": "Running Time",
"module tokens": "Tokens"
"module tokens": "Tokens",
"plugin output": "Plugin Output"
},
"retry": "Retry"
},
"common": {
"Add": "Add",
"Back": "Back",
"Beta": "Beta",
"Choose": "Choose",
"Close": "Clow",
"Collect": "Collect",
"Confirm Create": "Create",
"Confirm Move": "Move here",
"Confirm Update": "Update",
"Copy": "Copy",
"Copy Successful": "Copy Successful",
"Course": "",
"Create Failed": "Create Failed",
"Create Success": "Create Success",
"Create Virtual File Failed": "Create Virtual File Failed",
"Custom Title": "Custom Title",
"Delete": "Delete",
"Delete Failed": "Delete Failed",
"Delete Success": "Delete Successful",
"Delete Tip": "Delete Confirm",
"Delete Warning": "Warning",
"Edit": "Edit",
"Expired Time": "Expired",
@@ -130,10 +153,13 @@
"Filed is repeated": "",
"Input": "Input",
"Last Step": "Last",
"Loading": "Loading",
"Max credit": "Credit",
"Max credit tips": "What is the maximum amount of money that can be consumed by the link? If the link is exceeded, it will be banned. -1 indicates no limit.",
"Name": "Name",
"Name Can": "Name Can't Be Empty",
"Name is empty": "Name is empty",
"New Create": "Create",
"Next Step": "Next",
"Output": "Output",
"Params": "Params",
@@ -143,11 +169,17 @@
"Rename Success": "Rename Success",
"Request Error": "Request Error",
"Search": "Search",
"Select File Failed": "Select File Failed",
"Select One Folder": "Select a folder",
"Set Avatar": "Set Avatar",
"Set Name": "Make a nice name",
"Status": "Status",
"Test": "Test",
"Time": "Time",
"Unknow": "Unknow",
"Unknow Source": "UnKnow Source",
"Update Failed": "Update Failed",
"Update Success": "Update Success",
"Update Successful": "Update Successful",
"Update Time": "Update Time",
"error": {
@@ -304,11 +336,26 @@
"desc": "AI knowledge base question and answer platform based on LLM large model",
"slogan": "Let the AI know more about you"
},
"module": {
"Confirm Delete Module": "Confirm to delete the custom module?",
"Confirm Sync Plugin": "Confirm the latest sync plugin information? The plug-in connection and input content will be cleared, please confirm!",
"Create Your Module": "Create You Module",
"Intro": "Module Intro",
"Load Module Failed": "Load Module Failed",
"Plugin input is not value": "User-defined input parameters cannot be null",
"Plugin input is required": "The plug setting must contain an input module",
"Plugin input must connect": "Custom input modules must all be connected",
"Preview Plugin": "Preview Plugin",
"Save Config": "Save",
"Update Your Module": "Update Module"
},
"navbar": {
"Account": "Account",
"Apps": "Apps",
"Chat": "Chat",
"Datasets": "DataSets",
"Module": "Module",
"Plugin": "Plugin",
"Store": "Store",
"Tools": "Tools"
},
@@ -338,12 +385,26 @@
"token auth Tips": "Identity verification server address. If this value is set, the server will be specified to send a request for identity verification before each session",
"token auth use cases": "Review the authentication instructions"
},
"plugin": {
"Confirm Delete": "Confirm to delete the plugin?",
"Create Your Plugin": "Create Plugin",
"Get Plugin Module Detail Failed": "Get plugin detail failed",
"Intro": "Plugin Intro",
"Load Plugin Failed": "Load Plugin Failed",
"My Plugins": "My Plugins",
"No Intro": "This plugin is not introduced",
"Plugin Module": "Plugin",
"Set Name": "Plugin Name",
"Synchronous version": "Sync Version",
"To Edit Plugin": "To Edit",
"Update Your Plugin": "Update Plugin"
},
"system": {
"Help Document": "Document"
},
"template": {
"Quote Content Tip": "This configuration takes effect only when reference content is passed in (knowledge base search). You can customize the structure of the reference content to better fit different scenarios. You can use {{q}}, {{a}}, {{source}} as \"search content\", \"expected content\", and \"source\", they are all optional, and here are the default values: \n{instruction:\"{{q}}\",output:\"{{a}}\"}",
"Quote Prompt Tip": "This configuration takes effect only when reference content is passed in (knowledge base search). \n You can insert references with {{quote}}, here are the default values: \n\"\"\"{{quote}}\"\"\" The three quotes are the knowledge base I gave you, they have the highest priority. instruction is a relevant introduction and output is an expected answer or supplement."
"Quote Content Tip": "This configuration takes effect only when reference content is passed in (knowledge base search).\nYou can customize the structure of the reference content to better suit different scenarios. Some variables can be used for template configuration:\n{{q}} - retrieve content, {{a}} - expected content, {{source}} - source, {{sourceId}} - source file name, {{index}} - the first n references, {{with}} - the reference points (0-1), they are optional, Here are the default values:\n{{default}}",
"Quote Prompt Tip": "This configuration takes effect only when the knowledge base is searched.\nYou can use {{quote}} to insert the reference content template and {{question}} to insert the question. Here are the default values:\n{{default}}"
},
"user": {
"Account": "Account",

View File

@@ -27,6 +27,7 @@
"Connection is invalid": "连接无效",
"Connection type is different": "连接的类型不一致",
"Copy Module Config": "复制配置",
"Dataset Quote Template": "知识库问答模式",
"Export Config Successful": "已复制配置,请注意检查是否有重要数据",
"Export Configs": "导出配置",
"Feedback Count": "用户反馈",
@@ -46,7 +47,20 @@
"Paste Config": "粘贴配置",
"Variable Key Repeat Tip": "变量 key 重复",
"module": {
"Custom Title Tip": "该标题名字会展示在对话过程中"
"Combine Modules": "组合模块",
"Custom Title Tip": "该标题名字会展示在对话过程中",
"My Modules": "",
"No Modules": "还没有模块~",
"System Module": "系统模块",
"type": "\"{{type}}\"类型\n{{example}}",
"valueType": {
"any": "任意",
"boolean": "布尔",
"chatHistory": "聊天记录",
"datasetQuote": "引用内容",
"number": "数字",
"string": "字符串"
}
},
"modules": {
"Title is required": "模块名不能为空"
@@ -72,6 +86,7 @@
"Mark Description Title": "标注功能介绍",
"New Chat": "新对话",
"Question Guide Tips": "猜你想问",
"Quote": "引用",
"Read Mark Description": "查看标注功能介绍",
"Read User Feedback": "查看用户反馈",
"Select Mark Kb": "选择知识库",
@@ -104,24 +119,32 @@
"module similarity": "相似度",
"module temperature": "温度",
"module time": "运行时长",
"module tokens": "Tokens"
"module tokens": "Tokens",
"plugin output": "插件输出值"
},
"retry": "重新生成"
},
"common": {
"Add": "添加",
"Back": "返回",
"Beta": "实验版",
"Choose": "选择",
"Close": "关闭",
"Collect": "收藏",
"Confirm Create": "确认创建",
"Confirm Move": "移动到这",
"Confirm Update": "确认更新",
"Copy": "复制",
"Copy Successful": "复制成功",
"Course": "",
"Create Failed": "创建异常",
"Create Success": "创建成功",
"Create Virtual File Failed": "创建虚拟文件失败",
"Custom Title": "自定义标题",
"Delete": "删除",
"Delete Failed": "删除失败",
"Delete Success": "删除成功",
"Delete Tip": "删除提示",
"Delete Warning": "删除警告",
"Edit": "编辑",
"Expired Time": "过期时间",
@@ -130,10 +153,13 @@
"Filed is repeated": "字段重复了",
"Input": "输入",
"Last Step": "上一步",
"Loading": "加载中",
"Max credit": "最大金额",
"Max credit tips": "该链接最大可消耗多少金额,超出后链接将被禁止使用。-1 代表无限制。",
"Name": "名称",
"Name Can": "名称不能为空",
"Name is empty": "名称不能为空",
"New Create": "新建",
"Next Step": "下一步",
"Output": "输出",
"Params": "参数",
@@ -143,11 +169,17 @@
"Rename Success": "重命名成功",
"Request Error": "请求异常",
"Search": "搜索",
"Select File Failed": "选择文件异常",
"Select One Folder": "选择一个目录",
"Set Avatar": "点击设置头像",
"Set Name": "取个响亮的名字",
"Status": "状态",
"Test": "测试",
"Time": "时间",
"Unknow": "未知",
"Unknow Source": "未知来源",
"Update Failed": "更新异常",
"Update Success": "更新成功",
"Update Successful": "更新成功",
"Update Time": "更新时间",
"error": {
@@ -232,7 +264,7 @@
"data": {
"Delete Tip": "确认删除该条数据?",
"File import": "文件导入",
"Input Data": "导入数据",
"Input Data": "导入数据",
"Input Success Tip": "导入数据成功",
"Update Data": "更新数据",
"Update Success Tip": "更新数据成功"
@@ -304,11 +336,26 @@
"desc": "基于 LLM 大模型的 AI 知识库问答平台",
"slogan": "让 AI 更懂你的知识"
},
"module": {
"Confirm Delete Module": "确认删除该自定义模块?",
"Confirm Sync Plugin": "确认同步插件最新信息?插件的连线和输入的内容将会被清空,请确认!",
"Create Your Module": "创建自定义模块",
"Intro": "模块介绍",
"Load Module Failed": "加载模块失败",
"Plugin input is not value": "自定义输入的参数不能为空",
"Plugin input is required": "插件编排必须包含一个输入模块",
"Plugin input must connect": "自定义输入模块必须全部连接",
"Preview Plugin": "预览插件",
"Save Config": "保存配置",
"Update Your Module": "更新模块信息"
},
"navbar": {
"Account": "账号",
"Apps": "应用",
"Chat": "聊天",
"Datasets": "知识库",
"Module": "模块",
"Plugin": "插件",
"Store": "应用市场",
"Tools": "工具"
},
@@ -338,12 +385,26 @@
"token auth Tips": "身份校验服务器地址,如填写该值,每次对话前都会想指定服务器发送一个请求,进行身份校验",
"token auth use cases": "查看身份验证使用说明"
},
"plugin": {
"Confirm Delete": "确认删除该插件?",
"Create Your Plugin": "创建你的插件",
"Get Plugin Module Detail Failed": "获取插件信息异常",
"Intro": "插件介绍",
"Load Plugin Failed": "加载插件异常",
"My Plugins": "我的插件",
"No Intro": "这个插件没有介绍~",
"Plugin Module": "插件模块",
"Set Name": "给插件取个名字",
"Synchronous version": "同步版本",
"To Edit Plugin": "去编辑",
"Update Your Plugin": "更新插件"
},
"system": {
"Help Document": "帮助文档"
},
"template": {
"Quote Content Tip": "该配置只有传入引用内容(知识库搜索)时生效。\n可以自定义引用内容的结构以更好的适配不同场景。可以使用一些变量来进行模板配置:\n{{q}} - 检索内容, {{a}} - 预期内容, {{source}} - 来源,{{file_id}} - 来源文件名,{{index}} - 第n个引用他们都是可选的下面是默认值\n{{default}}",
"Quote Prompt Tip": "该配置只有传入引用内容(知识库搜索时生效。\n可以用 {{quote}} 来插入引用内容,使用 {{question}} 来插入问题。下面是默认值:\n{{default}}"
"Quote Content Tip": "该配置只有传入引用内容(知识库搜索)时生效。\n可以自定义引用内容的结构以更好的适配不同场景。可以使用一些变量来进行模板配置:\n{{q}} - 检索内容, {{a}} - 预期内容, {{source}} - 来源,{{sourceId}} - 来源文件名,{{index}} - 第n个引用{{score}} - 该引用的得分(0-1)他们都是可选的,下面是默认值:\n{{default}}",
"Quote Prompt Tip": "该配置只知识库搜索时生效。\n可以用 {{quote}} 来插入引用内容模板,使用 {{question}} 来插入问题。下面是默认值:\n{{default}}"
},
"user": {
"Account": "账号",

View File

@@ -77,13 +77,7 @@ const QuoteModal = ({
</>
}
>
<ModalBody
pt={0}
whiteSpace={'pre-wrap'}
textAlign={'justify'}
wordBreak={'break-all'}
fontSize={'sm'}
>
<ModalBody pt={0} whiteSpace={'pre-wrap'} textAlign={'justify'} wordBreak={'break-all'}>
{rawSearch.map((item, i) => (
<Box
key={i}
@@ -95,11 +89,18 @@ const QuoteModal = ({
position={'relative'}
overflow={'hidden'}
_hover={{ '& .hover-data': { display: 'flex' } }}
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
>
{!isShare && (
<Flex alignItems={'flex-end'} mb={3} color={'myGray.500'}>
<RawSourceText sourceName={item.sourceName} sourceId={item.sourceId} />
<Box flex={1} />
<Flex alignItems={'flex-end'} mb={3} fontSize={'sm'}>
<RawSourceText
fontWeight={'bold'}
color={'black'}
sourceName={item.sourceName}
sourceId={item.sourceId}
addr={!isShare}
/>
<Box flex={1} />
{!isShare && (
<Link
as={NextLink}
className="hover-data"
@@ -111,13 +112,13 @@ const QuoteModal = ({
{t('core.dataset.Go Dataset')}
<MyIcon name={'rightArrowLight'} w={'10px'} />
</Link>
</Flex>
)}
)}
</Flex>
<Box color={'black'}>{item.q}</Box>
<Box color={'black'}>{item.a}</Box>
<Box color={'myGray.600'}>{item.a}</Box>
{!isShare && (
<Flex alignItems={'center'} mt={3} gap={4} color={'myGray.500'}>
<Flex alignItems={'center'} fontSize={'sm'} mt={3} gap={4} color={'myGray.500'}>
{isPc && (
<MyTooltip label={t('core.dataset.data.id')}>
<Flex border={theme.borders.base} px={3} borderRadius={'md'}>

View File

@@ -1,19 +1,22 @@
import React, { useMemo, useState } from 'react';
import { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
import { Flex, BoxProps, useDisclosure } from '@chakra-ui/react';
import { Flex, BoxProps, useDisclosure, Image, useTheme } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic';
import Tag from '../Tag';
import MyTooltip from '../MyTooltip';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider';
const QuoteModal = dynamic(() => import('./QuoteModal'), { ssr: false });
const ContextModal = dynamic(() => import('./ContextModal'), { ssr: false });
const WholeResponseModal = dynamic(() => import('./WholeResponseModal'), { ssr: false });
const ResponseTags = ({ responseData = [] }: { responseData?: ChatHistoryItemResType[] }) => {
const theme = useTheme();
const { isPc } = useSystemStore();
const { t } = useTranslation();
const [quoteModalData, setQuoteModalData] = useState<SearchDataResponseItemType[]>();
@@ -27,18 +30,36 @@ const ResponseTags = ({ responseData = [] }: { responseData?: ChatHistoryItemRes
const {
chatAccount,
quoteList = [],
sourceList = [],
historyPreview = [],
runningTime = 0
} = useMemo(() => {
const chatData = responseData.find((item) => item.moduleType === FlowModuleTypeEnum.chatNode);
const chatData = responseData.find((item) => item.moduleType === FlowNodeTypeEnum.chatNode);
const quoteList = responseData
.filter((item) => item.moduleType === FlowNodeTypeEnum.chatNode)
.map((item) => item.quoteList)
.flat()
.filter((item) => item) as SearchDataResponseItemType[];
const sourceList = quoteList.reduce(
(acc: Record<string, SearchDataResponseItemType[]>, cur) => {
if (!acc[cur.sourceName]) {
acc[cur.sourceName] = [cur];
}
return acc;
},
{}
);
return {
chatAccount: responseData.filter((item) => item.moduleType === FlowModuleTypeEnum.chatNode)
chatAccount: responseData.filter((item) => item.moduleType === FlowNodeTypeEnum.chatNode)
.length,
quoteList: responseData
.filter((item) => item.moduleType === FlowModuleTypeEnum.chatNode)
.map((item) => item.quoteList)
quoteList,
sourceList: Object.values(sourceList)
.flat()
.filter((item) => item) as SearchDataResponseItemType[],
.map((item) => ({
sourceName: item.sourceName,
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName })
})),
historyPreview: chatData?.historyPreview,
runningTime: +responseData.reduce((sum, item) => sum + (item.runningTime || 0), 0).toFixed(2)
};
@@ -50,64 +71,93 @@ const ResponseTags = ({ responseData = [] }: { responseData?: ChatHistoryItemRes
};
return responseData.length === 0 ? null : (
<Flex alignItems={'center'} mt={2} flexWrap={'wrap'}>
{quoteList.length > 0 && (
<MyTooltip label="查看引用">
<Tag
colorSchema="blue"
cursor={'pointer'}
{...TagStyles}
onClick={() => setQuoteModalData(quoteList)}
>
{quoteList.length}
</Tag>
</MyTooltip>
)}
{chatAccount === 1 && (
<>
{sourceList.length > 0 && (
<>
{historyPreview.length > 0 && (
<MyTooltip label={'点击查看完整对话记录'}>
<Tag
colorSchema="green"
<ChatBoxDivider icon="core/chat/quoteFill" text={t('chat.Quote')} />
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}>
{sourceList.map((item) => (
<Flex
key={item.sourceName}
alignItems={'center'}
flexWrap={'wrap'}
fontSize={'sm'}
cursor={'pointer'}
{...TagStyles}
onClick={() => setContextModalData(historyPreview)}
border={theme.borders.sm}
py={1}
px={2}
borderRadius={'md'}
_hover={{
bg: 'myBlue.100'
}}
onClick={() => setQuoteModalData(quoteList)}
>
{historyPreview.length}
</Tag>
</MyTooltip>
)}
<Image src={item.icon} alt={''} mr={1} w={'12px'} />
{item.sourceName}
</Flex>
))}
</Flex>
</>
)}
{chatAccount > 1 && (
<Tag colorSchema="blue" {...TagStyles}>
AI
</Tag>
)}
<Flex alignItems={'center'} mt={2} flexWrap={'wrap'}>
{quoteList.length > 0 && (
<MyTooltip label="查看引用">
<Tag
colorSchema="blue"
cursor={'pointer'}
{...TagStyles}
onClick={() => setQuoteModalData(quoteList)}
>
{quoteList.length}
</Tag>
</MyTooltip>
)}
{chatAccount === 1 && (
<>
{historyPreview.length > 0 && (
<MyTooltip label={'点击查看完整对话记录'}>
<Tag
colorSchema="green"
cursor={'pointer'}
{...TagStyles}
onClick={() => setContextModalData(historyPreview)}
>
{historyPreview.length}
</Tag>
</MyTooltip>
)}
</>
)}
{chatAccount > 1 && (
<Tag colorSchema="blue" {...TagStyles}>
AI
</Tag>
)}
{isPc && runningTime > 0 && (
<MyTooltip label={'模块运行时间和'}>
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
{runningTime}s
{isPc && runningTime > 0 && (
<MyTooltip label={'模块运行时间和'}>
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
{runningTime}s
</Tag>
</MyTooltip>
)}
<MyTooltip label={'点击查看完整响应'}>
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
{t('chat.Complete Response')}
</Tag>
</MyTooltip>
)}
<MyTooltip label={'点击查看完整响应'}>
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
{t('chat.Complete Response')}
</Tag>
</MyTooltip>
{!!quoteModalData && (
<QuoteModal rawSearch={quoteModalData} onClose={() => setQuoteModalData(undefined)} />
)}
{!!contextModalData && (
<ContextModal context={contextModalData} onClose={() => setContextModalData(undefined)} />
)}
{isOpenWholeModal && (
<WholeResponseModal response={responseData} onClose={onCloseWholeModal} />
)}
</Flex>
{!!quoteModalData && (
<QuoteModal rawSearch={quoteModalData} onClose={() => setQuoteModalData(undefined)} />
)}
{!!contextModalData && (
<ContextModal context={contextModalData} onClose={() => setContextModalData(undefined)} />
)}
{isOpenWholeModal && (
<WholeResponseModal response={responseData} onClose={onCloseWholeModal} />
)}
</Flex>
</>
);
};

View File

@@ -32,7 +32,7 @@ function Row({ label, value }: { label: string; value?: string | number | React.
) : null;
}
const ResponseModal = ({
const WholeResponseModal = ({
response,
onClose
}: {
@@ -50,6 +50,7 @@ const ResponseModal = ({
<Image
mr={2}
src={
item.moduleLogo ||
ModuleTemplatesFlat.find((template) => item.moduleType === template.flowType)?.logo
}
alt={''}
@@ -192,10 +193,22 @@ const ResponseModal = ({
}
})()}
/>
{/* plugin */}
<Row
label={t('chat.response.plugin output')}
value={(() => {
try {
return JSON.stringify(activeModule?.pluginOutput, null, 2);
} catch (error) {
return '';
}
})()}
/>
</Box>
</Flex>
</MyModal>
);
};
export default ResponseModal;
export default WholeResponseModal;

View File

@@ -35,7 +35,7 @@ import { feConfigs } from '@/web/common/system/staticData';
import { eventBus } from '@/web/common/utils/eventbus';
import { adaptChat2GptMessages } from '@/utils/common/adapt/message';
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
import { AppModuleItemType } from '@/types/app';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { VariableInputEnum } from '@/constants/app';
import { useForm } from 'react-hook-form';
import type { MessageItemType } from '@/types/core/chat/type';
@@ -54,6 +54,7 @@ import Avatar from '@/components/Avatar';
import Markdown from '@/components/Markdown';
import MySelect from '@/components/Select';
import MyTooltip from '../MyTooltip';
import ChatBoxDivider from '@/components/core/chat/Divider';
import dynamic from 'next/dynamic';
const ResponseTags = dynamic(() => import('./ResponseTags'));
const FeedbackModal = dynamic(() => import('./FeedbackModal'));
@@ -99,7 +100,7 @@ type Props = {
showEmptyIntro?: boolean;
appAvatar?: string;
userAvatar?: string;
userGuideModule?: AppModuleItemType;
userGuideModule?: ModuleItemType;
active?: boolean;
onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat?: (e: StartChatFnProps) => Promise<{
@@ -488,7 +489,7 @@ const ChatBox = (
return {
bg: colorMap[chatContent.status] || colorMap.loading,
name: t(chatContent.moduleName || 'Running')
name: t(chatContent.moduleName || 'common.Loading')
};
}, [chatHistory, isChatting, t]);
/* style end */
@@ -496,6 +497,7 @@ const ChatBox = (
// page change and abort request
useEffect(() => {
isNewChatReplace.current = false;
setQuestionGuide([]);
return () => {
chatController.current?.abort('leave');
if (!isNewChatReplace.current) {
@@ -750,38 +752,28 @@ const ChatBox = (
{index === chatHistory.length - 1 &&
!isChatting &&
questionGuides.length > 0 && (
<Flex
mt={2}
borderTop={theme.borders.sm}
alignItems={'center'}
flexWrap={'wrap'}
>
<Box
color={'myGray.500'}
mt={2}
mr={2}
fontSize={'sm'}
fontStyle={'italic'}
>
{t('chat.Question Guide Tips')}
</Box>
{questionGuides.map((item) => (
<Button
mt={2}
key={item}
mr="2"
borderRadius={'md'}
variant={'outline'}
colorScheme={'gray'}
size={'xs'}
onClick={() => {
resetInputVal(item);
}}
>
{item}
</Button>
))}
</Flex>
<Box mt={2}>
<ChatBoxDivider
icon="core/chat/QGFill"
text={t('chat.Question Guide Tips')}
/>
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}>
{questionGuides.map((item) => (
<Button
key={item}
borderRadius={'md'}
variant={'outline'}
colorScheme={'gray'}
size={'xs'}
onClick={() => {
resetInputVal(item);
}}
>
{item}
</Button>
))}
</Flex>
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698493025597" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4931" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M845.4 481.9c24.1 0 47.1 4.6 68.9 13.9 21.8 9.3 40.9 22.1 57.2 38.3 16.3 16.3 29.2 35.2 38.5 56.9 9.3 21.7 14 44.5 14 68.5 0 24.8-4.7 47.8-14 69.1-9.3 21.3-22.2 40.1-38.5 56.3-16.3 16.3-35.4 29-57.2 38.3-21.8 9.3-44.8 13.9-68.9 13.9-22.6 0-43.8-4.1-63.6-12.2-19.9-8.1-37.6-18.8-53.1-31.9v145.2c0 20.9-7.4 38.7-22.2 53.4-14.8 14.7-32.7 22.1-53.7 22.1H77.1c-21 0-39.1-7.4-54.3-22.1C7.6 977 0 959.2 0 938.3V809.4c3.9-16.3 10.9-28.3 21-36 10.1-7.7 24.9-5.8 44.4 5.8 9.3 5.4 16.3 8.5 21 9.3 20.2 10.1 40.9 15.1 61.9 15.1s40.7-3.9 59-11.6c18.3-7.7 34.2-18.4 47.9-31.9 13.6-13.5 24.3-29.2 32.1-47 7.8-17.8 11.7-37.2 11.7-58.1s-3.9-40.4-11.7-58.6-18.5-34.1-32.1-47.6c-13.6-13.5-29.6-24.2-47.9-31.9-18.3-7.7-37.9-11.6-59-11.6-20.2 0-40.1 4.3-59.5 12.8-4.7 1.5-8.6 3.1-11.7 4.6-9.3 4.6-18.5 8.9-27.4 12.8s-16.9 5.4-23.9 4.6c-7-0.8-12.8-4.6-17.5-11.6-4.7-7-7.4-18.6-8.2-34.8V371.6c0-20.9 7.6-38.9 22.8-54C37.9 302.5 56 295 77.1 295h182.1c-12.5-15.5-22.4-32.7-29.8-51.7-7.4-19-11.1-39.3-11.1-61 0-25.5 4.9-49.4 14.6-71.4 9.7-22.1 22.8-41.2 39.1-57.5 16.3-16.3 35.6-29.2 57.8-38.9C352 4.8 375.6 0 400.5 0c24.9 0 48.5 4.8 70.6 14.5 22.2 9.7 41.5 22.6 57.8 38.9s29.4 35.4 39.1 57.5c9.7 22.1 14.6 45.9 14.6 71.4 0 43.4-13.6 80.9-40.9 112.6h110.9c21 0 38.9 7.5 53.7 22.6 14.8 15.1 22.2 33.1 22.2 54V526c15.6-13.2 33.3-23.8 53.1-31.9 20-8.1 41.2-12.2 63.8-12.2z" p-id="4932"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698397550956" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2700" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M970.43915282 590.98409465a55.23363335 55.23363335 0 0 0-69.04204072 35.90186227A394.36813906 394.36813906 0 0 1 521.94205392 898.63543088 392.15879331 392.15879331 0 0 1 125.36456912 512a392.15879331 392.15879331 0 0 1 396.5774848-386.63543088 400.99617502 400.99617502 0 0 1 256.8363931 92.24016784l-119.85698388-19.88410786a55.23363335 55.23363335 0 0 0-63.518677 45.8439149 55.23363335 55.23363335 0 0 0 45.84391489 63.51867829l234.19060403 38.66354218h9.38971716a55.23363335 55.23363335 0 0 0 18.77943563-3.31401797 18.22709886 18.22709886 0 0 0 5.52336243-3.31401798 43.08223369 43.08223369 0 0 0 11.04672744-6.07569919l4.97102697-6.07569919c0-2.76168122 4.97102697-4.97102697 7.18037141-8.28504493s0-5.52336372 2.76168252-7.73270948a74.01306768 74.01306768 0 0 0 3.86635343-9.94205393l41.42522469-220.93453081a55.23363335 55.23363335 0 0 0-110.46726541-20.98878138l-14.91308088 80.08876817A508.7017592 508.7017592 0 0 0 521.94205392 14.8973037 502.62606002 502.62606002 0 0 0 14.8973037 512a502.62606002 502.62606002 0 0 0 507.04475022 497.1026963A503.73073225 503.73073225 0 0 0 1009.1026963 660.02613665a55.23363335 55.23363335 0 0 0-38.66354348-69.042042z" p-id="2701"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698504394130" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4081" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M928 448h-64a19.2 19.2 0 0 1 0-38.4h64a19.2 19.2 0 0 1 0 38.4zM797.1072 738.4064l-45.2608-45.2608a19.2 19.2 0 0 1 27.1488-27.1488l45.3248 45.2608a19.2 19.2 0 0 1-27.2128 27.1488zM779.008 204.4032a19.2 19.2 0 0 1-27.1488-27.1488l45.2608-45.2608a19.2 19.2 0 0 1 27.2 27.1488z m-121.216 472.0128a282.368 282.368 0 0 0-17.2032 77.5808v20.8A37.7856 37.7856 0 0 1 614.4 810.6752V819.2H409.6v-8.5248a37.7856 37.7856 0 0 1-26.1888-35.84v-23.9488a290.0352 290.0352 0 0 0-16.9344-74.24A279.04 279.04 0 0 1 243.2 443.5968C243.2 290.56 363.52 166.4 512 166.4s268.8 124.16 268.8 277.1968a279.04 279.04 0 0 1-123.008 232.8192zM505.6 691.2a19.2 19.2 0 1 0-19.2-19.2 19.2 19.2 0 0 0 19.2 19.2z m6.4-358.4a115.2 115.2 0 0 0-114.4448 102.4h6.5024a17.728 17.728 0 1 0 20.9024 0h8.6656A79.8848 79.8848 0 0 1 512 368.64a77.7216 77.7216 0 0 1 76.8 79.36c0.8064 45.6064-64 76.8-64 76.8a97.4976 97.4976 0 0 0-34.432 46.4768 18.7392 18.7392 0 0 0-3.968 11.1232v7.68a56.6784 56.6784 0 0 0 0 11.52v6.4a19.2 19.2 0 0 0 38.4 0v-14.4768a18.6496 18.6496 0 0 1 0.384-4.6336C533.3632 557.9904 588.8 524.8 588.8 524.8c36.2368-23.296 38.4-76.8 38.4-76.8a115.2 115.2 0 0 0-115.2-115.2z m6.4-230.4A19.2 19.2 0 0 1 499.2 83.2v-64a19.2 19.2 0 1 1 38.4 0v64A19.2 19.2 0 0 1 518.4 102.4z m-264.3456 92.9536l-45.2608-45.2608A19.2 19.2 0 1 1 235.9424 122.88l45.2608 45.248a19.2 19.2 0 0 1-27.1488 27.2256zM160 448h-64a19.2 19.2 0 0 1 0-38.4h64a19.2 19.2 0 0 1 0 38.4z m94.0544 227.0464a19.2 19.2 0 0 1 27.1488 27.1488L235.9424 747.52a19.2 19.2 0 0 1-27.1488-27.1488zM652.8 846.9376a8.384 8.384 0 0 1 0.384 1.9072V870.4a8.4096 8.4096 0 0 1-0.384 1.9072V883.2H371.2v-51.2h281.6v14.9376zM627.2 947.2H396.8v-51.2h230.4v51.2z m-183.6672 23.9104H579.84a8.5248 8.5248 0 0 1 4.8896 1.6896H601.6v38.4H422.4v-38.4h16.2432a8.5248 8.5248 0 0 1 4.8896-1.6896z" fill="#1296db" p-id="4082"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698497259520" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10081" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M156.09136 606.57001a457.596822 457.596822 0 0 1 221.680239-392.516385 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z m406.752731 0a457.596822 457.596822 0 0 1 221.680239-392.007944 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z" fill="#E67E22" p-id="10082"></path></svg>

After

Width:  |  Height:  |  Size: 819 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698491522536" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4046" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 642.9c81.6 0 157.6-42.3 229.5-130.9-72-88.6-147.9-130.9-229.5-130.9S354.4 423.4 282.5 512c71.9 88.6 147.9 130.9 229.5 130.9z m0 62.3c-105.3 0-201.4-53.6-288.5-160.7-15.4-18.9-15.4-46 0-64.9C310.6 372.4 406.7 318.8 512 318.8s201.4 53.6 288.5 160.7c15.4 18.9 15.4 46 0 64.9C713.4 651.6 617.3 705.2 512 705.2z m0 0" fill="#333333" p-id="4047"></path><path d="M512 540c15.5 0 28-12.5 28-28 0-15.4-12.5-28-28-28s-28 12.5-28 28c0 15.4 12.5 28 28 28z m0 71.9c-35.7 0-68.7-19-86.6-49.9-17.9-30.9-17.9-69 0-99.9 17.9-30.9 50.9-49.9 86.6-49.9 55.2 0 100 44.7 100 99.9 0 55.1-44.8 99.8-100 99.8z m0 0" fill="#333333" p-id="4048"></path><path d="M136 888V745c0-19.9-16.1-36-36-36s-36 16.1-36 36v155c0 33.1 26.9 60 60 60h155c19.9 0 36-16.1 36-36s-16.1-36-36-36H136zM136 136h143c19.9 0 36-16.1 36-36s-16.1-36-36-36H124c-33.1 0-60 26.9-60 60v155c0 19.9 16.1 36 36 36s36-16.1 36-36V136zM888 136v143c0 19.9 16.1 36 36 36s36-16.1 36-36V124c0-33.1-26.9-60-60-60H745c-19.9 0-36 16.1-36 36s16.1 36 36 36h143zM888 888H745c-19.9 0-36 16.1-36 36s16.1 36 36 36h155c33.1 0 60-26.9 60-60V745c0-19.9-16.1-36-36-36s-36 16.1-36 36v143z" fill="#333333" p-id="4049"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -92,7 +92,13 @@ const iconPaths = {
pause: () => import('./icons/common/pause.svg'),
'core/app/aiLight': () => import('./icons/core/app/aiLight.svg'),
'core/app/aiFill': () => import('./icons/core/app/aiFill.svg'),
'common/text/t': () => import('./icons/common/text/t.svg')
'common/text/t': () => import('./icons/common/text/t.svg'),
'common/navbar/pluginLight': () => import('./icons/common/navbar/pluginLight.svg'),
'common/navbar/pluginFill': () => import('./icons/common/navbar/pluginFill.svg'),
'common/refreshLight': () => import('./icons/common/refreshLight.svg'),
'core/module/previewLight': () => import('./icons/core/module/previewLight.svg'),
'core/chat/quoteFill': () => import('./icons/core/chat/quoteFill.svg'),
'core/chat/QGFill': () => import('./icons/core/chat/QGFill.svg')
};
export type IconName = keyof typeof iconPaths;

View File

@@ -40,6 +40,13 @@ const Navbar = ({ unread }: { unread: number }) => {
link: `/app/list`,
activeLink: ['/app/list', '/app/detail']
},
{
label: t('navbar.Plugin'),
icon: 'common/navbar/pluginLight',
activeIcon: 'common/navbar/pluginFill',
link: `/plugin/list`,
activeLink: ['/plugin/list', '/plugin/edit']
},
{
label: t('navbar.Datasets'),
icon: 'dbLight',

View File

@@ -43,7 +43,6 @@ const MyModal = ({
{...props}
>
{!!title && <ModalHeader>{title}</ModalHeader>}
{onClose && <ModalCloseButton />}
<Box
overflow={props.overflow || 'overlay'}
h={'100%'}
@@ -52,6 +51,7 @@ const MyModal = ({
>
{children}
</Box>
{onClose && <ModalCloseButton />}
</ModalContent>
</Modal>
);

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { Box, useTheme, type BoxProps } from '@chakra-ui/react';
import MyBox from '../common/MyBox';
const PageContainer = ({ children, ...props }: BoxProps) => {
const PageContainer = ({ children, ...props }: BoxProps & { isLoading?: boolean }) => {
const theme = useTheme();
return (
<Box bg={'myGray.100'} h={'100%'} p={[0, 5]} px={[0, 6]} {...props}>
<MyBox bg={'myGray.100'} h={'100%'} p={[0, 5]} px={[0, 6]} {...props}>
<Box
h={'100%'}
bg={'white'}
@@ -14,7 +15,7 @@ const PageContainer = ({ children, ...props }: BoxProps) => {
>
{children}
</Box>
</Box>
</MyBox>
);
};

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import MyModal from '../MyModal';
import { Box, Button, Grid, useTheme } from '@chakra-ui/react';
import { Box, Button, Flex, Grid, useTheme } from '@chakra-ui/react';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type.d';
import { ModalBody, ModalFooter } from '@chakra-ui/react';
@@ -13,14 +13,14 @@ const PromptTemplate = ({
title: string;
templates: PromptTemplateItem[];
onClose: () => void;
onSuccess: (e: string) => void;
onSuccess: (e: PromptTemplateItem) => void;
}) => {
const theme = useTheme();
const [selectTemplateTitle, setSelectTemplateTitle] = useState<PromptTemplateItem>();
return (
<MyModal isOpen title={title} onClose={onClose}>
<ModalBody w={'600px'}>
<MyModal isOpen title={title} onClose={onClose} isCentered>
<ModalBody h="100%" w={'600px'} maxW={'90vw'} overflowY={'auto'}>
<Grid gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={4}>
{templates.map((item) => (
<Box
@@ -38,8 +38,9 @@ const PromptTemplate = ({
onClick={() => setSelectTemplateTitle(item)}
>
<Box>{item.title}</Box>
<Box color={'myGray.600'} fontSize={'sm'} whiteSpace={'pre-wrap'}>
{item.value}
{item.desc}
</Box>
</Box>
))}
@@ -50,7 +51,7 @@ const PromptTemplate = ({
disabled={!selectTemplateTitle}
onClick={() => {
if (!selectTemplateTitle) return;
onSuccess(selectTemplateTitle.value);
onSuccess(selectTemplateTitle);
onClose();
}}
>

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import Loading from '@/components/Loading';
type Props = BoxProps & {
isLoading?: boolean;
text?: string;
};
const MyBox = ({ text, isLoading, children, ...props }: Props) => {
return (
<Box position={'relative'} {...props}>
{children}
{isLoading && <Loading fixed={false} text={text} />}
</Box>
);
};
export default MyBox;

View File

@@ -30,9 +30,9 @@ const ParentPaths = (props: {
{concatPaths.map((item, i) => (
<Flex key={item.parentId} alignItems={'center'}>
<Box
fontSize={['md', 'lg']}
fontSize={['sm', 'lg']}
py={1}
px={[0, 2]}
px={[1, 2]}
borderRadius={'md'}
{...(i === concatPaths.length - 1
? {
@@ -51,7 +51,7 @@ const ParentPaths = (props: {
{item.parentName}
</Box>
{i !== concatPaths.length - 1 && (
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['14px', '24px']} />
)}
</Flex>
))}

View File

@@ -0,0 +1,19 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import MyIcon, { type IconName } from '@/components/Icon';
const ChatBoxDivider = ({ icon, text }: { icon: IconName; text: string }) => {
return (
<Box>
<Flex alignItems={'center'} py={2} gap={2}>
<MyIcon name={icon} w={'14px'} color={'myGray.900'} />
<Box color={'myGray.500'} fontSize={'sm'}>
{text}
</Box>
<Box h={'1px'} mt={1} bg={'myGray.200'} flex={'1'} />
</Flex>
</Box>
);
};
export default ChatBoxDivider;

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { EditFormType } from '@/web/core/app/basicSettings';
@@ -45,7 +45,6 @@ const AIChatSettingsModal = ({
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
key: 'quoteTemplate' | 'quotePrompt';
templates: PromptTemplateItem[];
}>();
@@ -163,8 +162,7 @@ const AIChatSettingsModal = ({
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择引用内容模板',
key: 'quoteTemplate',
title: '选择知识库提示词模板',
templates: Prompt_QuoteTemplateList
})
}
@@ -190,19 +188,6 @@ const AIChatSettingsModal = ({
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择引用提示词模板',
key: 'quotePrompt',
templates: Prompt_QuotePromptList
})
}
>
</Box>
</Flex>
<Textarea
rows={11}
@@ -227,7 +212,12 @@ const AIChatSettingsModal = ({
title={selectTemplateData.title}
templates={selectTemplateData.templates}
onClose={() => setSelectTemplateData(undefined)}
onSuccess={(e) => setValue(selectTemplateData.key, e)}
onSuccess={(e) => {
const quoteVal = e.value;
const promptVal = Prompt_QuotePromptList.find((item) => item.title === e.title)?.value;
setValue('quoteTemplate', quoteVal);
setValue('quotePrompt', promptVal);
}}
/>
)}
</MyModal>

View File

@@ -1,4 +1,4 @@
import { AppModuleItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { AppSchema } from '@/types/mongoSchema';
import React, {
useMemo,
@@ -10,7 +10,7 @@ import React, {
} from 'react';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
@@ -28,7 +28,7 @@ const ChatTest = (
onClose
}: {
app: AppSchema;
modules?: AppModuleItemType[];
modules?: ModuleItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
@@ -41,7 +41,7 @@ const ChatTest = (
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.find((item) => item.flowType === FlowNodeTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = chatList.slice(-historyMaxLen - 2, -2);

View File

@@ -10,9 +10,12 @@ import {
} from 'reactflow';
import type {
FlowModuleItemType,
FlowOutputTargetItemType,
FlowModuleItemChangeProps
} from '@/types/core/app/flow';
FlowModuleTemplateType
} from '@fastgpt/global/core/module/type.d';
import type {
FlowNodeOutputTargetItemType,
FlowNodeChangeProps
} from '@fastgpt/global/core/module/node/type';
import React, {
type SetStateAction,
type Dispatch,
@@ -25,17 +28,21 @@ import React, {
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@/web/common/hooks/useToast';
import { FlowModuleTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum,
FlowNodeValTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { useTranslation } from 'next-i18next';
import { AppModuleItemType } from '@/types/app';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type useFlowStoreType = {
appId: string;
export type useFlowProviderStoreType = {
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
filterAppIds: string[];
nodes: Node<FlowModuleItemType, string | undefined>[];
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
@@ -44,8 +51,9 @@ export type useFlowStoreType = {
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onDelNode: (nodeId: string) => void;
onChangeNode: (e: FlowModuleItemChangeProps) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onCopyNode: (nodeId: string) => void;
onResetNode: (id: string, module: FlowModuleTemplateType) => void;
onDelEdge: (e: {
moduleId: string;
sourceHandle?: string | undefined;
@@ -53,12 +61,12 @@ export type useFlowStoreType = {
}) => void;
onDelConnect: (id: string) => void;
onConnect: ({ connect }: { connect: Connection }) => any;
initData: (modules: AppModuleItemType[]) => void;
initData: (modules: ModuleItemType[]) => void;
};
const StateContext = createContext<useFlowStoreType>({
appId: '',
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
@@ -78,11 +86,10 @@ const StateContext = createContext<useFlowStoreType>({
onFixView: function (): void {
return;
},
onDelNode: function (nodeId: string): void {
return;
},
onChangeNode: function (e: FlowModuleItemChangeProps): void {
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onCopyNode: function (nodeId: string): void {
@@ -101,13 +108,22 @@ const StateContext = createContext<useFlowStoreType>({
onConnect: function ({ connect }: { connect: Connection }) {
return;
},
initData: function (modules: AppModuleItemType[]): void {
initData: function (modules: ModuleItemType[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (id: string, module: FlowModuleTemplateType): void {
throw new Error('Function not implemented.');
}
});
export const useFlowStore = () => useContext(StateContext);
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({ appId, children }: { appId: string; children: React.ReactNode }) => {
export const FlowProvider = ({
filterAppIds = [],
children
}: {
filterAppIds?: string[];
children: React.ReactNode;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
@@ -156,8 +172,11 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
({ connect }: { connect: Connection }) => {
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
if (source?.flowType === FlowModuleTypeEnum.classifyQuestion) {
return FlowValueTypeEnum.boolean;
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion) {
return FlowNodeValTypeEnum.boolean;
}
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
}
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
})();
@@ -173,8 +192,8 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
});
}
if (
sourceType !== FlowValueTypeEnum.any &&
targetType !== FlowValueTypeEnum.any &&
sourceType !== FlowNodeValTypeEnum.any &&
targetType !== FlowNodeValTypeEnum.any &&
sourceType !== targetType
) {
return toast({
@@ -209,15 +228,31 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
);
const onChangeNode = useCallback(
({ moduleId, key, type = 'inputs', value }: FlowModuleItemChangeProps) => {
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
const updateObj: Record<string, any> = {};
if (type === 'inputs') {
if (type === 'attr') {
if (key) {
updateObj[key] = value;
}
} else if (type === 'updateInput') {
updateObj.inputs = node.data.inputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceInput') {
onDelEdge({ moduleId, targetHandle: key });
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === key);
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addInput',
index: oldInputIndex,
value
});
});
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === value.key);
if (input) {
@@ -227,22 +262,51 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
});
updateObj.inputs = node.data.inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
if (index !== undefined) {
const inputs = [...node.data.inputs];
inputs.splice(index, 0, value);
updateObj.inputs = inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
}
}
} else if (type === 'delInput') {
onDelEdge({ moduleId, targetHandle: key });
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
} else if (type === 'attr') {
updateObj[key] = value;
} else if (type === 'outputs') {
// del output connect
const delOutputs = node.data.outputs.filter(
(item) => !value.find((output: FlowOutputTargetItemType) => output.key === item.key)
);
delOutputs.forEach((output) => {
onDelEdge({ moduleId, sourceHandle: output.key });
} else if (type === 'updateOutput') {
updateObj.outputs = node.data.outputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceOutput') {
onDelEdge({ moduleId, sourceHandle: key });
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === key);
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addOutput',
index: oldOutputIndex,
value
});
});
updateObj.outputs = value;
} else if (type === 'addOutput') {
const output = node.data.outputs.find((output) => output.key === value.key);
if (output) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.outputs = node.data.outputs;
} else {
if (index !== undefined) {
const outputs = [...node.data.outputs];
outputs.splice(index, 0, value);
updateObj.outputs = outputs;
} else {
updateObj.outputs = node.data.outputs.concat(value);
}
}
} else if (type === 'delOutput') {
onDelEdge({ moduleId, sourceHandle: key });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
}
return {
@@ -287,8 +351,36 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
[setNodes]
);
// reset a node data. delete edge and replace it
const onResetNode = useCallback(
(id: string, module: FlowModuleTemplateType) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ moduleId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ moduleId: id, sourceHandle: item.key });
});
return {
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
},
[onDelEdge, setNodes]
);
const initData = useCallback(
(modules: AppModuleItemType[]) => {
(modules: ModuleItemType[]) => {
const edges = appModule2FlowEdge({
modules,
onDelete: onDelConnect
@@ -304,7 +396,7 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
// use eventbus to avoid refresh ReactComponents
useEffect(() => {
const update = (e: FlowModuleItemChangeProps) => {
const update = (e: FlowNodeChangeProps) => {
onChangeNode(e);
};
eventBus.on(EventNameEnum.updaterNode, update);
@@ -314,8 +406,8 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
}, [onChangeNode]);
const value = {
appId,
reactFlowWrapper,
filterAppIds,
nodes,
setNodes,
onNodesChange,
@@ -325,6 +417,7 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
onFixView,
onDelNode,
onChangeNode,
onResetNode,
onCopyNode,
onDelEdge,
onDelConnect,
@@ -337,6 +430,53 @@ export const FlowProvider = ({ appId, children }: { appId: string; children: Rea
export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowModuleItemChangeProps) => {
export const onChangeNode = (e: FlowNodeChangeProps) => {
eventBus.emit(EventNameEnum.updaterNode, e);
};
export function flowNode2Modules({
nodes,
edges
}: {
nodes: Node<FlowModuleItemType, string | undefined>[];
edges: Edge<any>[];
}) {
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
logo: item.data.logo,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((item) => ({
...item,
connected: item.connected ?? item.type !== FlowNodeInputTypeEnum.target
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowNodeOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected =
input.connected ||
!!edges.find((edge) => edge.target === module.moduleId && edge.targetHandle === input.key);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter(
(edge) =>
edge.source === module.moduleId && edge.sourceHandle === output.key && edge.targetHandle
)
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
}

View File

@@ -3,13 +3,13 @@ import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowStore } from './Provider';
import { useFlowProviderStore } from './FlowProvider';
const ImportSettings = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [value, setValue] = useState('');
const { setNodes, setEdges, initData } = useFlowStore();
const { setNodes, setEdges, initData } = useFlowProviderStore();
return (
<MyModal isOpen w={'600px'} onClose={onClose} title={t('app.Import Config')}>

View File

@@ -1,22 +1,22 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { getMyModels } from '@/web/core/app/api';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@/types/core/app/flow';
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'react-i18next';
import { useLoading } from '@/web/common/hooks/useLoading';
import { useUserStore } from '@/web/support/user/useUserStore';
const SelectAppModal = ({
defaultApps = [],
filterApps = [],
filterAppIds = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
filterApps?: string[];
filterAppIds?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
@@ -26,11 +26,12 @@ const SelectAppModal = ({
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { data = [], isLoading } = useQuery(['loadMyApos'], () => getMyModels());
const { myApps, loadMyApps } = useUserStore();
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
const apps = useMemo(
() => data.filter((app) => !filterApps.includes(app._id)),
[data, filterApps]
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
[myApps, filterAppIds]
);
return (
@@ -38,13 +39,13 @@ const SelectAppModal = ({
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
onClose={onClose}
w={'700px'}
minW={'700px'}
position={'relative'}
>
<ModalBody
minH={'300px'}
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3,1fr)']}
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
gridGap={4}
>
{apps.map((app) => (
@@ -53,8 +54,7 @@ const SelectAppModal = ({
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
px={1}
py={2}
p={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {

View File

@@ -0,0 +1,243 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import type {
FlowModuleTemplateType,
SystemModuleTemplateType
} from '@fastgpt/global/core/module/type.d';
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
import { useFlowProviderStore } from './FlowProvider';
import { customAlphabet } from 'nanoid';
import { appModule2FlowNode } from '@/utils/adapt';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyIcon from '@/components/Icon';
import EmptyTip from '@/components/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { getPluginModuleDetail } from '@/web/core/plugin/api';
import { useToast } from '@/web/common/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
enum TemplateTypeEnum {
system = 'system',
combine = 'combine'
}
export type ModuleTemplateProps = {
systemTemplates: SystemModuleTemplateType;
pluginTemplates: SystemModuleTemplateType;
show2Plugin?: boolean;
};
const ModuleTemplateList = ({
systemTemplates,
pluginTemplates,
show2Plugin = false,
isOpen,
onClose
}: ModuleTemplateProps & {
isOpen: boolean;
onClose: () => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
const [templateType, setTemplateType] = React.useState(TemplateTypeEnum.system);
const typeList = useMemo(
() => [
{
type: TemplateTypeEnum.system,
label: t('app.module.System Module'),
child: <RenderList templates={systemTemplates} onClose={onClose} />
},
{
type: TemplateTypeEnum.combine,
label: t('plugin.Plugin Module'),
child: <RenderList templates={pluginTemplates} onClose={onClose} />
}
],
[pluginTemplates, onClose, systemTemplates, t]
);
const TemplateItem = useMemo(
() => typeList.find((item) => item.type === templateType)?.child,
[templateType, typeList]
);
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={'360px'}
onClick={onClose}
/>
<Flex
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'65px'}
left={0}
pb={4}
h={isOpen ? 'calc(100% - 100px)' : '0'}
w={isOpen ? ['100%', '360px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'20px'}
overflow={'hidden'}
transition={'.2s ease'}
userSelect={'none'}
>
<Flex pt={4} pb={1} px={5} gap={4} alignItems={'center'} fontSize={['md', 'xl']}>
{typeList.map((item) => (
<Box
key={item.label}
borderBottom={'2px solid transparent'}
{...(item.type === templateType
? {
color: 'myBlue.700',
borderBottomColor: 'myBlue.700',
fontWeight: 'bold'
}
: {
cursor: 'pointer',
onClick: () => setTemplateType(item.type)
})}
>
{item.label}
</Box>
))}
<Box flex={1} />
{show2Plugin && templateType === TemplateTypeEnum.combine && (
<Flex
alignItems={'center'}
_hover={{ textDecoration: 'underline' }}
cursor={'pointer'}
onClick={() => router.push('/plugin/list')}
>
<Box fontSize={'sm'} transform={'translateY(-1px)'}>
{t('plugin.To Edit Plugin')}
</Box>
<MyIcon name={'rightArrowLight'} w={'12px'} />
</Flex>
)}
</Flex>
{TemplateItem}
</Flex>
</>
);
};
export default React.memo(ModuleTemplateList);
var RenderList = React.memo(function RenderList({
templates,
onClose
}: {
templates: {
label: string;
list: FlowModuleTemplateType[];
}[];
onClose: () => void;
}) {
const { t } = useTranslation();
const { isPc } = useSystemStore();
const { setNodes, reactFlowWrapper } = useFlowProviderStore();
const { x, y, zoom } = useViewport();
const { setLoading } = useSystemStore();
const { toast } = useToast();
const onAddNode = useCallback(
async ({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
if (!reactFlowWrapper?.current) return;
let templateModule = { ...template };
// get plugin module
try {
if (templateModule.flowType === FlowNodeTypeEnum.pluginModule) {
setLoading(true);
const pluginModule = await getPluginModuleDetail(templateModule.id);
templateModule = {
...templateModule,
...pluginModule
};
}
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
} finally {
setLoading(false);
}
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
setNodes((state) =>
state.concat(
appModule2FlowNode({
item: {
...templateModule,
moduleId: nanoid(),
position: { x: mouseX, y: mouseY - 20 }
}
})
)
);
},
[reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
);
const list = useMemo(() => templates.map((item) => item.list).flat(), [templates]);
return list.length === 0 ? (
<EmptyTip text={t('app.module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'}>
<Box w={['100%', '330px']} mx={'auto'}>
{list.map((item) => (
<Flex
key={item.id}
alignItems={'center'}
p={5}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'md'}
draggable
onDragEnd={(e) => {
if (e.clientX < 360) return;
onAddNode({
template: item,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (isPc) return;
onClose();
onAddNode({
template: item,
position: { x: e.clientX, y: e.clientY }
});
}}
>
<Avatar src={item.logo} w={'34px'} objectFit={'contain'} borderRadius={'0'} />
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{item.name}</Box>
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'sm'}>
{item.intro}
</Box>
</Box>
</Flex>
))}
</Box>
</Box>
);
});

View File

@@ -0,0 +1,153 @@
import React, { useState } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@/components/MyModal';
import Avatar from '@/components/Avatar';
import { FlowNodeValTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { useTranslation } from 'react-i18next';
import MySelect from '@/components/Select';
const typeSelectList = [
{
label: '字符串',
value: FlowNodeValTypeEnum.string
},
{
label: '数字',
value: FlowNodeValTypeEnum.number
},
{
label: '布尔',
value: FlowNodeValTypeEnum.boolean
},
{
label: '历史记录',
value: FlowNodeValTypeEnum.chatHistory
},
{
label: '引用内容',
value: FlowNodeValTypeEnum.datasetQuote
},
{
label: '任意',
value: FlowNodeValTypeEnum.any
}
];
export type EditFieldModeType = 'input' | 'output' | 'pluginInput';
export type EditFieldType = {
key: string;
label?: string;
valueType?: `${FlowNodeValTypeEnum}`;
description?: string;
required?: boolean;
};
const FieldEditModal = ({
mode,
defaultField = {
label: '',
key: '',
description: '',
valueType: FlowNodeValTypeEnum.string,
required: false
},
onClose,
onSubmit
}: {
mode: EditFieldModeType;
defaultField?: EditFieldType;
onClose: () => void;
onSubmit: (data: EditFieldType) => void;
}) => {
const { t } = useTranslation();
const { register, getValues, setValue, handleSubmit } = useForm<EditFieldType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
const title = ['input', 'pluginInput'].includes(mode)
? t('app.Input Field Settings')
: t('app.Output Field Settings');
return (
<MyModal
isOpen={true}
title={
<Flex alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
{title}
</Flex>
}
onClose={onClose}
>
<ModalBody minH={'260px'} overflow={'visible'}>
{mode === 'input' && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('required')} />
</Flex>
)}
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<MySelect
w={'288px'}
list={typeSelectList}
value={getValues('valueType')}
onchange={(e: string) => {
const type = e as `${FlowNodeValTypeEnum}`;
setValue('valueType', type);
if (
type === FlowNodeValTypeEnum.chatHistory ||
type === FlowNodeValTypeEnum.datasetQuote
) {
const label = typeSelectList.find((item) => item.value === type)?.label;
setValue('label', label);
}
setRefresh(!refresh);
}}
/>
</Flex>
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="预约字段/sql语句……"
{...register('label', { required: '字段名不能为空' })}
/>
</Flex>
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="appointment/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
<Flex mb={5} alignItems={'flex-start'}>
<Box flex={'0 0 70px'}></Box>
<Textarea placeholder="可选" rows={3} {...register('description')} />
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -0,0 +1,196 @@
import React, { useMemo } from 'react';
import { Box, Flex, useTheme, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import Avatar from '@/components/Avatar';
import type { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowProviderStore, onChangeNode } from '../../FlowProvider';
import {
FlowNodeSpecialInputKeyEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getPluginModuleDetail } from '@/web/core/plugin/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useConfirm } from '@/web/common/hooks/useConfirm';
type Props = FlowModuleItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
isPreview?: boolean;
};
const NodeCard = (props: Props) => {
const {
children,
logo = '/icon/logo.svg',
name = '未知模块',
description,
minW = '300px',
moduleId,
flowType,
inputs,
isPreview
} = props;
const { onCopyNode, onResetNode, onDelNode } = useFlowProviderStore();
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
const { setLoading } = useSystemStore();
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const { openConfirm, ConfirmModal } = useConfirm({
content: t('module.Confirm Sync Plugin')
});
const menuList = useMemo(
() => [
...(flowType === FlowNodeTypeEnum.pluginModule
? [
{
icon: 'common/refreshLight',
label: t('plugin.Synchronous version'),
onClick: () => {
const pluginId = inputs.find(
(item) => item.key === FlowNodeSpecialInputKeyEnum.pluginId
)?.value;
if (!pluginId) return;
openConfirm(async () => {
try {
setLoading(true);
const pluginModule = await getPluginModuleDetail(pluginId);
onResetNode(moduleId, pluginModule);
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
}
setLoading(false);
})();
}
}
]
: [
{
icon: 'edit',
label: t('common.Rename'),
onClick: () =>
onOpenModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
moduleId,
type: 'attr',
key: 'name',
value: e
});
}
})
}
]),
{
icon: 'copy',
label: t('common.Copy'),
onClick: () => onCopyNode(moduleId)
},
{
icon: 'delete',
label: t('common.Delete'),
onClick: () => onDelNode(moduleId)
},
{
icon: 'back',
label: t('common.Back'),
onClick: () => {}
}
],
[
flowType,
inputs,
moduleId,
name,
onCopyNode,
onDelNode,
onOpenModal,
onResetNode,
openConfirm,
setLoading,
t,
toast
]
);
return (
<Box
minW={minW}
maxW={'500px'}
bg={'white'}
border={theme.borders.md}
borderRadius={'md'}
boxShadow={'sm'}
className={isPreview ? 'nodrag' : ''}
>
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
<Avatar src={logo} borderRadius={'md'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
{name}
</Box>
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon
display={['none', 'inline']}
transform={'translateY(1px)'}
mb={'1px'}
ml={1}
/>
</MyTooltip>
)}
<Box flex={1} />
{!isPreview && (
<Menu autoSelect={false} isLazy>
<MenuButton
className={'nodrag'}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
borderRadius={'md'}
onClick={(e) => {
e.stopPropagation();
}}
>
<MyIcon name={'more'} w={'14px'} p={2} />
</MenuButton>
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
{menuList.map((item) => (
<MenuItem key={item.label} onClick={item.onClick} py={[2, 3]}>
<MyIcon name={item.icon as any} w={['14px', '16px']} />
<Box ml={[1, 2]}>{item.label}</Box>
</MenuItem>
))}
</MenuList>
</Menu>
)}
</Flex>
{children}
<EditTitleModal />
<ConfirmModal />
</Box>
);
};
export default React.memo(NodeCard);

Some files were not shown because too many files have changed in this diff Show More