Compare commits

...

7 Commits

Author SHA1 Message Date
Archer
c5664c7e90 feat: vision model (#489)
* mongo init

* perf: mongo connect

* perf: tts

perf: whisper and tts

peref: tts whisper permission

log

reabase (#488)

* perf: modal

* i18n

* perf: schema lean

* feat: vision model format

* perf: tts loading

* perf: static data

* perf: tts

* feat: image

* perf: image

* perf: upload image and title

* perf: image size

* doc

* perf: color

* doc

* speaking can not select file

* doc
2023-11-18 15:42:35 +08:00
heheer
70f3373246 add image input (#486)
* add image input

* use json
2023-11-17 18:22:29 +08:00
左风
af16817a4a add wechat (#482) 2023-11-17 17:15:41 +08:00
Archer
4358b6de4d Add whisper and tts ui (#484)
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
2023-11-17 00:03:05 +08:00
Archer
f6aea484ce fix: 46 tmbId empty (#480)
* mongo init

* perf: mongo connect

* perf: favicon

* fix: member  id

* 46fix sh

* doc
2023-11-16 17:10:04 +08:00
Archer
fbe1d8cfed Fixed the duplicate data check problem, history filter and add tts stream (#477) 2023-11-16 16:22:08 +08:00
Archer
16103029f5 doc and config rerank (#475) 2023-11-16 10:46:47 +08:00
124 changed files with 2075 additions and 683 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

View File

@@ -36,6 +36,7 @@ weight: 520
"quoteMaxToken": 2000, // 最大引用内容长度
"maxTemperature": 1.2, // 最大温度值
"censor": false, // 是否开启敏感词过滤(商业版)
"vision": false, // 支持图片输入
"defaultSystemChatPrompt": ""
},
{
@@ -47,6 +48,7 @@ weight: 520
"quoteMaxToken": 8000,
"maxTemperature": 1.2,
"censor": false,
"vision": false,
"defaultSystemChatPrompt": ""
},
{
@@ -58,6 +60,19 @@ weight: 520
"quoteMaxToken": 4000,
"maxTemperature": 1.2,
"censor": false,
"vision": false,
"defaultSystemChatPrompt": ""
},
{
"model": "gpt-4-vision-preview",
"name": "GPT4-Vision",
"maxContext": 128000,
"maxResponse": 4000,
"price": 0,
"quoteMaxToken": 100000,
"maxTemperature": 1.2,
"censor": false,
"vision": true,
"defaultSystemChatPrompt": ""
}
],
@@ -123,13 +138,23 @@ weight: 520
{
"model": "tts-1",
"name": "OpenAI TTS1",
"price": 0
},
{
"model": "tts-1-hd",
"name": "OpenAI TTS1HD",
"price": 0
"price": 0,
"baseUrl": "",
"key": "",
"voices": [
{ "label": "Alloy", "value": "alloy", "bufferId": "openai-Alloy" },
{ "label": "Echo", "value": "echo", "bufferId": "openai-Echo" },
{ "label": "Fable", "value": "fable", "bufferId": "openai-Fable" },
{ "label": "Onyx", "value": "onyx", "bufferId": "openai-Onyx" },
{ "label": "Nova", "value": "nova", "bufferId": "openai-Nova" },
{ "label": "Shimmer", "value": "shimmer", "bufferId": "openai-Shimmer" }
]
}
]
],
"WhisperModel": {
"model": "whisper-1",
"name": "Whisper1",
"price": 0
}
}
```

View File

@@ -7,16 +7,16 @@ toc: true
weight: 836
---
# V4.6 版本加入了简单的团队功能,可以邀请其他用户进来管理资源。该版本升级后无法执行旧的升级脚本,且无法回退。
**V4.6 版本加入了简单的团队功能,可以邀请其他用户进来管理资源。该版本升级后无法执行旧的升级脚本,且无法回退。**
# 1. 更新镜像并变更配置文件
## 1. 更新镜像并变更配置文件
更新镜像至 latest 或者 v4.6 版本。商业版镜像更新至 V0.2.1
最新配置可参考: [V46版本最新 config.json](/docs/development/configuration),商业镜像配置文件也更新,参考最新的飞书文档。
# 2. 执行初始化 API
## 2. 执行初始化 API
发起 2 个 HTTP 请求({{rootkey}} 替换成环境变量里的`rootkey`{{host}}替换成自己域名)
@@ -45,7 +45,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv46-2' \
4. 初始化 Mongo Data
# V4.6功能介绍
## V4.6功能介绍
1. 新增 - 团队空间
2. 新增 - 多路向量(多个向量映射一组数据)
@@ -53,3 +53,15 @@ curl --location --request POST 'https://{{host}}/api/admin/initv46-2' \
4. 新增 - 支持知识库配置文本预处理模型
5. 线上环境新增 - ReRank向量召回提高召回精度
6. 优化 - 知识库导出,可直接触发流下载,无需等待转圈圈
## 4.6缺陷修复
旧的 4.6 版本由于缺少一个字段,导致文件导入时知识库数据无法显示,可执行下面的脚本:
https://xxxxx/api/admin/initv46-fix
```bash
curl --location --request POST 'https://{{host}}/api/admin/initv46-fix' \
--header 'rootkey: {{rootkey}}' \
--header 'Content-Type: application/json'
```

View File

@@ -0,0 +1,16 @@
---
title: 'V4.6.1'
description: 'FastGPT V4.6 .1'
icon: 'upgrade'
draft: false
toc: true
weight: 835
---
## V4.6.1 功能介绍
1. 新增 - GPT4-v 模型支持
2. 新增 - whisper 语音输入
3. 优化 - TTS 流传输
4. 优化 - TTS 缓存

View File

@@ -9,23 +9,23 @@ weight: 310
在 FastGPT 的 AI 对话模块中,有一个 AI 高级配置,里面包含了 AI 模型的参数配置,本文详细介绍这些配置的含义。
# 返回AI内容
## 返回AI内容
这是一个开关,打开的时候,当 AI 对话模块运行时会将其输出的内容返回到浏览器API响应如果关闭AI 输出的内容不会返回到浏览器但是生成的内容仍可以通过【AI回复】进行输出。你可以将【AI回复】连接到其他模块中。
# 温度
## 温度
可选范围0-10约大代表生成的内容约自由扩散越小代表约严谨。调节能力有限知识库问答场景通常设置为0。
# 回复上限
## 回复上限
控制 AI 回复的最大 Tokens较小的值可以一定程度上减少 AI 的废话,但也可能导致 AI 回复不完整。
# 引用模板 & 引用提示词
## 引用模板 & 引用提示词
这两个参数与知识库问答场景相关,可以控制知识库相关的提示词。
## AI 对话消息组成
### AI 对话消息组成
想使用明白这两个变量,首先要了解传递传递给 AI 模型的消息格式。它是一个数组FastGPT 中这个数组的组成形式为:
@@ -42,7 +42,7 @@ weight: 310
Tips: 可以通过点击上下文按键查看完整的上下文组成,便于调试。
{{% /alert %}}
## 引用模板和提示词设计
### 引用模板和提示词设计
引用模板和引用提示词通常是成对出现,引用提示词依赖引用模板。
@@ -50,7 +50,7 @@ FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变
可以通过 [知识库结构讲解](/docs/use-cases/datasetEngine/) 了解详细的知识库的结构。
### 引用模板
#### 引用模板
```
{instruction:"{{q}}",output:"{{a}}",source:"{{source}}"}
@@ -64,7 +64,7 @@ FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变
{instruction:"电影《铃芽之旅》的编剧是谁22",output:"新海诚是本片的编剧。",source:"手动输入"}
```
### 引用提示词
#### 引用提示词
引用模板需要和引用提示词一起使用,提示词中可以写引用模板的格式说明以及对话的要求等。可以使用 {{quote}} 来使用 **引用模板**,使用 {{question}} 来引入问题。例如:
@@ -95,15 +95,15 @@ FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变
我的问题是:"{{question}}"
```
### 总结
#### 总结
引用模板规定了搜索出来的内容如何组成一句话,其由 q,a,index,source 多个变量组成。
引用提示词由`引用模板``提示词`组成,提示词通常是对引用模板的一个描述,加上对模型的要求。
## 引用模板和提示词设计 示例
### 引用模板和提示词设计 示例
### 通用模板与问答模板对比
#### 通用模板与问答模板对比
我们通过一组`你是谁`的手动数据,对通用模板与问答模板的效果进行对比。此处特意打了个搞笑的答案,通用模板下 GPT35 就变得不那么听话了,而问答模板下 GPT35 依然能够回答正确。这是由于结构化的提示词,在大语言模型中具有更强的引导作用。
@@ -117,7 +117,7 @@ Tips: 建议根据不同的场景每种知识库仅选择1类数据类型
| ![](/imgs/datasetprompt3.png) | ![](/imgs/datasetprompt5.png) |
| ![](/imgs/datasetprompt4.png) | ![](/imgs/datasetprompt6.png) |
### 严格模板
#### 严格模板
使用非严格模板,我们随便询问一个不在知识库中的内容,模型通常会根据其自身知识进行回答。
@@ -125,7 +125,7 @@ Tips: 建议根据不同的场景每种知识库仅选择1类数据类型
| --- | --- | --- |
| ![](/imgs/datasetprompt7.png) | ![](/imgs/datasetprompt8.png) |![](/imgs/datasetprompt9.png) |
### 提示词设计思路
#### 提示词设计思路
1. 使用序号进行不同要求描述。
2. 使用首先、然后、最后等词语进行描述。

View File

@@ -7,7 +7,7 @@ toc: true
weight: 311
---
# 理解向量
## 理解向量
FastGPT 采用了 RAG 中的 Embedding 方案构建知识库,要使用好 FastGPT 需要简单的理解`Embedding`向量是如何工作的及其特点。
@@ -21,7 +21,7 @@ FastGPT 采用了 RAG 中的 Embedding 方案构建知识库,要使用好 Fast
检索器的精度比较容易解决,向量模型的训练略复杂,因此数据和检索词质量优化成了一个重要的环节。
# FastGPT 中向量的结构设计
## FastGPT 中向量的结构设计
FastGPT 采用了 `PostgresSQL``PG Vector` 插件作为向量检索器,索引为`HNSW`。且`PostgresSQL`仅用于向量检索,`MongoDB`用于其他数据的存取。
@@ -29,13 +29,13 @@ FastGPT 采用了 `PostgresSQL` 的 `PG Vector` 插件作为向量检索器,
![](/imgs/datasetSetting1.png)
## 多向量的目的和使用方式
### 多向量的目的和使用方式
在一组数据中,如果我们希望它尽可能长,但语义又要在向量中尽可能提现,则没有办法通过一组向量来表示。因此,我们采用了多向量映射的方式,将一组数据映射到多组向量中,从而保障数据的完整性和语义的提现
在一组向量中内容的长度和语义的丰富度通常是矛盾的无法兼得。因此FastGPT 采用了多向量映射的方式,将一组数据映射到多组向量中,从而保障数据的完整性和语义的丰富度
你可以为一组较长的文本,添加多组向量,从而在检索时,只要其中一组向量被检索到,该数据也将被召回。
## 提高向量搜索精度的方法
### 提高向量搜索精度的方法
1. 更好分词分段:当一段话的结构和语义是完整的,并且是单一的,精度也会提高。因此,许多系统都会优化分词器,尽可能的保障每组数据的完整性。
2. 精简`index`的内容,减少向量内容的长度:当`index`的内容更少,更准确时,检索精度自然会提高。但与此同时,会牺牲一定的检索范围,适合答案较为严格的场景。
@@ -43,7 +43,7 @@ FastGPT 采用了 `PostgresSQL` 的 `PG Vector` 插件作为向量检索器,
4. 优化检索词:在实际使用过程中,用户的问题通常是模糊的或是缺失的,并不一定是完整清晰的问题。因此优化用户的问题(检索词)很大程度上也可以提高精度。
5. 微调向量模型:由于市面上直接使用的向量模型都是通用型模型,在特定领域的检索精度并不高,因此微调向量模型可以很大程度上提高专业领域的检索效果。
# FastGPT 构建知识库方案
## FastGPT 构建知识库方案
在 FastGPT 中,整个知识库由库、集合和数据 3 部分组成。集合可以简单理解为一个`文件`。一个`库`中可以包含多个`集合`,一个`集合`中可以包含多组`数据`。最小的搜索单位是`库`,也就是说,知识库搜索时,是对整个`库`进行搜索,而集合仅是为了对数据进行分类管理,与搜索效果无关。(起码目前还是)
@@ -51,7 +51,7 @@ FastGPT 采用了 `PostgresSQL` 的 `PG Vector` 插件作为向量检索器,
| --- | --- | --- |
| ![](/imgs/datasetEngine1.png) | ![](/imgs/datasetEngine2.png) | ![](/imgs/datasetEngine3.png) |
## 导入数据方案1 - 直接分段导入
### 导入数据方案1 - 直接分段导入
选择文件导入时,可以选择直接分段方案。直接分段会利用`句子分词器`对文本进行一定长度拆分,最终分割中多组的`q`。如果使用了直接分段方案,我们建议在`应用`设置`引用提示词`时,使用`通用模板`即可,无需选择`问答模板`
@@ -60,7 +60,7 @@ FastGPT 采用了 `PostgresSQL` 的 `PG Vector` 插件作为向量检索器,
| ![](/imgs/datasetEngine4.png) | ![](/imgs/datasetEngine5.png) |
## 导入数据方案2 - QA导入
### 导入数据方案2 - QA导入
选择文件导入时可以选择QA拆分方案。仍然需要使用到`句子分词器`对文本进行拆分,但长度比直接分段大很多。在导入后,会先调用`大模型`对分段进行学习,并给出一些`问题``答案`,最终问题和答案会一起被存储到`q`中。注意,新版的 FastGPT 为了提高搜索的范围,不再将问题和答案分别存储到 qa 中。
@@ -68,7 +68,7 @@ FastGPT 采用了 `PostgresSQL` 的 `PG Vector` 插件作为向量检索器,
| --- | --- |
| ![](/imgs/datasetEngine6.png) | ![](/imgs/datasetEngine7.png) |
## 导入数据方案3 - 手动录入
### 导入数据方案3 - 手动录入
在 FastGPT 中,你可以在任何一个`集合`中点击右上角的`插入`手动录入知识点,或者使用`标注`功能手动录入。被搜索的内容为`q`,补充内容(可选)为`a`
@@ -76,16 +76,16 @@ FastGPT 采用了 `PostgresSQL` 的 `PG Vector` 插件作为向量检索器,
| --- | --- | --- |
| ![](/imgs/datasetEngine8.png) | ![](/imgs/datasetEngine9.png) | ![](/imgs/datasetEngine10.png) |
## 导入数据方案4 - CSV录入
### 导入数据方案4 - CSV录入
有些数据较为独特,可能需要单独的进行预处理分割后再导入 FastGPT此时可以选择 csv 导入,可批量的将处理好的数据导入。
![](/imgs/datasetEngine11.png)
## 导入数据方案5 - API导入
### 导入数据方案5 - API导入
参考[FastGPT OpenAPI使用](/docs/development/openapi/#知识库添加数据)。
# QA的组合与引用提示词构建
## QA的组合与引用提示词构建
参考[引用模板与引用提示词示例](/docs/use-cases/ai_settings/#示例)

View File

@@ -0,0 +1,76 @@
---
title: " 接入微信和企业微信 "
description: "FastGPT 接入微信和企业微信 "
icon: "chat"
draft: false
toc: true
weight: 322
---
# FastGPT 三分钟接入微信/企业微信
私人微信和企业微信接入的方式基本一样,不同的地方会刻意指出。
[查看视频教程](https://www.bilibili.com/video/BV1cu411F7FN/?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=903c2b09b7412037c2eddc6a8fb9828b)
## 创建APIKey
首先找到我们需要接入的应用,然后点击「外部使用」->「API访问」创建一个APIKey并保存。
![](/imgs/wechat1.png)
## 配置微秘书
打开[微秘书](https://wechat.aibotk.com?r=zWLnZK) 注册登陆后找到菜单栏「基础配置」->「智能配置」,按照下图配置。
![](/imgs/wechat2.png)
继续往下看到 `apikey``服务器根地址`,这里`apikey`填写我们在 FastGPT 应用外部访问中创建的 APIkey服务器根地址填写官方地址或者私有化部署的地址这里用官方地址示例注意要添加`/v1`后缀,填写完毕后保存。
![](/imgs/wechat3.png)
## sealos部署服务
[访问sealos](https://cloud.sealos.io/) 登陆进来之后打开「应用管理」-> 「新建应用」。
- 应用名:称随便填写
- 镜像名:私人微信填写 aibotk/wechat-assistant 企业微信填写 aibotk/worker-assistant
- cpu和内存建议 1c1g
![](/imgs/wechat4.png)
往下翻页找到「高级配置」-> 「编辑环境变量」
![](/imgs/wechat5.png)
这里需要填写四个环境变量:
AIBOTK_KEY="微秘书 APIKEY"
AIBOTK_SECRET="微秘书 APISECRET"
WORK_PRO_TOKEN="你申请的企微 token" (企业微信需要填写,私人微信不需要)
WECHATY_PUPPET_SERVICE_AUTHORITY=token-service-discovery-test.juzibot.com企业微信需要填写私人微信不需要
这里最后两个变量只有部署企业微信才需要,私人微信只需要填写前两个即可。
![](/imgs/wechat6.png)
这里环境变量我们介绍下如何填写:
`AIBOTK_KEY``AIBOTK_SECRET` 我们需要回到[微秘书](https://wechat.aibotk.com?r=zWLnZK)找到「个人中心」,这里的 APIKEY 对应 AIBOTK_KEY APISECRET 对应 `AIBOTK_SECRET`
![](/imgs/wechat7.png)
`WORK_PRO_TOKEN` [点击这里](https://tss.juzibot.com?aff=aibotk)申请 token 然后填入即可。
`WECHATY_PUPPET_SERVICE_AUTHORITY`的值复制过去就可以。
填写完毕后点右上角「部署」,等待应用状态变为运行中。
![](/imgs/wechat8.png)
返回[微秘书](https://wechat.aibotk.com?r=zWLnZK) 找到「首页」,扫码登陆需要接入的微信号。
![](/imgs/wechat9.png)
## 测试
只需要发送信息,或者拉入群聊@登陆的微信就会回复信息啦
![](/imgs/wechat10.png)

View File

@@ -24,7 +24,7 @@ export const simpleText = (text: string) => {
};
/*
replace {{variable}} to value
replace {{variable}} to value
*/
export function replaceVariable(text: string, obj: Record<string, string | number>) {
for (const key in obj) {

View File

@@ -9,6 +9,7 @@ export type ChatModelItemType = LLMModelItemType & {
quoteMaxToken: number;
maxTemperature: number;
censor?: boolean;
vision?: boolean;
defaultSystemChatPrompt?: string;
};
@@ -29,4 +30,13 @@ export type AudioSpeechModelType = {
model: string;
name: string;
price: number;
baseUrl?: string;
key?: string;
voices: { label: string; value: string; bufferId: string }[];
};
export type WhisperModelType = {
model: string;
name: string;
price: number;
};

View File

@@ -3,7 +3,8 @@ import type {
ChatModelItemType,
FunctionModelItemType,
VectorModelItemType,
AudioSpeechModelType
AudioSpeechModelType,
WhisperModelType
} from './model.d';
export const defaultChatModels: ChatModelItemType[] = [
@@ -16,6 +17,7 @@ export const defaultChatModels: ChatModelItemType[] = [
quoteMaxToken: 2000,
maxTemperature: 1.2,
censor: false,
vision: false,
defaultSystemChatPrompt: ''
},
{
@@ -27,6 +29,7 @@ export const defaultChatModels: ChatModelItemType[] = [
quoteMaxToken: 8000,
maxTemperature: 1.2,
censor: false,
vision: false,
defaultSystemChatPrompt: ''
},
{
@@ -38,6 +41,19 @@ export const defaultChatModels: ChatModelItemType[] = [
quoteMaxToken: 4000,
maxTemperature: 1.2,
censor: false,
vision: false,
defaultSystemChatPrompt: ''
},
{
model: 'gpt-4-vision-preview',
name: 'GPT4-Vision',
maxContext: 128000,
maxResponse: 4000,
price: 0,
quoteMaxToken: 100000,
maxTemperature: 1.2,
censor: false,
vision: true,
defaultSystemChatPrompt: ''
}
];
@@ -105,11 +121,20 @@ export const defaultAudioSpeechModels: AudioSpeechModelType[] = [
{
model: 'tts-1',
name: 'OpenAI TTS1',
price: 0
},
{
model: 'tts-1-hd',
name: 'OpenAI TTS1',
price: 0
price: 0,
voices: [
{ label: 'Alloy', value: 'Alloy', bufferId: 'openai-Alloy' },
{ label: 'Echo', value: 'Echo', bufferId: 'openai-Echo' },
{ label: 'Fable', value: 'Fable', bufferId: 'openai-Fable' },
{ label: 'Onyx', value: 'Onyx', bufferId: 'openai-Onyx' },
{ label: 'Nova', value: 'Nova', bufferId: 'openai-Nova' },
{ label: 'Shimmer', value: 'Shimmer', bufferId: 'openai-Shimmer' }
]
}
];
export const defaultWhisperModel: WhisperModelType = {
model: 'whisper-1',
name: 'Whisper1',
price: 0
};

View File

@@ -1,8 +0,0 @@
import { Text2SpeechVoiceEnum } from './constant';
export type Text2SpeechProps = {
model?: string;
voice?: `${Text2SpeechVoiceEnum}`;
input: string;
speed?: number;
};

View File

@@ -1,17 +0,0 @@
export enum Text2SpeechVoiceEnum {
alloy = 'alloy',
echo = 'echo',
fable = 'fable',
onyx = 'onyx',
nova = 'nova',
shimmer = 'shimmer'
}
export const openaiTTSList = [
Text2SpeechVoiceEnum.alloy,
Text2SpeechVoiceEnum.echo,
Text2SpeechVoiceEnum.fable,
Text2SpeechVoiceEnum.onyx,
Text2SpeechVoiceEnum.nova,
Text2SpeechVoiceEnum.shimmer
];
export const openaiTTSModel = 'tts-1';

View File

@@ -5,12 +5,14 @@ import type {
ChatCompletionMessageParam,
ChatCompletionContentPart
} from 'openai/resources';
export type ChatCompletionContentPart = ChatCompletionContentPart;
export type ChatCompletionCreateParams = ChatCompletionCreateParams;
export type ChatMessageItemType = Omit<ChatCompletionMessageParam> & {
export type ChatMessageItemType = Omit<ChatCompletionMessageParam, 'name'> & {
name?: any;
dataId?: string;
content: any;
};
} & any;
export type ChatCompletion = ChatCompletion;
export type StreamChatType = Stream<ChatCompletionChunk>;

View File

@@ -1,7 +1,6 @@
import { ModuleItemType } from '../module/type';
import { AppTypeEnum } from './constants';
import { PermissionTypeEnum } from '../../support/permission/constant';
import { Text2SpeechVoiceEnum } from '../ai/speech/constant';
export interface AppSchema {
_id: string;

View File

@@ -54,3 +54,6 @@ export const ChatSourceMap = {
export const HUMAN_ICON = `/icon/human.svg`;
export const LOGO_ICON = `/icon/logo.svg`;
export const IMG_BLOCK_KEY = 'img-block';
export const FILE_BLOCK_KEY = 'file-block';

View File

@@ -39,7 +39,6 @@ export type ChatItemSchema = {
userFeedback?: string;
adminFeedback?: AdminFbkType;
[TaskResponseKeyEnum.responseData]?: ChatHistoryItemResType[];
tts?: Buffer;
};
export type AdminFbkType = {
@@ -62,7 +61,7 @@ export type ChatItemType = {
export type ChatSiteItemType = {
status: 'loading' | 'running' | 'finish';
moduleName?: string;
ttsBuffer?: Buffer;
ttsBuffer?: Uint8Array;
} & ChatItemType;
export type HistoryItemType = {

View File

@@ -0,0 +1,6 @@
import { IMG_BLOCK_KEY, FILE_BLOCK_KEY } from './constants';
export function chatContentReplaceBlock(content: string = '') {
const regex = new RegExp(`\`\`\`(${IMG_BLOCK_KEY})\\n([\\s\\S]*?)\`\`\``, 'g');
return content.replace(regex, '').trim();
}

View File

@@ -32,6 +32,7 @@ export type FlowNodeInputItemType = {
connected?: boolean;
description?: string;
placeholder?: string;
plusField?: boolean;
max?: number;
min?: number;
step?: number;

View File

@@ -0,0 +1,36 @@
import { connectionMongo, type Model } from '../../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { TTSBufferSchemaType } from './type.d';
export const collectionName = 'ttsbuffers';
const TTSBufferSchema = new Schema({
bufferId: {
type: String,
required: true
},
text: {
type: String,
required: true
},
buffer: {
type: Buffer,
required: true
},
createTime: {
type: Date,
default: () => new Date()
}
});
try {
TTSBufferSchema.index({ bufferId: 1 });
// 24 hour
TTSBufferSchema.index({ createTime: 1 }, { expireAfterSeconds: 24 * 60 * 60 });
} catch (error) {
console.log(error);
}
export const MongoTTSBuffer: Model<TTSBufferSchemaType> =
models[collectionName] || model(collectionName, TTSBufferSchema);
MongoTTSBuffer.syncIndexes();

View File

@@ -0,0 +1,5 @@
export type TTSBufferSchemaType = {
bufferId: string;
text: string;
buffer: Buffer;
};

View File

@@ -5,12 +5,26 @@ export function getMongoImgUrl(id: string) {
return `${imageBaseUrl}${id}`;
}
export async function uploadMongoImg({ base64Img, userId }: { base64Img: string; userId: string }) {
export const maxImgSize = 1024 * 1024 * 12;
export async function uploadMongoImg({
base64Img,
teamId,
expiredTime
}: {
base64Img: string;
teamId: string;
expiredTime?: Date;
}) {
if (base64Img.length > maxImgSize) {
return Promise.reject('Image too large');
}
const base64Data = base64Img.split(',')[1];
const { _id } = await MongoImage.create({
userId,
binary: Buffer.from(base64Data, 'base64')
teamId,
binary: Buffer.from(base64Data, 'base64'),
expiredTime
});
return getMongoImgUrl(String(_id));

View File

@@ -1,16 +1,27 @@
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { connectionMongo, type Model } from '../../mongo';
const { Schema, model, models } = connectionMongo;
const ImageSchema = new Schema({
userId: {
teamId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
ref: TeamCollectionName
},
binary: {
type: Buffer
},
expiredTime: {
type: Date
}
});
export const MongoImage: Model<{ userId: string; binary: Buffer }> =
try {
ImageSchema.index({ expiredTime: 1 }, { expireAfterSeconds: 60 });
} catch (error) {
console.log(error);
}
export const MongoImage: Model<{ teamId: string; binary: Buffer }> =
models['image'] || model('image', ImageSchema);
MongoImage.syncIndexes();

View File

@@ -32,10 +32,10 @@ export function getUploadModel({ maxSize = 500 }: { maxSize?: number }) {
})
}).any();
async doUpload(req: NextApiRequest, res: NextApiResponse) {
async doUpload<T = Record<string, any>>(req: NextApiRequest, res: NextApiResponse) {
return new Promise<{
files: FileType[];
metadata: Record<string, any>;
metadata: T;
bucketName?: `${BucketNameEnum}`;
}>((resolve, reject) => {
// @ts-ignore

View File

@@ -1,26 +1,49 @@
import { Text2SpeechProps } from '@fastgpt/global/core/ai/speech/api';
import type { NextApiResponse } from 'next';
import { getAIApi } from '../config';
import { defaultAudioSpeechModels } from '../../../../global/core/ai/model';
import { Text2SpeechVoiceEnum } from '@fastgpt/global/core/ai/speech/constant';
import { UserModelSchema } from '@fastgpt/global/support/user/type';
export async function text2Speech({
model = defaultAudioSpeechModels[0].model,
voice = Text2SpeechVoiceEnum.alloy,
res,
onSuccess,
onError,
input,
speed = 1
}: Text2SpeechProps) {
const ai = getAIApi();
const mp3 = await ai.audio.speech.create({
model = defaultAudioSpeechModels[0].model,
voice,
speed = 1,
props
}: {
res: NextApiResponse;
onSuccess: (e: { model: string; buffer: Buffer }) => void;
onError: (e: any) => void;
input: string;
model: string;
voice: string;
speed?: number;
props?: UserModelSchema['openaiAccount'];
}) {
const ai = getAIApi(props);
const response = await ai.audio.speech.create({
model,
// @ts-ignore
voice,
input,
response_format: 'mp3',
speed
});
const buffer = Buffer.from(await mp3.arrayBuffer());
return {
model,
voice,
tts: buffer
};
const readableStream = response.body as unknown as NodeJS.ReadableStream;
readableStream.pipe(res);
let bufferStore = Buffer.from([]);
readableStream.on('data', (chunk) => {
bufferStore = Buffer.concat([bufferStore, chunk]);
});
readableStream.on('end', () => {
onSuccess({ model, buffer: bufferStore });
});
readableStream.on('error', (e) => {
onError(e);
});
}

View File

@@ -6,7 +6,7 @@ export const baseUrl = process.env.ONEAPI_URL || openaiBaseUrl;
export const systemAIChatKey = process.env.CHAT_API_KEY || '';
export const getAIApi = (props?: UserModelSchema['openaiAccount'], timeout = 6000) => {
export const getAIApi = (props?: UserModelSchema['openaiAccount'], timeout = 60000) => {
return new OpenAI({
apiKey: props?.key || systemAIChatKey,
baseURL: props?.baseUrl || baseUrl,

View File

@@ -67,3 +67,5 @@ try {
export const MongoApp: Model<AppType> =
models[appCollectionName] || model(appCollectionName, AppSchema);
MongoApp.syncIndexes();

View File

@@ -68,9 +68,6 @@ const ChatItemSchema = new Schema({
[TaskResponseKeyEnum.responseData]: {
type: Array,
default: []
},
tts: {
type: Buffer
}
});
@@ -86,3 +83,5 @@ try {
export const MongoChatItem: Model<ChatItemType> =
models['chatItem'] || model('chatItem', ChatItemSchema);
MongoChatItem.syncIndexes();

View File

@@ -92,7 +92,7 @@ const ChatSchema = new Schema({
});
try {
ChatSchema.index({ userId: 1 });
ChatSchema.index({ tmbId: 1 });
ChatSchema.index({ updateTime: -1 });
ChatSchema.index({ appId: 1 });
} catch (error) {
@@ -101,3 +101,4 @@ try {
export const MongoChat: Model<ChatType> =
models[chatCollectionName] || model(chatCollectionName, ChatSchema);
MongoChat.syncIndexes();

View File

@@ -1,7 +1,8 @@
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatRoleEnum, IMG_BLOCK_KEY } from '@fastgpt/global/core/chat/constants';
import { countMessagesTokens, countPromptTokens } from '@fastgpt/global/common/string/tiktoken';
import { adaptRole_Chat2Message } from '@fastgpt/global/core/chat/adapt';
import type { ChatCompletionContentPart } from '@fastgpt/global/core/ai/type.d';
/* slice chat context by tokens */
export function ChatContextFilter({
@@ -51,3 +52,101 @@ export function ChatContextFilter({
return [...systemPrompts, ...chats];
}
/**
string to vision model. Follow the markdown code block rule for interception:
@rule:
```img-block
{src:""}
{src:""}
```
```file-block
{name:"",src:""},
{name:"",src:""}
```
@example:
Whats in this image?
```img-block
{src:"https://1.png"}
```
@return
[
{ type: 'text', text: 'Whats in this image?' },
{
type: 'image_url',
image_url: {
url: 'https://1.png'
}
}
]
*/
export function formatStr2ChatContent(str: string) {
const content: ChatCompletionContentPart[] = [];
let lastIndex = 0;
const regex = new RegExp(`\`\`\`(${IMG_BLOCK_KEY})\\n([\\s\\S]*?)\`\`\``, 'g');
let match;
while ((match = regex.exec(str)) !== null) {
// add previous text
if (match.index > lastIndex) {
const text = str.substring(lastIndex, match.index).trim();
if (text) {
content.push({ type: 'text', text });
}
}
const blockType = match[1].trim();
if (blockType === IMG_BLOCK_KEY) {
const blockContentLines = match[2].trim().split('\n');
const jsonLines = blockContentLines.map((item) => {
try {
return JSON.parse(item) as { src: string };
} catch (error) {
return { src: '' };
}
});
for (const item of jsonLines) {
if (!item.src) throw new Error("image block's content error");
}
content.push(
...jsonLines.map((item) => ({
type: 'image_url' as any,
image_url: {
url: item.src
}
}))
);
}
lastIndex = regex.lastIndex;
}
// add remaining text
if (lastIndex < str.length) {
const remainingText = str.substring(lastIndex).trim();
if (remainingText) {
content.push({ type: 'text', text: remainingText });
}
}
// Continuous text type content, if type=text, merge them
for (let i = 0; i < content.length - 1; i++) {
const currentContent = content[i];
const nextContent = content[i + 1];
if (currentContent.type === 'text' && nextContent.type === 'text') {
currentContent.text += nextContent.text;
content.splice(i + 1, 1);
i--;
}
}
if (content.length === 1 && content[0].type === 'text') {
return content[0].text;
}
return content ? content : null;
}

View File

@@ -22,9 +22,9 @@ export async function findDatasetIdTreeByTopDatasetId(
}
export async function getCollectionWithDataset(collectionId: string) {
const data = (
await MongoDatasetCollection.findById(collectionId).populate('datasetId')
)?.toJSON() as CollectionWithDatasetType;
const data = (await MongoDatasetCollection.findById(collectionId)
.populate('datasetId')
.lean()) as CollectionWithDatasetType;
if (!data) {
return Promise.reject('Collection is not exist');
}

View File

@@ -76,3 +76,4 @@ try {
export const MongoDatasetData: Model<DatasetDataSchemaType> =
models[DatasetDataCollectionName] || model(DatasetDataCollectionName, DatasetDataSchema);
MongoDatasetData.syncIndexes();

View File

@@ -82,3 +82,4 @@ try {
export const MongoDataset: Model<DatasetSchemaType> =
models[DatasetCollectionName] || model(DatasetCollectionName, DatasetSchema);
MongoDataset.syncIndexes();

View File

@@ -104,3 +104,5 @@ try {
export const MongoDatasetTraining: Model<DatasetTrainingSchemaType> =
models[DatasetTrainingCollectionName] || model(DatasetTrainingCollectionName, TrainingDataSchema);
MongoDatasetTraining.syncIndexes();

View File

@@ -46,10 +46,11 @@ const PluginSchema = new Schema({
});
try {
PluginSchema.index({ userId: 1 });
PluginSchema.index({ tmbId: 1 });
} catch (error) {
console.log(error);
}
export const MongoPlugin: Model<PluginItemSchema> =
models[ModuleCollectionName] || model(ModuleCollectionName, PluginSchema);
MongoPlugin.syncIndexes();

View File

@@ -31,3 +31,4 @@ const PromotionRecordSchema = new Schema({
export const MongoPromotionRecord: Model<PromotionRecordType> =
models['promotionRecord'] || model('promotionRecord', PromotionRecordSchema);
MongoPromotionRecord.syncIndexes();

View File

@@ -70,3 +70,4 @@ const OpenApiSchema = new Schema(
export const MongoOpenApi: Model<OpenApiSchema> =
models['openapi'] || model('openapi', OpenApiSchema);
MongoOpenApi.syncIndexes();

View File

@@ -71,3 +71,5 @@ const OutLinkSchema = new Schema({
export const MongoOutLink: Model<SchemaType> =
models['outlinks'] || model('outlinks', OutLinkSchema);
MongoOutLink.syncIndexes();

View File

@@ -22,12 +22,12 @@ export async function authApp({
}
> {
const result = await parseHeaderCert(props);
const { userId, teamId, tmbId } = result;
const { teamId, tmbId } = result;
const { role } = await getTeamInfoByTmbId({ tmbId });
const { app, isOwner, canWrite } = await (async () => {
// get app
const app = (await MongoApp.findOne({ _id: appId, teamId }))?.toJSON();
const app = await MongoApp.findOne({ _id: appId, teamId }).lean();
if (!app) {
return Promise.reject(AppErrEnum.unAuthApp);
}

View File

@@ -24,9 +24,9 @@ export async function authChat({
const { chat, isOwner, canWrite } = await (async () => {
// get chat
const chat = (
await MongoChat.findOne({ chatId, teamId }).populate('appId')
)?.toJSON() as ChatWithAppSchema;
const chat = (await MongoChat.findOne({ chatId, teamId })
.populate('appId')
.lean()) as ChatWithAppSchema;
if (!chat) {
return Promise.reject('Chat is not exists');

View File

@@ -31,7 +31,7 @@ export async function authDataset({
const { role } = await getTeamInfoByTmbId({ tmbId });
const { dataset, isOwner, canWrite } = await (async () => {
const dataset = (await MongoDataset.findOne({ _id: datasetId, teamId }))?.toObject();
const dataset = await MongoDataset.findOne({ _id: datasetId, teamId }).lean();
if (!dataset) {
return Promise.reject(DatasetErrEnum.unAuthDataset);

View File

@@ -64,3 +64,4 @@ const UserSchema = new Schema({
export const MongoUser: Model<UserModelSchema> =
models[userCollectionName] || model(userCollectionName, UserSchema);
MongoUser.syncIndexes();

View File

@@ -59,3 +59,4 @@ try {
}
export const MongoBill: Model<BillType> = models['bill'] || model('bill', BillSchema);
MongoBill.syncIndexes();

View File

@@ -15,6 +15,7 @@
"quoteMaxToken": 2000,
"maxTemperature": 1.2,
"censor": false,
"vision": false,
"defaultSystemChatPrompt": ""
},
{
@@ -26,6 +27,7 @@
"quoteMaxToken": 8000,
"maxTemperature": 1.2,
"censor": false,
"vision": false,
"defaultSystemChatPrompt": ""
},
{
@@ -37,6 +39,19 @@
"quoteMaxToken": 4000,
"maxTemperature": 1.2,
"censor": false,
"vision": false,
"defaultSystemChatPrompt": ""
},
{
"model": "gpt-4-vision-preview",
"name": "GPT4-Vision",
"maxContext": 128000,
"maxResponse": 4000,
"price": 0,
"quoteMaxToken": 100000,
"maxTemperature": 1.2,
"censor": false,
"vision": true,
"defaultSystemChatPrompt": ""
}
],
@@ -102,12 +117,22 @@
{
"model": "tts-1",
"name": "OpenAI TTS1",
"price": 0
},
{
"model": "tts-1-hd",
"name": "OpenAI TTS1HD",
"price": 0
"price": 0,
"baseUrl": "",
"key": "",
"voices": [
{ "label": "Alloy", "value": "alloy", "bufferId": "openai-Alloy" },
{ "label": "Echo", "value": "echo", "bufferId": "openai-Echo" },
{ "label": "Fable", "value": "fable", "bufferId": "openai-Fable" },
{ "label": "Onyx", "value": "onyx", "bufferId": "openai-Onyx" },
{ "label": "Nova", "value": "nova", "bufferId": "openai-Nova" },
{ "label": "Shimmer", "value": "shimmer", "bufferId": "openai-Shimmer" }
]
}
]
],
"WhisperModel": {
"model": "whisper-1",
"name": "Whisper1",
"price": 0
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.6",
"version": "4.6.1",
"private": false,
"scripts": {
"dev": "next dev",

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -73,6 +73,7 @@
"Complete Response": "Complete Response",
"Confirm to clear history": "Confirm to clear history?",
"Confirm to clear share chat history": " Are you sure to delete all chats?",
"Converting to text": "Converting to text...",
"Exit Chat": "Exit",
"Feedback Close": "Close Feedback",
"Feedback Failed": "Feedback Failed",
@@ -190,6 +191,7 @@
"Update Success": "Update Success",
"Update Successful": "Update Successful",
"Update Time": "Update Time",
"Upload File Failed": "Upload File Failed",
"Username": "UserName",
"error": {
"unKnow": "There was an accident"
@@ -216,12 +218,15 @@
"app": {
"Next Step Guide": "Next step guide",
"Question Guide Tip": "At the end of the conversation, three leading questions will be asked.",
"Select TTS": "Select TTS",
"TTS": "Audio Speech",
"TTS Tip": "After this function is enabled, the voice playback function can be used after each conversation. Use of this feature may incur additional charges.",
"tts": {
"Close": "NoUse",
"Model alloy": "Female - Alloy",
"Model echo": "Male - Echo",
"Speech model": "Speech model",
"Speech speed": "Speed",
"Test Listen": "Test",
"Test Listen Text": "Hello, this is FastGPT, how can I help you?",
"Web": "Browser (free)"
@@ -231,8 +236,14 @@
"Audio Speech Error": "Audio Speech Error",
"Record": "Speech",
"Restart": "Restart",
"Select File": "Select file",
"Send Message": "Send Message",
"Stop Speak": "Stop Speak"
"Speaking": "I'm listening...",
"Stop Speak": "Stop Speak",
"Type a message": "Input problem",
"tts": {
"Stop Speech": "Stop"
}
},
"dataset": {
"Choose Dataset": "Choose Dataset",
@@ -313,10 +324,15 @@
"Course": "Document",
"Delete": "Delete",
"Index": "Index({{amount}})"
}
},
"input is empty": "The data content cannot be empty"
},
"deleteDatasetTips": "Are you sure to delete the knowledge base? Data cannot be recovered after deletion, please confirm!",
"deleteFolderTips": "Are you sure to delete this folder and all the knowledge bases it contains? Data cannot be recovered after deletion, please confirm!",
"import csv tip": "Ensure that the CSV is in UTF-8 format; otherwise, garbled characters will be displayed",
"recall": {
"rerank": "Rerank"
},
"test": {
"noResult": "Search results are empty"
}
@@ -575,6 +591,7 @@
"wallet": {
"bill": {
"Audio Speech": "Audio Speech",
"Whisper": "Whisper",
"bill username": "User"
}
}

View File

@@ -73,6 +73,7 @@
"Complete Response": "完整响应",
"Confirm to clear history": "确认清空该应用的在线聊天记录?分享和 API 调用的记录不会被清空。",
"Confirm to clear share chat history": "确认删除所有聊天记录?",
"Converting to text": "正在转换为文本...",
"Exit Chat": "退出聊天",
"Feedback Close": "关闭反馈",
"Feedback Failed": "提交反馈异常",
@@ -190,6 +191,7 @@
"Update Success": "更新成功",
"Update Successful": "更新成功",
"Update Time": "更新时间",
"Upload File Failed": "上传文件失败",
"Username": "用户名",
"error": {
"unKnow": "出现了点意外~"
@@ -216,12 +218,15 @@
"app": {
"Next Step Guide": "下一步指引",
"Question Guide Tip": "对话结束后,会为生成 3 个引导性问题。",
"Select TTS": "选择语音播放模式",
"TTS": "语音播报",
"TTS Tip": "开启后,每次对话后可使用语音播放功能。使用该功能可能产生额外费用。",
"tts": {
"Close": "不使用",
"Model alloy": "女声 - Alloy",
"Model echo": "男声 - Echo",
"Speech model": "语音模型",
"Speech speed": "语速",
"Test Listen": "试听",
"Test Listen Text": "你好,我是 FastGPT有什么可以帮助你么",
"Web": "浏览器自带(免费)"
@@ -231,8 +236,14 @@
"Audio Speech Error": "语音播报异常",
"Record": "语音输入",
"Restart": "重开对话",
"Select File": "选择文件",
"Send Message": "发送",
"Stop Speak": "停止录音"
"Speaking": "我在听,请说...",
"Stop Speak": "停止录音",
"Type a message": "输入问题",
"tts": {
"Stop Speech": "停止"
}
},
"dataset": {
"Choose Dataset": "关联知识库",
@@ -313,10 +324,15 @@
"Course": "说明文档",
"Delete": "删除数据",
"Index": "数据索引({{amount}})"
}
},
"input is empty": "数据内容不能为空 "
},
"deleteDatasetTips": "确认删除该知识库?删除后数据无法恢复,请确认!",
"deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!",
"import csv tip": "请确保CSV为UTF-8格式否则会乱码",
"recall": {
"rerank": "结果重排"
},
"test": {
"noResult": "搜索结果为空"
}
@@ -575,6 +591,7 @@
"wallet": {
"bill": {
"Audio Speech": "语音播报",
"Whisper": "语音输入",
"bill username": "用户"
}
}

View File

@@ -0,0 +1,448 @@
import { useSpeech } from '@/web/common/hooks/useSpeech';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
import React, { useRef, useEffect, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import MyTooltip from '../MyTooltip';
import MyIcon from '../Icon';
import styles from './index.module.scss';
import { useRouter } from 'next/router';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgAndUpload } from '@/web/common/file/controller';
import { useToast } from '@/web/common/hooks/useToast';
import { customAlphabet } from 'nanoid';
import { IMG_BLOCK_KEY } from '@fastgpt/global/core/chat/constants';
import { addDays } from 'date-fns';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
enum FileTypeEnum {
image = 'image',
file = 'file'
}
type FileItemType = {
id: string;
rawFile: File;
type: `${FileTypeEnum}`;
name: string;
icon: string; // img is base64
src?: string;
};
const MessageInput = ({
onChange,
onSendMessage,
onStop,
isChatting,
TextareaDom,
showFileSelector = false,
resetInputVal
}: {
onChange: (e: string) => void;
onSendMessage: (e: string) => void;
onStop: () => void;
isChatting: boolean;
showFileSelector?: boolean;
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
resetInputVal: (val: string) => void;
}) => {
const { shareId } = useRouter().query as { shareId?: string };
const { toast } = useToast();
const {
isSpeaking,
isTransCription,
stopSpeak,
startSpeak,
speakingTimeString,
renderAudioGraph,
stream
} = useSpeech({ shareId });
const { isPc } = useSystemStore();
const canvasRef = useRef<HTMLCanvasElement>(null);
const { t } = useTranslation();
const textareaMinH = '22px';
const [fileList, setFileList] = useState<FileItemType[]>([]);
const havInput = !!TextareaDom.current?.value || fileList.length > 0;
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: 'image/*',
multiple: true,
maxCount: 10
});
const uploadFile = async (file: FileItemType) => {
if (file.type === FileTypeEnum.image) {
try {
const src = await compressImgAndUpload({
file: file.rawFile,
maxW: 1000,
maxH: 1000,
maxSize: 1024 * 1024 * 5,
// 30 day expired.
expiredTime: addDays(new Date(), 30)
});
setFileList((state) =>
state.map((item) =>
item.id === file.id
? {
...item,
src: `${location.origin}${src}`
}
: item
)
);
} catch (error) {
setFileList((state) => state.filter((item) => item.id !== file.id));
toast({
status: 'error',
title: t('common.Upload File Failed')
});
}
}
};
const onSelectFile = useCallback(async (files: File[]) => {
if (!files || files.length === 0) {
return;
}
const loadFiles = await Promise.all(
files.map(
(file) =>
new Promise<FileItemType>((resolve, reject) => {
if (file.type.includes('image')) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const item = {
id: nanoid(),
rawFile: file,
type: FileTypeEnum.image,
name: file.name,
icon: reader.result as string
};
uploadFile(item);
resolve(item);
};
reader.onerror = () => {
reject(reader.error);
};
} else {
resolve({
id: nanoid(),
rawFile: file,
type: FileTypeEnum.file,
name: file.name,
icon: 'pdf'
});
}
})
)
);
setFileList((state) => [...state, ...loadFiles]);
}, []);
const handleSend = useCallback(async () => {
const textareaValue = TextareaDom.current?.value || '';
const images = fileList.filter((item) => item.type === FileTypeEnum.image);
const imagesText =
images.length === 0
? ''
: `\`\`\`${IMG_BLOCK_KEY}
${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
\`\`\`
`;
const inputMessage = `${imagesText}${textareaValue}`;
onSendMessage(inputMessage);
setFileList([]);
}, [TextareaDom, fileList, onSendMessage]);
useEffect(() => {
if (!stream) {
return;
}
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
analyser.fftSize = 4096;
analyser.smoothingTimeConstant = 1;
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
const renderCurve = () => {
if (!canvasRef.current) return;
renderAudioGraph(analyser, canvasRef.current);
window.requestAnimationFrame(renderCurve);
};
renderCurve();
}, [renderAudioGraph, stream]);
return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
pt={fileList.length > 0 ? '10px' : ['14px', '18px']}
pb={['14px', '18px']}
position={'relative'}
boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`}
borderRadius={['none', 'md']}
bg={'white'}
{...(isPc
? {
border: '1px solid',
borderColor: 'rgba(0,0,0,0.12)'
}
: {
borderTop: '1px solid',
borderTopColor: 'rgba(0,0,0,0.15)'
})}
>
{/* translate loading */}
<Flex
position={'absolute'}
top={0}
bottom={0}
left={0}
right={0}
zIndex={10}
pl={5}
alignItems={'center'}
bg={'white'}
color={'myBlue.600'}
visibility={isSpeaking && isTransCription ? 'visible' : 'hidden'}
>
<Spinner size={'sm'} mr={4} />
{t('chat.Converting to text')}
</Flex>
{/* file preview */}
<Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}>
{fileList.map((item) => (
<Box
key={item.id}
border={'1px solid rgba(0,0,0,0.12)'}
mr={2}
mb={2}
rounded={'md'}
position={'relative'}
_hover={{
'.close-icon': { display: item.src ? 'block' : 'none' }
}}
>
{/* uploading */}
{!item.src && (
<Flex
position={'absolute'}
alignItems={'center'}
justifyContent={'center'}
rounded={'md'}
color={'myBlue.600'}
top={0}
left={0}
bottom={0}
right={0}
bg={'rgba(255,255,255,0.8)'}
>
<Spinner />
</Flex>
)}
<MyIcon
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.700'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
position={'absolute'}
bg={'white'}
right={'-8px'}
top={'-8px'}
onClick={() => {
setFileList((state) => state.filter((file) => file.id !== item.id));
}}
className="close-icon"
display={['', 'none']}
/>
{item.type === FileTypeEnum.image && (
<Image
alt={'img'}
src={item.icon}
w={['50px', '70px']}
h={['50px', '70px']}
borderRadius={'md'}
objectFit={'contain'}
/>
)}
</Box>
))}
</Flex>
<Flex alignItems={'flex-end'} mt={fileList.length > 0 ? 1 : 0} pl={[2, 4]}>
{/* file selector */}
{showFileSelector && (
<Flex
h={'22px'}
alignItems={'center'}
justifyContent={'center'}
cursor={'pointer'}
transform={'translateY(1px)'}
onClick={() => {
if (isSpeaking) return;
onOpenSelectFile;
}}
>
<MyTooltip label={t('core.chat.Select File')}>
<MyIcon name={'core/chat/fileSelect'} />
</MyTooltip>
<File onSelect={onSelectFile} />
</Flex>
)}
{/* input area */}
<Textarea
ref={TextareaDom}
py={0}
pl={2}
pr={['30px', '48px']}
border={'none'}
_focusVisible={{
border: 'none'
}}
placeholder={isSpeaking ? t('core.chat.Speaking') : t('core.chat.Type a message')}
resize={'none'}
rows={1}
height={'22px'}
lineHeight={'22px'}
maxHeight={'150px'}
maxLength={-1}
overflowY={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
boxShadow={'none !important'}
color={'myGray.900'}
isDisabled={isSpeaking}
onChange={(e) => {
const textarea = e.target;
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
onChange(textarea.value);
}}
onKeyDown={(e) => {
// enter send.(pc or iframe && enter and unPress shift)
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
handleSend();
e.preventDefault();
}
// 全选内容
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
}}
onPaste={(e) => {
const clipboardData = e.clipboardData;
if (clipboardData) {
const items = clipboardData.items;
const files = Array.from(items)
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
.filter((item) => item) as File[];
onSelectFile(files);
}
}}
/>
<Flex
alignItems={'center'}
position={'absolute'}
right={[2, 4]}
bottom={['10px', '12px']}
>
{/* voice-input */}
{!shareId && !havInput && !isChatting && (
<>
<canvas
ref={canvasRef}
style={{
height: '30px',
width: isSpeaking && !isTransCription ? '100px' : 0,
background: 'white',
zIndex: 0
}}
/>
<Flex
mr={2}
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['26px', '32px']}
w={['26px', '32px']}
borderRadius={'md'}
cursor={'pointer'}
_hover={{ bg: '#F5F5F8' }}
onClick={() => {
if (isSpeaking) {
return stopSpeak();
}
startSpeak(resetInputVal);
}}
>
<MyTooltip label={isSpeaking ? t('core.chat.Stop Speak') : t('core.chat.Record')}>
<MyIcon
name={isSpeaking ? 'core/chat/stopSpeechFill' : 'core/chat/recordFill'}
width={['20px', '22px']}
height={['20px', '22px']}
color={'myBlue.600'}
/>
</MyTooltip>
</Flex>
</>
)}
{/* send and stop icon */}
{isSpeaking ? (
<Box color={'#5A646E'} w={'36px'} textAlign={'right'}>
{speakingTimeString}
</Box>
) : (
<Flex
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['28px', '32px']}
w={['28px', '32px']}
borderRadius={'md'}
bg={isSpeaking || isChatting ? '' : !havInput ? '#E5E5E5' : 'myBlue.600'}
cursor={havInput ? 'pointer' : 'not-allowed'}
lineHeight={1}
onClick={() => {
if (isChatting) {
return onStop();
}
if (havInput) {
return handleSend();
}
}}
>
{isChatting ? (
<MyIcon
className={styles.stopIcon}
width={['22px', '25px']}
height={['22px', '25px']}
cursor={'pointer'}
name={'stop'}
color={'gray.500'}
/>
) : (
<MyTooltip label={t('core.chat.Send Message')}>
<MyIcon
name={'core/chat/sendFill'}
width={['18px', '20px']}
height={['18px', '20px']}
color={'white'}
/>
</MyTooltip>
)}
</Flex>
)}
</Flex>
</Flex>
</Box>
</Box>
);
};
export default React.memo(MessageInput);

View File

@@ -22,11 +22,11 @@ import {
Card,
Flex,
Input,
Textarea,
Button,
useTheme,
BoxProps,
FlexProps
FlexProps,
Image
} from '@chakra-ui/react';
import { feConfigs } from '@/web/common/system/staticData';
import { eventBus } from '@/web/common/utils/eventbus';
@@ -62,7 +62,7 @@ import styles from './index.module.scss';
import { postQuestionGuide } from '@/web/core/ai/api';
import { splitGuideModule } from '@/global/core/app/modules/utils';
import { AppTTSConfigType } from '@/types/app';
import { useSpeech } from '@/web/common/hooks/useSpeech';
import MessageInput from './MessageInput';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
@@ -100,6 +100,7 @@ type Props = {
appAvatar?: string;
userAvatar?: string;
userGuideModule?: ModuleItemType;
showFileSelector?: boolean;
active?: boolean; // can use
onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat?: (e: StartChatFnProps) => Promise<{
@@ -119,6 +120,7 @@ const ChatBox = (
appAvatar,
userAvatar,
userGuideModule,
showFileSelector,
active = true,
onUpdateVariable,
onStartChat,
@@ -150,8 +152,6 @@ const ChatBox = (
const [adminMarkData, setAdminMarkData] = useState<AdminMarkType & { chatItemId: string }>();
const [questionGuides, setQuestionGuide] = useState<string[]>([]);
const { isSpeaking, startSpeak, stopSpeak } = useSpeech();
const isChatting = useMemo(
() =>
chatHistory[chatHistory.length - 1] &&
@@ -241,6 +241,7 @@ const ChatBox = (
TextareaDom.current.style.height =
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
}
setRefresh((state) => !state);
}, 100);
}, []);
@@ -633,7 +634,7 @@ const ChatBox = (
borderRadius={'8px 0 8px 8px'}
textAlign={'left'}
>
<Box as={'p'}>{item.value}</Box>
<Markdown source={item.value} isChatting={false} />
</Card>
</Box>
</>
@@ -795,110 +796,19 @@ const ChatBox = (
</Box>
{/* message input */}
{onStartChat && variableIsFinish && active ? (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
py={'18px'}
position={'relative'}
boxShadow={`0 0 10px rgba(0,0,0,0.2)`}
{...(isPc
? {
border: '1px solid',
borderColor: 'rgba(0,0,0,0.12)'
}
: {
borderTop: '1px solid',
borderTopColor: 'rgba(0,0,0,0.15)'
})}
borderRadius={['none', 'md']}
backgroundColor={'white'}
>
{/* 输入框 */}
<Textarea
ref={TextareaDom}
py={0}
pr={['45px', '55px']}
border={'none'}
_focusVisible={{
border: 'none'
}}
placeholder="提问"
resize={'none'}
rows={1}
height={'22px'}
lineHeight={'22px'}
maxHeight={'150px'}
maxLength={-1}
overflowY={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
boxShadow={'none !important'}
color={'myGray.900'}
onChange={(e) => {
const textarea = e.target;
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
setRefresh((state) => !state);
}}
onKeyDown={(e) => {
// enter send.(pc or iframe && enter and unPress shift)
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
handleSubmit((data) => sendPrompt(data, TextareaDom.current?.value))();
e.preventDefault();
}
// 全选内容
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
}}
/>
{/* 发送和等待按键 */}
<Flex
alignItems={'center'}
justifyContent={'center'}
h={['26px', '32px']}
w={['26px', '32px']}
position={'absolute'}
right={['12px', '14px']}
bottom={['15px', '13px']}
borderRadius={'md'}
// bg={TextareaDom.current?.value ? 'myBlue.600' : ''}
cursor={'pointer'}
lineHeight={1}
onClick={() => {
if (isChatting) {
return chatController.current?.abort('stop');
}
if (TextareaDom.current?.value) {
return handleSubmit((data) => sendPrompt(data, TextareaDom.current?.value))();
}
// speech
// if (isSpeaking) {
// return stopSpeak();
// }
// startSpeak();
}}
>
{isChatting ? (
<MyIcon
className={styles.stopIcon}
width={['22px', '25px']}
height={['22px', '25px']}
cursor={'pointer'}
name={'stop'}
color={'gray.500'}
/>
) : (
<MyTooltip label={t('core.chat.Send Message')}>
<MyIcon
name={'core/chat/sendFill'}
width={['16px', '22px']}
height={['16px', '22px']}
color={TextareaDom.current?.value ? 'myBlue.600' : 'myGray.400'}
/>
</MyTooltip>
)}
</Flex>
</Box>
</Box>
<MessageInput
onChange={(e) => {
setRefresh(!refresh);
}}
onSendMessage={(e) => {
handleSubmit((data) => sendPrompt(data, e))();
}}
onStop={() => chatController.current?.abort('stop')}
isChatting={isChatting}
TextareaDom={TextareaDom}
resetInputVal={resetInputVal}
showFileSelector={showFileSelector}
/>
) : null}
{/* user feedback modal */}
@@ -1206,33 +1116,38 @@ function ChatController({
<MyIcon {...controlIconStyle} name={'loading'} />
</MyTooltip>
) : audioPlaying ? (
<MyTooltip label={'终止播放'}>
<MyIcon
{...controlIconStyle}
name={'pause'}
_hover={{ color: '#E74694' }}
onClick={() => cancelAudio()}
/>
</MyTooltip>
<Flex alignItems={'center'} mr={2}>
<MyTooltip label={t('core.chat.tts.Stop Speech')}>
<MyIcon
{...controlIconStyle}
mr={1}
name={'core/chat/stopSpeech'}
color={'#E74694'}
onClick={() => cancelAudio()}
/>
</MyTooltip>
<Image src="/icon/speaking.gif" w={'23px'} alt={''} />
</Flex>
) : (
<MyTooltip label={'语音播报'}>
<MyTooltip label={t('core.app.TTS')}>
<MyIcon
{...controlIconStyle}
name={'voice'}
_hover={{ color: '#E74694' }}
onClick={async () => {
const buffer = await playAudio({
const response = await playAudio({
buffer: chat.ttsBuffer,
chatItemId: chat.dataId,
text: chat.value
});
if (!setChatHistory) return;
if (!setChatHistory || !response.buffer) return;
setChatHistory((state) =>
state.map((item) =>
item.dataId === chat.dataId
? {
...item,
ttsBuffer: buffer
ttsBuffer: response.buffer
}
: item
)

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00004 2.66665C6.58555 2.66665 5.229 3.22855 4.2288 4.22874C3.22861 5.22894 2.66671 6.58549 2.66671 7.99998V8.66665H4.00004C4.53047 8.66665 5.03918 8.87736 5.41425 9.25243C5.78933 9.62751 6.00004 10.1362 6.00004 10.6666V12.6666C6.00004 13.1971 5.78933 13.7058 5.41425 14.0809C5.03918 14.4559 4.53047 14.6666 4.00004 14.6666H3.33337C2.80294 14.6666 2.29423 14.4559 1.91916 14.0809C1.54409 13.7058 1.33337 13.1971 1.33337 12.6666V7.99998C1.33337 6.23187 2.03575 4.53618 3.286 3.28593C4.53624 2.03569 6.23193 1.33331 8.00004 1.33331C9.76815 1.33331 11.4638 2.03569 12.7141 3.28593C13.9643 4.53618 14.6667 6.23187 14.6667 7.99998V12.6666C14.6667 13.1971 14.456 13.7058 14.0809 14.0809C13.7058 14.4559 13.1971 14.6666 12.6667 14.6666H12C11.4696 14.6666 10.9609 14.4559 10.5858 14.0809C10.2108 13.7058 10 13.1971 10 12.6666V10.6666C10 10.1362 10.2108 9.62751 10.5858 9.25243C10.9609 8.87736 11.4696 8.66665 12 8.66665H13.3334V7.99998C13.3334 6.58549 12.7715 5.22894 11.7713 4.22874C10.7711 3.22855 9.41453 2.66665 8.00004 2.66665ZM13.3334 9.99998H12C11.8232 9.99998 11.6537 10.0702 11.5286 10.1952C11.4036 10.3203 11.3334 10.4898 11.3334 10.6666V12.6666C11.3334 12.8435 11.4036 13.013 11.5286 13.138C11.6537 13.2631 11.8232 13.3333 12 13.3333H12.6667C12.8435 13.3333 13.0131 13.2631 13.1381 13.138C13.2631 13.013 13.3334 12.8435 13.3334 12.6666V9.99998ZM2.66671 12.6666C2.66671 12.8435 2.73695 13.013 2.86197 13.138C2.98699 13.2631 3.15656 13.3333 3.33337 13.3333H4.00004C4.17685 13.3333 4.34642 13.2631 4.47144 13.138C4.59647 13.013 4.66671 12.8435 4.66671 12.6666V10.6666C4.66671 10.4898 4.59647 10.3203 4.47144 10.1952C4.34642 10.0702 4.17685 9.99998 4.00004 9.99998H2.66671V12.6666Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 22 18" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.63694 1.22278C5.02752 1.61324 5.02762 2.24641 4.63715 2.63699C2.94991 4.32474 2.00208 6.61352 2.00208 8.99999C2.00208 11.3865 2.94991 13.6752 4.63715 15.363C5.02762 15.7536 5.02752 16.3867 4.63694 16.7772C4.24636 17.1677 3.61319 17.1676 3.22273 16.777C1.16054 14.7142 0.0020752 11.9168 0.0020752 8.99999C0.0020752 6.08319 1.16054 3.2858 3.22273 1.22299C3.61319 0.832409 4.24636 0.832314 4.63694 1.22278ZM17.3629 1.22278C17.7535 0.832314 18.3867 0.832409 18.7772 1.22299C20.8393 3.2858 21.9978 6.08319 21.9978 8.99999C21.9978 11.9168 20.8393 14.7142 18.7772 16.777C18.3867 17.1676 17.7535 17.1677 17.3629 16.7772C16.9724 16.3867 16.9723 15.7536 17.3627 15.363C19.05 13.6752 19.9978 11.3865 19.9978 8.99999C19.9978 6.61352 19.05 4.32474 17.3627 2.63699C16.9723 2.24641 16.9724 1.61324 17.3629 1.22278ZM7.46744 4.04328C7.85775 4.43402 7.8574 5.06719 7.46665 5.45749C7.00177 5.92186 6.63298 6.4733 6.38135 7.08029C6.12973 7.68728 6.00022 8.33792 6.00022 8.99499C6.00022 9.65207 6.12973 10.3027 6.38135 10.9097C6.63298 11.5167 7.00177 12.0681 7.46665 12.5325C7.8574 12.9228 7.85775 13.556 7.46744 13.9467C7.07713 14.3374 6.44397 14.3378 6.05323 13.9475C5.40239 13.2974 4.88608 12.5254 4.53381 11.6756C4.18154 10.8258 4.00022 9.9149 4.00022 8.99499C4.00022 8.07508 4.18154 7.1642 4.53381 6.31441C4.88608 5.46462 5.40239 4.6926 6.05323 4.04249C6.44397 3.65219 7.07713 3.65254 7.46744 4.04328ZM14.5324 4.05328C14.9227 3.66254 15.5559 3.66219 15.9467 4.05249C16.5975 4.7026 17.1138 5.47462 17.4661 6.32441C17.8183 7.1742 17.9997 8.08509 17.9997 9.00499C17.9997 9.9249 17.8183 10.8358 17.4661 11.6856C17.1138 12.5354 16.5975 13.3074 15.9467 13.9575C15.5559 14.3478 14.9227 14.3474 14.5324 13.9567C14.1421 13.566 14.1425 12.9328 14.5332 12.5425C14.9981 12.0781 15.3669 11.5267 15.6185 10.9197C15.8701 10.3127 15.9997 9.66207 15.9997 9.00499C15.9997 8.34792 15.8701 7.69728 15.6185 7.09029C15.3669 6.4833 14.9981 5.93186 14.5332 5.46749C14.1425 5.07719 14.1421 4.44402 14.5324 4.05328ZM10.9999 7.99999C10.4477 7.99999 9.99994 8.44771 9.99994 8.99999C9.99994 9.55228 10.4477 9.99999 10.9999 9.99999C11.5522 9.99999 11.9999 9.55228 11.9999 8.99999C11.9999 8.44771 11.5522 7.99999 10.9999 7.99999ZM7.99994 8.99999C7.99994 7.34314 9.34309 5.99999 10.9999 5.99999C12.6568 5.99999 13.9999 7.34314 13.9999 8.99999C13.9999 10.6568 12.6568 12 10.9999 12C9.34309 12 7.99994 10.6568 7.99994 8.99999Z" fill="#3370FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="18" viewBox="0 0 19 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.15 1.98982C12.4865 1.98982 11.8501 2.2534 11.3809 2.72259L3.72259 10.3809C2.94066 11.1628 2.50138 12.2234 2.50138 13.3292C2.50138 14.435 2.94066 15.4955 3.72259 16.2774C4.50451 17.0593 5.56502 17.4986 6.67083 17.4986C7.77664 17.4986 8.83715 17.0593 9.61907 16.2774L17.2774 8.61908C17.6028 8.29364 18.1305 8.29364 18.4559 8.61908C18.7814 8.94452 18.7814 9.47216 18.4559 9.79759L10.7976 17.4559C9.7031 18.5504 8.21866 19.1653 6.67083 19.1653C5.123 19.1653 3.63856 18.5504 2.54407 17.4559C1.44959 16.3614 0.834717 14.877 0.834717 13.3292C0.834717 11.7813 1.44959 10.2969 2.54407 9.20242L10.2024 1.54408C10.9842 0.762333 12.0444 0.323151 13.15 0.323151C14.2556 0.323151 15.3158 0.762332 16.0976 1.54408C16.8793 2.32583 17.3185 3.38611 17.3185 4.49167C17.3185 5.59723 16.8793 6.65751 16.0976 7.43926L8.43092 15.0976C7.96191 15.5666 7.32579 15.8301 6.6625 15.8301C5.99921 15.8301 5.36309 15.5666 4.89407 15.0976C4.42506 14.6286 4.16157 13.9925 4.16157 13.3292C4.16157 12.6659 4.42506 12.0298 4.89407 11.5607L11.9694 4.49373C12.2951 4.16849 12.8227 4.1688 13.1479 4.49443C13.4732 4.82006 13.4729 5.34769 13.1472 5.67294L6.07259 12.7393C5.91635 12.8957 5.82824 13.1081 5.82824 13.3292C5.82824 13.5504 5.91613 13.7626 6.07259 13.9191C6.22904 14.0755 6.44124 14.1634 6.6625 14.1634C6.88376 14.1634 7.09595 14.0755 7.25241 13.9191L14.9191 6.26075C15.3881 5.79159 15.6519 5.15505 15.6519 4.49167C15.6519 3.82814 15.3883 3.19178 14.9191 2.72259C14.4499 2.2534 13.8135 1.98982 13.15 1.98982Z" fill="#485058"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +1,3 @@
<?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="1699507042803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2849" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 628.50844445L512 628.50844445c106.79940741 0 194.18074075-87.38133333 194.18074075-194.18074075L706.18074075 201.31081482c0-106.79940741-87.38133333-194.18074075-194.18074075-194.18074074l0 0c-106.79940741 0-194.18074075 87.38133333-194.18074075 194.18074074l0 233.01688888C317.81925925 541.12711111 405.20059259 628.50844445 512 628.50844445z" p-id="2850"></path><path d="M857.39899259 488.21285925c3.2768-21.23851852-11.16539259-41.02068148-32.40391111-44.29748147-21.23851852-3.15543703-41.02068148 11.28675555-44.29748148 32.40391111C760.30862222 607.39128889 644.89244445 706.18074075 512 706.18074075c-132.89244445 0-248.42998518-98.91081482-268.6976-229.98281483-3.2768-21.23851852-23.18032592-35.68071111-44.29748148-32.4039111-21.23851852 3.2768-35.68071111 23.05896297-32.40391111 44.29748148 24.51531852 158.37866667 150.49007408 276.46482963 306.56284444 293.45564445L473.16385185 900.36148148l-116.50844444 0c-21.48124445 0-38.83614815 17.3549037-38.83614816 38.83614815s17.3549037 38.83614815 38.83614816 38.83614815l310.68918518 0c21.48124445 0 38.83614815-17.3549037 38.83614816-38.83614815s-17.3549037-38.83614815-38.83614816-38.83614815l-116.50844444 0 0-118.81434073C706.78755555 764.55632592 832.88367408 646.59152592 857.39899259 488.21285925z" p-id="2851"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.64302 0.976311C8.26814 0.351189 9.11599 0 10 0C10.8841 0 11.7319 0.351189 12.3571 0.976311C12.9822 1.60143 13.3334 2.44928 13.3334 3.33333V10C13.3334 10.8841 12.9822 11.7319 12.3571 12.357C11.7319 12.9821 10.8841 13.3333 10 13.3333C9.11599 13.3333 8.26814 12.9821 7.64302 12.357C7.0179 11.7319 6.66671 10.8841 6.66671 10V3.33333C6.66671 2.44928 7.0179 1.60143 7.64302 0.976311ZM10 1.66667C9.55801 1.66667 9.13409 1.84226 8.82153 2.15482C8.50897 2.46738 8.33337 2.89131 8.33337 3.33333V10C8.33337 10.442 8.50897 10.866 8.82153 11.1785C9.13409 11.4911 9.55801 11.6667 10 11.6667C10.4421 11.6667 10.866 11.4911 11.1786 11.1785C11.4911 10.866 11.6667 10.442 11.6667 10V3.33333C11.6667 2.89131 11.4911 2.46738 11.1786 2.15482C10.866 1.84226 10.4421 1.66667 10 1.66667ZM4.16671 7.5C4.62694 7.5 5.00004 7.8731 5.00004 8.33333V10C5.00004 11.3261 5.52682 12.5979 6.46451 13.5355C7.40219 14.4732 8.67396 15 10 15C11.3261 15 12.5979 14.4732 13.5356 13.5355C14.4733 12.5979 15 11.3261 15 10V8.33333C15 7.8731 15.3731 7.5 15.8334 7.5C16.2936 7.5 16.6667 7.8731 16.6667 8.33333V10C16.6667 11.7681 15.9643 13.4638 14.7141 14.714C13.6619 15.7662 12.2942 16.4304 10.8334 16.6144V18.3333H13.3334C13.7936 18.3333 14.1667 18.7064 14.1667 19.1667C14.1667 19.6269 13.7936 20 13.3334 20H6.66671C6.20647 20 5.83337 19.6269 5.83337 19.1667C5.83337 18.7064 6.20647 18.3333 6.66671 18.3333H9.16671V16.6144C7.70587 16.4304 6.33818 15.7662 5.286 14.714C4.03575 13.4638 3.33337 11.7681 3.33337 10V8.33333C3.33337 7.8731 3.70647 7.5 4.16671 7.5Z" fill="#485058"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="32" height="32" fill="url(#pattern0)" />
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_18_1411" transform="scale(0.00666667)" />
</pattern>
<image id="image0_18_1411" width="150" height="150"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAABV1JREFUeF7tnEFy3DYQRTV738Y5gG/kHMW5UQ6Q3Mb7caFSqEAsggQo/j+Q/9NOLqob/fpNgyAlP974goCAwEMQk5AQeEMsJJAQQCwJVoIiFg5ICCCWBCtBEQsHJAQQS4KVoIiFAxICiCXBSlDEwgEJAcSSYCUoYuGAhABiSbASFLFwQEIAsSRYCYpYOCAhgFgSrARFLByQEEAsCVaCIhYOSAgglgQrQRELByQEEEuClaCIhQMSAoglwUpQxMIBCQHEkmAlKGLhgIQAYkmwEhSxcEBCALEkWAmKWDggIYBYEqwERSwckBBALAlWgiIWDkgIIJYEK0ERCwckBBBLgpWgiIUDEgKIJcFKUMTCAQkBxJJgJShi4YCEAGJJsBIUsXBAQgCxJFgJilg4ICGAWBKsBEUsHJAQQCwJVoIiFg5ICCCWBCtBEQsHJAQQS4KVoIiFAxICiCXBSlDEwgEJAcSSYCUoYuGAhMBSYn378/n8+fy/zn/+eiy1PkkHLgRtOa3KaJnGFViFcSvWl8fb298/kKu698f357Mw2TIq368m2BJi7UlVYBaI5Qu5/uOwnehVuBU5vVysnlTtDrHap/HC7vXhH+lJtSqnJcRqR/u2Ayt+Gj9syYUAZRtsf6x80zZvtduGl4u1BVbgbaGVf0ufWpXTO7sauRBr82ltxepBmxWrbq/tYeCqmNtYr4rzdTOxWoxlOiDWjlhbobbQZsTqnS5nDwH1BNbKeXVb3q6pxplZE2JN3j+UBt4l1tFBYFaKu05gZ4eT0QmIWDeLVcKVUT/SgLOT0+h2cRZndoKeHU5GHqcg1ovFareuvYPAiKC9A0WVfFTQ+uzpjjUh1oJiVSFGJ83e0b6WVW+UR+6P9rbBuu23x/ER2RFrUbFmttTe0b4VdETS7ZbaO/Ui1qQ0I5ef3bzPCHE0Iepa/h14sX20pplJc/QopZ2AiDViyuQ1CrHae5rtlPioWDPb6tHku0usKvrMfd9kiy5dvsST96PHDatNLMQa8+y3FEs9sUZlv3Ma927emVgd0e+E33sY2U7EO7ZCxDqfWkysHUZ3yX5XnLJEJta5zO+uuBP+3stnxc07E+u8yb/1xNo7FNy1FY7EKfjbZ1l76xl9XcXEOpfZNrFUYo3KsBWrFt6uazQWYi0gVvvS90oTjybN7O8+nT19H518iPVCsaoQvccNo00sP997Xzg6YVoMPblmYiHWi8XqyTUjVS1hT4grcXpb4sirnLoWxFpArMklfIrLEWuyTXc+bphM/akuR6zJdh39nlENdXX7mVzK0pf3PoC80jlo29HJaeYGd2kzbljc3jMxxDoBuycXUr2HdvRHqzMHgRscPw3x8ifvR8fy1WCd0jRd8Bn+V56lxDL1hTQGAohlgJyYArESu26oGbEMkBNTIFZi1w01I5YBcmIKxErsuqFmxDJATkyBWIldN9SMWAbIiSkQK7HrhpoRywA5MQViJXbdUDNiGSAnpkCsxK4bakYsA+TEFIiV2HVDzYhlgJyYArESu26oGbEMkBNTIFZi1w01I5YBcmIKxErsuqFmxDJATkyBWIldN9SMWAbIiSkQK7HrhpoRywA5MQViJXbdUDNiGSAnpkCsxK4bakYsA+TEFIiV2HVDzYhlgJyYArESu26oGbEMkBNTIFZi1w01I5YBcmIKxErsuqFmxDJATkyBWIldN9SMWAbIiSkQK7HrhpoRywA5MQViJXbdUDNiGSAnpkCsxK4bakYsA+TEFIiV2HVDzYhlgJyYArESu26oGbEMkBNTIFZi1w01I5YBcmIKxErsuqFmxDJATkyBWIldN9SMWAbIiSl+AcEzjbVWPFoJAAAAAElFTkSuQmCC" />
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.99996 1.99999C4.68625 1.99999 1.99996 4.68628 1.99996 7.99999C1.99996 11.3137 4.68625 14 7.99996 14C11.3137 14 14 11.3137 14 7.99999C14 4.68628 11.3137 1.99999 7.99996 1.99999ZM0.666626 7.99999C0.666626 3.9499 3.94987 0.666656 7.99996 0.666656C12.05 0.666656 15.3333 3.9499 15.3333 7.99999C15.3333 12.0501 12.05 15.3333 7.99996 15.3333C3.94987 15.3333 0.666626 12.0501 0.666626 7.99999ZM5.33329 5.99999C5.33329 5.6318 5.63177 5.33332 5.99996 5.33332H9.99996C10.3682 5.33332 10.6666 5.6318 10.6666 5.99999V9.99999C10.6666 10.3682 10.3682 10.6667 9.99996 10.6667H5.99996C5.63177 10.6667 5.33329 10.3682 5.33329 9.99999V5.99999ZM6.66663 6.66666V9.33332H9.33329V6.66666H6.66663Z" />
</svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@@ -1,8 +1,10 @@
<?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="1699507299637"
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3033"
xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
<path
d="M512 0a512 512 0 0 1 512 512c0 282.769067-229.230933 512-512 512S0 794.769067 0 512 229.230933 0 512 0zM388.022613 314.88C347.62752 314.88 314.88 347.62752 314.88 388.022613v247.954774C314.88 676.37248 347.62752 709.12 388.022613 709.12h247.954774C676.37248 709.12 709.12 676.37248 709.12 635.977387V388.022613C709.12 347.62752 676.37248 314.88 635.977387 314.88H388.022613z"
p-id="3034"></path>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<g clip-path="url(#clip0_74_2)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.49999C5.85791 2.49999 2.50004 5.85786 2.50004 10C2.50004 14.1421 5.85791 17.5 10 17.5C14.1422 17.5 17.5 14.1421 17.5 10C17.5 5.85786 14.1422 2.49999 10 2.49999ZM0.833374 10C0.833374 4.93739 4.93743 0.833328 10 0.833328C15.0627 0.833328 19.1667 4.93739 19.1667 10C19.1667 15.0626 15.0627 19.1667 10 19.1667C4.93743 19.1667 0.833374 15.0626 0.833374 10ZM6.66671 7.5C6.66671 7.03976 7.0398 6.66666 7.50004 6.66666H12.5C12.9603 6.66666 13.3334 7.03976 13.3334 7.5V12.5C13.3334 12.9602 12.9603 13.3333 12.5 13.3333H7.50004C7.0398 13.3333 6.66671 12.9602 6.66671 12.5V7.5ZM8.33337 8.33333V11.6667H11.6667V8.33333H8.33337Z" fill="#3370FF"/>
</g>
<defs>
<clipPath id="clip0_74_2">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 953 B

View File

@@ -105,10 +105,15 @@ const iconPaths = {
'support/permission/privateLight': () => import('./icons/support/permission/privateLight.svg'),
'support/permission/publicLight': () => import('./icons/support/permission/publicLight.svg'),
'core/app/ttsFill': () => import('./icons/core/app/ttsFill.svg'),
'core/app/tts': () => import('./icons/core/app/tts.svg'),
'core/app/headphones': () => import('./icons/core/app/headphones.svg'),
'common/playLight': () => import('./icons/common/playLight.svg'),
'core/chat/sendFill': () => import('./icons/core/chat/sendFill.svg'),
'core/chat/recordFill': () => import('./icons/core/chat/recordFill.svg'),
'core/chat/stopSpeechFill': () => import('./icons/core/chat/stopSpeechFill.svg')
'core/chat/stopSpeechFill': () => import('./icons/core/chat/stopSpeechFill.svg'),
'core/chat/stopSpeech': () => import('./icons/core/chat/stopSpeech.svg'),
'core/chat/speaking': () => import('./icons/core/chat/speaking.svg'),
'core/chat/fileSelect': () => import('./icons/core/chat/fileSelect.svg')
};
export type IconName = keyof typeof iconPaths;

View File

@@ -0,0 +1,31 @@
import { Box, Flex } from '@chakra-ui/react';
import MdImage from '../img/Image';
import { useMemo } from 'react';
const ImageBlock = ({ images }: { images: string }) => {
const formatData = useMemo(
() =>
images.split('\n').map((item) => {
try {
return JSON.parse(item) as { src: string };
} catch (error) {
return { src: '' };
}
}),
[images]
);
return (
<Flex alignItems={'center'} wrap={'wrap'} gap={4}>
{formatData.map(({ src }) => {
return (
<Box key={src} rounded={'md'} flex={'0 0 auto'} w={'120px'}>
<MdImage src={src} />
</Box>
);
})}
</Flex>
);
};
export default ImageBlock;

View File

@@ -1,9 +1,18 @@
import React, { useState } from 'react';
import { Image, Skeleton } from '@chakra-ui/react';
import {
Image,
Modal,
ModalCloseButton,
ModalContent,
ModalOverlay,
Skeleton,
useDisclosure
} from '@chakra-ui/react';
const MdImage = ({ src }: { src?: string }) => {
const [isLoading, setIsLoading] = useState(true);
const [succeed, setSucceed] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<Skeleton
minH="100px"
@@ -18,6 +27,7 @@ const MdImage = ({ src }: { src?: string }) => {
borderRadius={'md'}
src={src}
alt={''}
maxH={'150px'}
fallbackSrc={'/imgs/errImg.png'}
fallbackStrategy={'onError'}
cursor={succeed ? 'pointer' : 'default'}
@@ -30,9 +40,23 @@ const MdImage = ({ src }: { src?: string }) => {
onError={() => setIsLoading(false)}
onClick={() => {
if (!succeed) return;
window.open(src, '_blank');
onOpen();
}}
/>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent m={'auto'}>
<Image
src={src}
alt={''}
fallbackSrc={'/imgs/errImg.png'}
fallbackStrategy={'onError'}
loading="eager"
objectFit={'contain'}
/>
</ModalContent>
<ModalCloseButton bg={'myWhite.500'} zIndex={999999} />
</Modal>
</Skeleton>
);
};

View File

@@ -16,12 +16,14 @@ const MdImage = dynamic(() => import('./img/Image'));
const ChatGuide = dynamic(() => import('./chat/Guide'));
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'));
const QuoteBlock = dynamic(() => import('./chat/Quote'));
const ImageBlock = dynamic(() => import('./chat/Image'));
export enum CodeClassName {
guide = 'guide',
mermaid = 'mermaid',
echarts = 'echarts',
quote = 'quote'
quote = 'quote',
img = 'img'
}
function Code({ inline, className, children }: any) {
@@ -41,6 +43,9 @@ function Code({ inline, className, children }: any) {
if (codeType === CodeClassName.quote) {
return <QuoteBlock code={String(children)} />;
}
if (codeType === CodeClassName.img) {
return <ImageBlock images={String(children)} />;
}
return (
<CodeLight className={className} inline={inline} match={match}>
{children}

View File

@@ -42,7 +42,23 @@ const MyModal = ({
maxH={'90vh'}
{...props}
>
{!!title && <ModalHeader>{title}</ModalHeader>}
{!title && onClose && <ModalCloseButton zIndex={1} />}
{!!title && (
<ModalHeader
display={'flex'}
alignItems={'center'}
fontWeight={500}
background={'#FBFBFC'}
borderBottom={'1px solid #F4F6F8'}
roundedTop={'lg'}
py={3}
>
{title}
<Box flex={1} />
{onClose && <ModalCloseButton position={'relative'} top={0} right={0} />}
</ModalHeader>
)}
<Box
overflow={props.overflow || 'overlay'}
h={'100%'}
@@ -51,7 +67,6 @@ const MyModal = ({
>
{children}
</Box>
{onClose && <ModalCloseButton />}
</ModalContent>
</Modal>
);

View File

@@ -6,7 +6,8 @@ import {
MenuItem,
Button,
useDisclosure,
useOutsideClick
useOutsideClick,
MenuButton
} from '@chakra-ui/react';
import type { ButtonProps } from '@chakra-ui/react';
import { ChevronDownIcon } from '@chakra-ui/icons';
@@ -47,80 +48,81 @@ const MySelect = (
});
return (
<Menu autoSelect={false} isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
<Box
<Menu
autoSelect={false}
isOpen={isOpen}
onOpen={onOpen}
onClose={onClose}
strategy={'fixed'}
matchWidth
>
{/* <Box
ref={SelectRef}
position={'relative'}
onClick={() => {
isOpen ? onClose() : onOpen();
}}
>
<Button
ref={ref}
width={width}
px={3}
variant={'base'}
display={'flex'}
alignItems={'center'}
justifyContent={'space-between'}
_active={{
transform: ''
}}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600'
}
: {})}
{...props}
>
{selectItem?.alias || selectItem?.label || placeholder}
<Box flex={1} />
<ChevronDownIcon />
</Button>
<MenuList
minW={(() => {
const w = ref.current?.clientWidth;
if (w) {
return `${w}px !important`;
> */}
<MenuButton
as={Button}
ref={ref}
width={width}
px={3}
rightIcon={<ChevronDownIcon />}
variant={'base'}
textAlign={'left'}
_active={{
transform: 'none'
}}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600'
}
return Array.isArray(width)
? width.map((item) => `${item} !important`)
: `${width} !important`;
})()}
p={'6px'}
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
: {})}
{...props}
>
{selectItem?.alias || selectItem?.label || placeholder}
</MenuButton>
<MenuList
minW={(() => {
const w = ref.current?.clientWidth;
if (w) {
return `${w}px !important`;
}
zIndex={99}
transform={'translateY(35px) !important'}
maxH={'40vh'}
overflowY={'auto'}
>
{list.map((item) => (
<MenuItem
key={item.value}
{...menuItemStyles}
{...(value === item.value
? {
color: 'myBlue.600',
bg: 'myWhite.300'
}
: {})}
onClick={() => {
if (onchange && value !== item.value) {
onchange(item.value);
return Array.isArray(width)
? width.map((item) => `${item} !important`)
: `${width} !important`;
})()}
p={'6px'}
border={'1px solid #fff'}
boxShadow={'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'}
zIndex={99}
maxH={'40vh'}
overflowY={'auto'}
>
{list.map((item) => (
<MenuItem
key={item.value}
{...menuItemStyles}
{...(value === item.value
? {
color: 'myBlue.600',
bg: 'myWhite.300'
}
}}
whiteSpace={'pre-wrap'}
>
{item.label}
</MenuItem>
))}
</MenuList>
</Box>
: {})}
onClick={() => {
if (onchange && value !== item.value) {
onchange(item.value);
}
}}
whiteSpace={'pre-wrap'}
>
{item.label}
</MenuItem>
))}
</MenuList>
</Menu>
);
};

View File

@@ -9,7 +9,8 @@ import {
useTheme,
Textarea,
Grid,
Divider
Divider,
Switch
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import { useForm } from 'react-hook-form';
@@ -30,6 +31,7 @@ export type KbParamsType = {
searchSimilarity: number;
searchLimit: number;
searchEmptyText: string;
rerank: boolean;
};
export const DatasetSelectModal = ({
@@ -225,10 +227,11 @@ export const DatasetSelectModal = ({
);
};
export const KbParamsModal = ({
export const DatasetParamsModal = ({
searchEmptyText,
searchLimit,
searchSimilarity,
rerank,
onClose,
onChange
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
@@ -237,7 +240,8 @@ export const KbParamsModal = ({
defaultValues: {
searchEmptyText,
searchLimit,
searchSimilarity
searchSimilarity,
rerank
}
});
@@ -245,6 +249,24 @@ export const KbParamsModal = ({
<MyModal isOpen={true} onClose={onClose} title={'搜索参数调整'} minW={['90vw', '600px']}>
<Flex flexDirection={'column'}>
<ModalBody>
{feConfigs?.isPlus && (
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
<Box flex={'0 0 100px'} mb={[8, 0]}>
<MyTooltip label={'将召回的结果进行进一步重排,可增加召回率'} forceShow>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Box>
<Switch
size={'lg'}
isChecked={getValues('rerank')}
onChange={(e) => {
setValue('rerank', e.target.checked);
setRefresh(!refresh);
}}
/>
</Box>
)}
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
<Box flex={'0 0 100px'} mb={[8, 0]}>

View File

@@ -16,6 +16,7 @@ import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { getGuideModule } from '@/global/core/app/modules/utils';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
export type ChatTestComponentRef = {
resetChatTest: () => void;
@@ -115,6 +116,7 @@ const ChatTest = (
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(modules)}
showFileSelector={checkChatSupportSelectFileByModules(modules)}
onStartChat={startChat}
onDelMessage={() => {}}
/>

View File

@@ -14,7 +14,8 @@ import {
useDisclosure,
Button,
useTheme,
Grid
Grid,
Switch
} from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
@@ -35,6 +36,7 @@ import type { SelectedDatasetType } from '@fastgpt/global/core/module/api.d';
import { useQuery } from '@tanstack/react-query';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import type { EditFieldModeType, EditFieldType } from '../modules/FieldEditModal';
import { feConfigs } from '@/web/common/system/staticData';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
const SelectAppModal = dynamic(() => import('../../SelectAppModal'));
@@ -163,7 +165,10 @@ const RenderInput = ({
editFiledType?: EditFieldModeType;
}) => {
const sortInputs = useMemo(
() => flowInputList.sort((a, b) => (a.key === FlowNodeInputTypeEnum.switch ? -1 : 1)),
() =>
flowInputList
.filter((item) => !item.plusField || feConfigs.isPlus)
.sort((a, b) => (a.key === FlowNodeInputTypeEnum.switch ? -1 : 1)),
[flowInputList]
);
return (
@@ -187,6 +192,9 @@ const RenderInput = ({
{item.type === FlowNodeInputTypeEnum.input && (
<TextInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.switch && (
<SwitchRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.textarea && (
<TextareaRender item={item} moduleId={moduleId} />
)}
@@ -277,6 +285,26 @@ var TextInputRender = React.memo(function TextInputRender({ item, moduleId }: Re
);
});
var SwitchRender = React.memo(function SwitchRender({ item, moduleId }: RenderProps) {
return (
<Switch
size={'lg'}
isChecked={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.checked
}
});
}}
/>
);
});
var TextareaRender = React.memo(function TextareaRender({ item, moduleId }: RenderProps) {
return (
<Textarea

View File

@@ -52,19 +52,19 @@ const UpdateInviteModal = () => {
return (
<MyModal
isOpen={inviteList.length > 0}
isOpen={inviteList && inviteList.length > 0}
title={
<>
<Box>{t('user.team.Processing invitations')}</Box>
<Box fontWeight={'normal'} fontSize={'sm'} color={'myGray.500'}>
{t('user.team.Processing invitations Tips', { amount: inviteList.length })}
{t('user.team.Processing invitations Tips', { amount: inviteList?.length })}
</Box>
</>
}
maxW={['90vw', '500px']}
>
<ModalBody>
{inviteList.map((item) => (
{inviteList?.map((item) => (
<Flex
key={item.teamId}
alignItems={'center'}

View File

@@ -292,6 +292,14 @@ export const DatasetSearchModule: FlowModuleTemplateType = {
{ label: '20', value: 20 }
]
},
{
key: 'rerank',
type: FlowNodeInputTypeEnum.switch,
label: '结果重排',
description: '将召回的结果进行进一步重排,可增加召回率',
plusField: true,
value: false
},
Input_Template_UserChatInput
],
outputs: [

View File

@@ -2,18 +2,32 @@ import type {
ChatModelItemType,
FunctionModelItemType,
LLMModelItemType,
VectorModelItemType
VectorModelItemType,
AudioSpeechModels,
WhisperModelType
} from '@fastgpt/global/core/ai/model.d';
import type { FeConfigsType } from '@fastgpt/global/common/system/types/index.d';
export type ConfigFileType = {
FeConfig: FeConfigsType;
SystemParams: SystemEnvType;
ChatModels: ChatModelItemType[];
QAModels: LLMModelItemType[];
CQModels: FunctionModelItemType[];
ExtractModels: FunctionModelItemType[];
QGModels: LLMModelItemType[];
VectorModels: VectorModelItemType[];
AudioSpeechModels: AudioSpeechModelType[];
WhisperModel: WhisperModelType;
};
export type InitDateResponse = {
chatModels: ChatModelItemType[];
qaModels: LLMModelItemType[];
cqModels: FunctionModelItemType[];
extractModels: FunctionModelItemType[];
qgModels: LLMModelItemType[];
vectorModels: VectorModelItemType[];
audioSpeechModels: AudioSpeechModels[];
feConfigs: FeConfigsType;
priceMd: string;
systemVersion: string;

View File

@@ -22,6 +22,7 @@ export type SearchTestProps = {
datasetId: string;
text: string;
limit?: number;
rerank?: boolean;
};
/* ======= collections =========== */

View File

@@ -1,7 +1,7 @@
import type { AppTTSConfigType } from '@/types/app';
export type GetChatSpeechProps = {
chatItemId?: string;
ttsConfig: AppTTSConfigType;
input: string;
shareId?: string;
};

View File

@@ -95,13 +95,13 @@ function App({ Component, pageProps }: AppProps) {
<title>{feConfigs?.systemTitle || process.env.SYSTEM_NAME || 'GPT'}</title>
<meta
name="description"
content="FastGPT is a knowledge-based question answering system built on the LLM. It offers out-of-the-box data processing and model invocation capabilities. Moreover, it allows for workflow orchestration through Flow visualization, thereby enabling complex question and answer scenarios!"
content="FastGPT 是一个大模型应用编排系统,提供开箱即用的数据处理、模型调用等能力,可以快速的构建知识库并通过 Flow 可视化进行工作流编排,实现复杂的知识库场景!"
/>
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no, viewport-fit=cover"
/>
<link rel="icon" href={feConfigs.favicon || '/favicon.ico'} />
<link rel="icon" href={feConfigs.favicon || process.env.SYSTEM_FAVICON} />
</Head>
{scripts?.map((item, i) => <Script key={i} strategy="lazyOnload" {...item}></Script>)}

View File

@@ -10,6 +10,8 @@ import {
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { Types, connectionMongo } from '@fastgpt/service/common/mongo';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
let success = 0;
/* pg 中的数据搬到 mongo dataset.datas 中,并做映射 */
@@ -43,57 +45,59 @@ type PgItemType = {
};
async function init(limit: number): Promise<any> {
const { rows: idList } = await PgClient.query<{ id: string }>(
`SELECT id FROM ${PgDatasetTableName} WHERE inited=1`
const { rows } = await PgClient.query<{ id: string; data_id: string }>(
`SELECT id,data_id FROM ${PgDatasetTableName} WHERE team_id = tmb_id`
);
console.log('totalCount', idList.length);
console.log('totalCount', rows.length);
await delay(2000);
if (idList.length === 0) return;
if (rows.length === 0) return;
for (let i = 0; i < limit; i++) {
initData(i);
}
async function initData(index: number): Promise<any> {
const dataId = idList[index]?.id;
if (!dataId) {
const item = rows[index];
if (!item) {
console.log('done');
return;
}
// get limit data where data_id is null
const { rows } = await PgClient.query<PgItemType>(
`SELECT id,q,a,dataset_id,collection_id,data_id FROM ${PgDatasetTableName} WHERE id=${dataId};`
);
const data = rows[0];
if (!data) {
console.log('done');
return;
// get mongo
const mongoData = await MongoDatasetData.findById(item.data_id, '_id teamId tmbId');
if (!mongoData) {
return initData(index + limit);
}
try {
// update mongo data and update inited
await MongoDatasetData.findByIdAndUpdate(data.data_id, {
q: data.q,
a: data.a,
indexes: [
{
defaultIndex: !data.a,
type: data.a ? DatasetDataIndexTypeEnum.qa : DatasetDataIndexTypeEnum.chunk,
dataId: data.id,
text: data.q
}
]
// find team owner
const db = connectionMongo?.connection?.db;
const TeamMember = db.collection(TeamMemberCollectionName);
const tmb = await TeamMember.findOne({
teamId: new Types.ObjectId(mongoData.teamId),
role: 'owner'
});
// update pg data_id
await PgClient.query(`UPDATE ${PgDatasetTableName} SET inited=0 WHERE id=${dataId};`);
if (!tmb) {
return initData(index + limit);
}
// update mongo and pg tmb_id
await MongoDatasetData.findByIdAndUpdate(item.data_id, {
tmbId: tmb._id
});
await PgClient.query(
`UPDATE ${PgDatasetTableName} SET tmb_id = '${String(tmb._id)}' WHERE id = '${item.id}'`
);
console.log(++success);
return initData(index + limit);
} catch (error) {
console.log(error);
console.log(data);
await delay(500);
return initData(index);
}

View File

@@ -4,17 +4,18 @@ import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { uploadMongoImg } from '@fastgpt/service/common/file/image/controller';
type Props = { base64Img: string };
type Props = { base64Img: string; expiredTime?: Date };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { userId } = await authCert({ req, authToken: true });
const { base64Img } = req.body as Props;
const { teamId } = await authCert({ req, authToken: true });
const { base64Img, expiredTime } = req.body as Props;
const data = await uploadMongoImg({
userId,
base64Img
teamId,
base64Img,
expiredTime
});
jsonRes(res, { data });

View File

@@ -1,12 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { GetChatSpeechProps } from '@/global/core/chat/api.d';
import { text2Speech } from '@fastgpt/service/core/ai/audio/speech';
import { pushAudioSpeechBill } from '@/service/support/wallet/bill/push';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { authCertAndShareId } from '@fastgpt/service/support/permission/auth/common';
import { authType2BillSource } from '@/service/support/wallet/bill/utils';
import { getAudioSpeechModel } from '@/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
/*
1. get tts from chatItem store
@@ -18,50 +19,67 @@ import { authType2BillSource } from '@/service/support/wallet/bill/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { chatItemId, ttsConfig, input } = req.body as GetChatSpeechProps;
const { ttsConfig, input, shareId } = req.body as GetChatSpeechProps;
const { teamId, tmbId, authType } = await authCert({ req, authToken: true });
const chatItem = await (async () => {
if (!chatItemId) return null;
return await MongoChatItem.findOne(
{
dataId: chatItemId
},
'tts'
);
})();
if (chatItem?.tts) {
return jsonRes(res, {
data: chatItem.tts
});
if (!ttsConfig.model || !ttsConfig.voice) {
throw new Error('model or voice not found');
}
const { tts, model } = await text2Speech({
const { teamId, tmbId, authType } = await authCertAndShareId({ req, authToken: true, shareId });
const ttsModel = getAudioSpeechModel(ttsConfig.model);
const voiceData = ttsModel.voices?.find((item) => item.value === ttsConfig.voice);
if (!voiceData) {
throw new Error('voice not found');
}
const ttsBuffer = await MongoTTSBuffer.findOne(
{
bufferId: voiceData.bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed })
},
'buffer'
);
if (ttsBuffer?.buffer) {
return res.end(new Uint8Array(ttsBuffer.buffer.buffer));
}
await text2Speech({
res,
input,
model: ttsConfig.model,
voice: ttsConfig.voice,
input
});
speed: ttsConfig.speed,
props: {
// temp code
baseUrl: ttsModel.baseUrl || '',
key: ttsModel.key || ''
},
onSuccess: async ({ model, buffer }) => {
try {
pushAudioSpeechBill({
model: model,
textLength: input.length,
tmbId,
teamId,
source: authType2BillSource({ authType })
});
(async () => {
if (!chatItem) return;
try {
chatItem.tts = tts;
await chatItem.save();
} catch (error) {}
})();
jsonRes(res, {
data: tts
});
pushAudioSpeechBill({
model: model,
textLength: input.length,
tmbId,
teamId,
source: authType2BillSource({ authType })
await MongoTTSBuffer.create({
bufferId: voiceData.bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed }),
buffer
});
} catch (error) {}
},
onError: (err) => {
jsonRes(res, {
code: 500,
error: err
});
}
});
} catch (err) {
jsonRes(res, {

View File

@@ -17,10 +17,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const datasets = await MongoDataset.find({
...mongoRPermission({ teamId, tmbId, role }),
type: 'dataset'
});
}).lean();
const data = datasets.map((item) => ({
...item.toJSON(),
...item,
vectorModel: getVectorModel(item.vectorModel),
agentModel: getQAModel(item.agentModel),
canWrite: String(item.tmbId) === tmbId,

View File

@@ -22,11 +22,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
...mongoRPermission({ teamId, tmbId, role }),
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
}).sort({ updateTime: -1 });
})
.sort({ updateTime: -1 })
.lean();
const data = await Promise.all(
datasets.map(async (item) => ({
...item.toJSON(),
...item,
vectorModel: getVectorModel(item.vectorModel),
agentModel: getQAModel(item.agentModel),
canWrite,

View File

@@ -16,7 +16,7 @@ import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { datasetId, text, limit = 20 } = req.body as SearchTestProps;
const { datasetId, text, limit = 20, rerank } = req.body as SearchTestProps;
if (!datasetId || !text) {
throw new Error('缺少参数');
@@ -38,7 +38,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
text,
model: dataset.vectorModel,
limit: Math.min(limit, 50),
datasetIds: [datasetId]
datasetIds: [datasetId],
rerank
});
// push bill

View File

@@ -2,7 +2,7 @@ import type { FeConfigsType, SystemEnvType } from '@fastgpt/global/common/system
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { readFileSync } from 'fs';
import type { InitDateResponse } from '@/global/common/api/systemRes';
import type { ConfigFileType, InitDateResponse } from '@/global/common/api/systemRes';
import { formatPrice } from '@fastgpt/global/support/wallet/bill/tools';
import { getTikTokenEnc } from '@fastgpt/global/common/string/tiktoken';
import { initHttpAgent } from '@fastgpt/service/common/middle/httpAgent';
@@ -13,15 +13,9 @@ import {
defaultExtractModels,
defaultQGModels,
defaultVectorModels,
defaultAudioSpeechModels
defaultAudioSpeechModels,
defaultWhisperModel
} from '@fastgpt/global/core/ai/model';
import {
AudioSpeechModelType,
ChatModelItemType,
FunctionModelItemType,
LLMModelItemType,
VectorModelItemType
} from '@fastgpt/global/core/ai/model.d';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
getInitConfig();
@@ -34,8 +28,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
qaModels: global.qaModels,
cqModels: global.cqModels,
extractModels: global.extractModels,
qgModels: global.qgModels,
vectorModels: global.vectorModels,
audioSpeechModels: global.audioSpeechModels.map((item) => ({
...item,
baseUrl: undefined,
key: undefined
})),
priceMd: global.priceMd,
systemVersion: global.systemVersion || '0.0.0'
}
@@ -79,60 +77,39 @@ export function getInitConfig() {
const filename =
process.env.NODE_ENV === 'development' ? 'data/config.local.json' : '/app/data/config.json';
const res = JSON.parse(readFileSync(filename, 'utf-8')) as {
FeConfig: FeConfigsType;
SystemParams: SystemEnvType;
ChatModels: ChatModelItemType[];
QAModels: LLMModelItemType[];
CQModels: FunctionModelItemType[];
ExtractModels: FunctionModelItemType[];
QGModels: LLMModelItemType[];
VectorModels: VectorModelItemType[];
AudioSpeechModels: AudioSpeechModelType[];
};
const res = JSON.parse(readFileSync(filename, 'utf-8')) as ConfigFileType;
console.log(`System Version: ${global.systemVersion}`);
console.log(res);
global.systemEnv = res.SystemParams
? { ...defaultSystemEnv, ...res.SystemParams }
: defaultSystemEnv;
global.feConfigs = res.FeConfig
? { ...defaultFeConfigs, ...res.FeConfig, isPlus: !!res.SystemParams?.pluginBaseUrl }
: defaultFeConfigs;
global.chatModels = res.ChatModels || defaultChatModels;
global.qaModels = res.QAModels || defaultQAModels;
global.cqModels = res.CQModels || defaultCQModels;
global.extractModels = res.ExtractModels || defaultExtractModels;
global.qgModels = res.QGModels || defaultQGModels;
global.vectorModels = res.VectorModels || defaultVectorModels;
global.audioSpeechModels = res.AudioSpeechModels || defaultAudioSpeechModels;
setDefaultData(res);
} catch (error) {
setDefaultData();
console.log('get init config error, set default', error);
}
}
export function setDefaultData() {
global.systemEnv = defaultSystemEnv;
global.feConfigs = defaultFeConfigs;
export function setDefaultData(res?: ConfigFileType) {
global.systemEnv = res?.SystemParams
? { ...defaultSystemEnv, ...res.SystemParams }
: defaultSystemEnv;
global.feConfigs = res?.FeConfig
? { ...defaultFeConfigs, ...res.FeConfig, isPlus: !!res.SystemParams?.pluginBaseUrl }
: defaultFeConfigs;
global.chatModels = defaultChatModels;
global.qaModels = defaultQAModels;
global.cqModels = defaultCQModels;
global.extractModels = defaultExtractModels;
global.qgModels = defaultQGModels;
global.chatModels = res?.ChatModels || defaultChatModels;
global.qaModels = res?.QAModels || defaultQAModels;
global.cqModels = res?.CQModels || defaultCQModels;
global.extractModels = res?.ExtractModels || defaultExtractModels;
global.qgModels = res?.QGModels || defaultQGModels;
global.vectorModels = defaultVectorModels;
global.audioSpeechModels = defaultAudioSpeechModels;
global.vectorModels = res?.VectorModels || defaultVectorModels;
global.audioSpeechModels = res?.AudioSpeechModels || defaultAudioSpeechModels;
global.whisperModel = res?.WhisperModel || defaultWhisperModel;
global.priceMd = '';
console.log('use default config');
console.log(global);
}
@@ -174,6 +151,10 @@ ${global.extractModels
${global.qgModels
?.map((item) => `| 下一步指引-${item.name} | ${formatPrice(item.price, 1000)} |`)
.join('\n')}
${global.audioSpeechModels
?.map((item) => `| 语音播放-${item.name} | ${formatPrice(item.price, 1000)} |`)
.join('\n')}
${`| 语音输入-${global.whisperModel.name} | ${global.whisperModel.price}/分钟 |`}
`;
console.log(global.priceMd);
}

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { authCert, authCertAndShareId } from '@fastgpt/service/support/permission/auth/common';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { getUploadModel } from '@fastgpt/service/common/file/upload/multer';
import fs from 'fs';
import { getAIApi } from '@fastgpt/service/core/ai/config';
import { pushWhisperBill } from '@/service/support/wallet/bill/push';
const upload = getUploadModel({
maxSize: 2
@@ -12,9 +13,16 @@ const upload = getUploadModel({
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
files,
metadata: { duration, shareId }
} = await upload.doUpload<{ duration: number; shareId?: string }>(req, res);
const { teamId, tmbId } = await authCert({ req, authToken: true });
const { files } = await upload.doUpload(req, res);
if (!global.whisperModel) {
throw new Error('whisper model not found');
}
const file = files[0];
@@ -26,7 +34,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const result = await ai.audio.transcriptions.create({
file: fs.createReadStream(file.path),
model: 'whisper-1'
model: global.whisperModel.model
});
pushWhisperBill({
teamId,
tmbId,
duration
});
jsonRes(res, {

View File

@@ -86,6 +86,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
if (chatMessages[chatMessages.length - 1].obj !== ChatRoleEnum.Human) {
chatMessages.pop();
}
// user question
const question = chatMessages.pop();
if (!question) {
@@ -173,15 +174,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const isAppOwner = !shareId && String(user.team.tmbId) === String(app.tmbId);
/* format prompts */
const prompts = history.concat(gptMessage2ChatType(messages));
// set sse response headers
if (stream) {
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
}
const concatHistory = history.concat(chatMessages);
/* start flow controller */
const { responseData, answerText } = await dispatchModules({
@@ -193,7 +186,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
tmbId: user.team.tmbId,
variables,
params: {
history: prompts,
history: concatHistory,
userChatInput: question.value
},
stream,

View File

@@ -52,13 +52,14 @@ import MyIcon from '@/components/Icon';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { addVariable } from '@/components/core/module/VariableEditModal';
import { KbParamsModal } from '@/components/core/module/DatasetSelectModal';
import { DatasetParamsModal } from '@/components/core/module/DatasetSelectModal';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import PermissionIconText from '@/components/support/permission/IconText';
import QGSwitch from '../QGSwitch';
import TTSSelect from '../TTSSelect';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
const VariableEditModal = dynamic(() => import('@/components/core/module/VariableEditModal'));
const InfoModal = dynamic(() => import('../InfoModal'));
@@ -585,15 +586,15 @@ const Settings = ({ appId }: { appId: string }) => {
)}
{isOpenKbParams && (
<KbParamsModal
searchEmptyText={getValues('dataset.searchEmptyText')}
searchLimit={getValues('dataset.searchLimit')}
searchSimilarity={getValues('dataset.searchSimilarity')}
<DatasetParamsModal
{...getValues('dataset')}
onClose={onCloseKbParams}
onChange={({ searchEmptyText, searchLimit, searchSimilarity }) => {
setValue('dataset.searchEmptyText', searchEmptyText);
setValue('dataset.searchLimit', searchLimit);
setValue('dataset.searchSimilarity', searchSimilarity);
onChange={(e) => {
setValue('dataset', {
...getValues('dataset'),
...e
});
setRefresh((state) => !state);
}}
/>
@@ -676,6 +677,7 @@ const ChatTest = ({ appId }: { appId: string }) => {
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(modules)}
showFileSelector={checkChatSupportSelectFileByModules(modules)}
onStartChat={startChat}
onDelMessage={() => {}}
/>

View File

@@ -1,15 +1,16 @@
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { Box, Flex } from '@chakra-ui/react';
import { Box, Button, Flex, ModalBody, useDisclosure, Image } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import MySelect from '@/components/Select';
import { TTSTypeEnum } from '@/constants/app';
import { Text2SpeechVoiceEnum, openaiTTSModel } from '@fastgpt/global/core/ai/speech/constant';
import { AppTTSConfigType } from '@/types/app';
import { useAudioPlay } from '@/web/common/utils/voice';
import { useLoading } from '@/web/common/hooks/useLoading';
import { audioSpeechModels } from '@/web/common/system/staticData';
import MyModal from '@/components/MyModal';
import MySlider from '@/components/Slider';
const TTSSelect = ({
value,
@@ -19,8 +20,16 @@ const TTSSelect = ({
onChange: (e: AppTTSConfigType) => void;
}) => {
const { t } = useTranslation();
const { playAudio, audioLoading } = useAudioPlay({ ttsConfig: value });
const { Loading } = useLoading();
const { isOpen, onOpen, onClose } = useDisclosure();
const list = useMemo(
() => [
{ label: t('core.app.tts.Close'), value: TTSTypeEnum.none },
{ label: t('core.app.tts.Web'), value: TTSTypeEnum.web },
...audioSpeechModels.map((item) => item?.voices || []).flat()
],
[t]
);
const formatValue = useMemo(() => {
if (!value || !value.type) {
@@ -31,62 +40,126 @@ const TTSSelect = ({
}
return value.voice;
}, [value]);
const formLabel = useMemo(
() => list.find((item) => item.value === formatValue)?.label || t('common.UnKnow'),
[formatValue, list, t]
);
const { playAudio, cancelAudio, audioLoading, audioPlaying } = useAudioPlay({ ttsConfig: value });
const onclickChange = useCallback(
(e: string) => {
if (e === TTSTypeEnum.none || e === TTSTypeEnum.web) {
onChange({ type: e as `${TTSTypeEnum}` });
} else {
const audioModel = audioSpeechModels.find(
(item) => item.voices?.find((voice) => voice.value === e)
);
if (!audioModel) {
return;
}
onChange({
...value,
type: TTSTypeEnum.model,
model: openaiTTSModel,
voice: e as `${Text2SpeechVoiceEnum}`,
speed: 1
model: audioModel.model,
voice: e
});
}
},
[onChange]
[onChange, value]
);
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/ttsFill'} mr={2} w={'16px'} />
<MyIcon name={'core/app/tts'} mr={2} w={'16px'} />
<Box>{t('core.app.TTS')}</Box>
<MyTooltip label={t('core.app.TTS Tip')} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
{formatValue !== TTSTypeEnum.none && (
<MyTooltip label={t('core.app.tts.Test Listen')}>
<MyIcon
mr={1}
name="common/playLight"
w={['14px', '16px']}
cursor={'pointer'}
onClick={() => {
playAudio({
text: t('core.app.tts.Test Listen Text')
});
}}
/>
</MyTooltip>
)}
<MySelect
w={'150px'}
value={formatValue}
list={[
{ label: t('core.app.tts.Close'), value: TTSTypeEnum.none },
{ label: t('core.app.tts.Web'), value: TTSTypeEnum.web },
{ label: 'Alloy', value: Text2SpeechVoiceEnum.alloy },
{ label: 'Echo', value: Text2SpeechVoiceEnum.echo },
{ label: 'Fable', value: Text2SpeechVoiceEnum.fable },
{ label: 'Onyx', value: Text2SpeechVoiceEnum.onyx },
{ label: 'Nova', value: Text2SpeechVoiceEnum.nova },
{ label: 'Shimmer', value: Text2SpeechVoiceEnum.shimmer }
]}
onchange={onclickChange}
/>
<Loading loading={audioLoading} />
<MyTooltip label={t('core.app.Select TTS')}>
<Box
cursor={'pointer'}
_hover={{ bg: 'myGray.100' }}
py={2}
px={3}
borderRadius={'md'}
onClick={onOpen}
color={'myGray.600'}
>
{formLabel}
</Box>
</MyTooltip>
<MyModal
title={
<>
<MyIcon name={'core/app/tts'} mr={2} w={'20px'} />
{t('core.app.TTS')}
</>
}
isOpen={isOpen}
onClose={onClose}
w={'500px'}
>
<ModalBody px={[5, 16]} py={[4, 8]}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
{t('core.app.tts.Speech model')}
<MySelect w={'220px'} value={formatValue} list={list} onchange={onclickChange} />
</Flex>
<Flex mt={8} justifyContent={'space-between'} alignItems={'center'}>
{t('core.app.tts.Speech speed')}
<MySlider
markList={[
{ label: '0.3', value: 0.3 },
{ label: '2', value: 2 }
]}
width={'220px'}
min={0.3}
max={2}
step={0.1}
value={value.speed || 1}
onChange={(e) => {
onChange({
...value,
speed: e
});
}}
/>
</Flex>
{formatValue !== TTSTypeEnum.none && (
<Flex mt={10} justifyContent={'end'}>
{audioPlaying ? (
<Flex>
<Image src="/icon/speaking.gif" w={'24px'} alt={''} />
<Button
ml={2}
variant={'gray'}
isLoading={audioLoading}
leftIcon={<MyIcon name={'core/chat/stopSpeech'} w={'16px'} />}
onClick={() => {
cancelAudio();
}}
>
{t('core.chat.tts.Stop Speech')}
</Button>
</Flex>
) : (
<Button
isLoading={audioLoading}
leftIcon={<MyIcon name={'core/app/headphones'} w={'16px'} />}
onClick={() => {
playAudio({
text: t('core.app.tts.Test Listen Text')
});
}}
>
{t('core.app.tts.Test Listen')}
</Button>
)}
</Flex>
)}
</ModalBody>
</MyModal>
</Flex>
);
};

View File

@@ -7,6 +7,7 @@ import Avatar from '@/components/Avatar';
import ToolMenu from './ToolMenu';
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { useRouter } from 'next/router';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
const ChatHeader = ({
history,
@@ -27,7 +28,10 @@ const ChatHeader = ({
const theme = useTheme();
const { isPc } = useSystemStore();
const title = useMemo(
() => history[history.length - 2]?.value?.slice(0, 8) || appName || '新对话',
() =>
chatContentReplaceBlock(history[history.length - 2]?.value)?.slice(0, 8) ||
appName ||
'新对话',
[appName, history]
);

View File

@@ -32,6 +32,8 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
const router = useRouter();
@@ -78,7 +80,10 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
abortSignal: controller
});
const newTitle = prompts[0].content?.slice(0, 20) || '新对话';
const newTitle =
chatContentReplaceBlock(prompts[0].content).slice(0, 20) ||
prompts[1]?.value?.slice(0, 20) ||
'新对话';
// update history
if (completionChatId !== chatId) {
@@ -363,6 +368,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
appAvatar={chatData.app.avatar}
userAvatar={userInfo?.avatar}
userGuideModule={chatData.app?.userGuideModule}
showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)}
feedbackType={'user'}
onUpdateVariable={(e) => {}}
onStartChat={startChat}

View File

@@ -20,6 +20,7 @@ import PageContainer from '@/components/PageContainer';
import ChatHeader from './components/ChatHeader';
import ChatHistorySlider from './components/ChatHistorySlider';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
const OutLink = ({
shareId,
@@ -254,6 +255,9 @@ const OutLink = ({
appAvatar={shareChatData.app.avatar}
userAvatar={shareChatData.userAvatar}
userGuideModule={shareChatData.app?.userGuideModule}
showFileSelector={checkChatSupportSelectFileByChatModels(
shareChatData.app.chatModels
)}
feedbackType={'user'}
onUpdateVariable={(e) => {
setShareChatData((state) => ({

View File

@@ -2,11 +2,13 @@ import React from 'react';
import { Box, Flex, Button } from '@chakra-ui/react';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useImportStore, SelectorContainer, PreviewFileOrChunk } from './Provider';
import { useTranslation } from 'next-i18next';
const fileExtension = '.csv';
const csvTemplate = `index,content\n"被索引的内容","对应的答案。CSV 中请注意内容不能包含双引号,双引号是列分割符号"\n"什么是 laf","laf 是一个云函数开发平台……",""\n"什么是 sealos","Sealos 是以 kubernetes 为内核的云操作系统发行版,可以……"`;
const CsvImport = () => {
const { t } = useTranslation();
const { successChunks, totalChunks, isUnselectedFile, onclickUpload, uploading } =
useImportStore();
@@ -24,6 +26,7 @@ const CsvImport = () => {
value: csvTemplate,
type: 'text/csv'
}}
tip={t('dataset.import csv tip')}
>
<Flex mt={3}>
<Button isDisabled={uploading} onClick={openConfirm(onclickUpload)}>

View File

@@ -55,6 +55,7 @@ export interface Props extends BoxProps {
};
showUrlFetch?: boolean;
showCreateFile?: boolean;
tip?: string;
}
const FileSelect = ({
@@ -65,6 +66,7 @@ const FileSelect = ({
fileTemplate,
showUrlFetch = true,
showCreateFile = true,
tip,
...props
}: Props) => {
const { datasetDetail } = useDatasetStore();
@@ -423,6 +425,7 @@ const FileSelect = ({
{t('file.Click to download file template', { name: fileTemplate.filename })}
</Box>
)}
{!!tip && <Box color={'myGray.500'}>{tip}</Box>}
{selectingText !== undefined && (
<FileSelectLoading loading text={selectingText} fixed={false} />
)}

View File

@@ -403,12 +403,14 @@ export const SelectorContainer = ({
showUrlFetch,
showCreateFile,
fileTemplate,
tip,
children
}: {
fileExtension: string;
showUrlFetch?: boolean;
showCreateFile?: boolean;
fileTemplate?: FileSelectProps['fileTemplate'];
tip?: string;
children: React.ReactNode;
}) => {
const { files, setPreviewFile, isUnselectedFile, setFiles, chunkLen } = useImportStore();
@@ -433,6 +435,7 @@ export const SelectorContainer = ({
showUrlFetch={showUrlFetch}
showCreateFile={showCreateFile}
fileTemplate={fileTemplate}
tip={tip}
py={isUnselectedFile ? '100px' : 5}
/>
{!isUnselectedFile && (

View File

@@ -117,10 +117,8 @@ const InputDataModal = ({
const { mutate: sureImportData, isLoading: isImporting } = useRequest({
mutationFn: async (e: InputDataType) => {
if (!e.q) {
return toast({
title: '匹配的知识点不能为空',
status: 'warning'
});
setCurrentTab(TabEnum.content);
return Promise.reject(t('dataset.data.input is empty'));
}
if (countPromptTokens(e.q) >= maxToken) {
return toast({

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Box, Textarea, Button, Flex, useTheme, Grid, Progress } from '@chakra-ui/react';
import { Box, Textarea, Button, Flex, useTheme, Grid, Progress, Switch } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useSearchTestStore, SearchTestStoreItemType } from '@/web/core/dataset/store/searchTest';
import { getDatasetDataItemById, postSearchText } from '@/web/core/dataset/api';
@@ -15,6 +15,7 @@ import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { useTranslation } from 'next-i18next';
import { feConfigs } from '@/web/common/system/staticData';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
const Test = ({ datasetId }: { datasetId: string }) => {
@@ -28,6 +29,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
const [inputText, setInputText] = useState('');
const [datasetTestItem, setDatasetTestItem] = useState<SearchTestStoreItemType>();
const [editInputData, setEditInputData] = useState<InputDataType & { collectionId: string }>();
const [rerank, setRerank] = useState(false);
const kbTestHistory = useMemo(
() => datasetTestList.filter((item) => item.datasetId === datasetId),
@@ -35,7 +37,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
);
const { mutate, isLoading } = useRequest({
mutationFn: () => postSearchText({ datasetId, text: inputText.trim() }),
mutationFn: () => postSearchText({ datasetId, text: inputText.trim(), rerank, limit: 20 }),
onSuccess(res: SearchDataResponseItemType[]) {
if (!res || res.length === 0) {
return toast({
@@ -91,7 +93,13 @@ const Test = ({ datasetId }: { datasetId: string }) => {
onChange={(e) => setInputText(e.target.value)}
/>
<Flex alignItems={'center'} justifyContent={'flex-end'}>
<Box mr={3} color={'myGray.500'}>
{feConfigs?.isPlus && (
<Flex alignItems={'center'}>
{t('dataset.recall.rerank')}
<Switch ml={1} isChecked={rerank} onChange={(e) => setRerank(e.target.checked)} />
</Flex>
)}
<Box mx={3} color={'myGray.500'}>
{inputText.length}
</Box>
<Button isDisabled={inputText === ''} isLoading={isLoading} onClick={mutate}>

View File

@@ -10,7 +10,6 @@ import Ability from './components/Ability';
import Choice from './components/Choice';
import Footer from './components/Footer';
import Loading from '@/components/Loading';
import Head from 'next/head';
const Home = ({ homeUrl = '/' }: { homeUrl: string }) => {
const router = useRouter();
@@ -26,9 +25,6 @@ const Home = ({ homeUrl = '/' }: { homeUrl: string }) => {
return (
<>
<Head>
<title>{feConfigs?.systemTitle || 'FastGPT'}</title>
</Head>
<Box id="home" bg={'myWhite.600'} h={'100vh'} overflowY={'auto'} overflowX={'hidden'}>
<Box position={'fixed'} zIndex={10} top={0} left={0} right={0}>
<Navbar />

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