Compare commits

...

38 Commits

Author SHA1 Message Date
Archer
608e58ba41 4.8.13 test (#3107)
* perf: select file

* perf: drop files

* fix: imple mode adapt files
2024-11-09 15:07:24 +08:00
Archer
044b0c57f7 4.8.13 test (#3106)
* perf: select file

* perf: drop files

* perf: env template
2024-11-09 14:46:14 +08:00
a.e.
7d7454ef3b feat: source id prefix env (#3103) 2024-11-09 14:44:10 +08:00
Archer
0d658c0114 fix: plugin select files and ai response check (#3104)
* fix: plugin select files and ai response check

* perf: text editor selector;tool call tip;remove invalid image url;

* perf: select file

* perf: drop files
2024-11-09 14:43:15 +08:00
Archer
d58cf44778 4.8.13 test (#3102)
* fix: loop index;edge parent check

* perf: reference invalid check

* fix: ts
2024-11-08 20:53:58 +08:00
Archer
7537330a3b feat: loop start add index (#3101)
* feat: loop start add index

* update doc
2024-11-08 17:21:19 +08:00
heheer
a7f881fc5e array reference check & node ui (#3100) 2024-11-08 17:19:05 +08:00
Archer
fc7304d3cd 4.8.13 test (#3098)
* perf: loop node refresh

* rename context

* comment

* fix: ts

* perf: push chat log
2024-11-08 16:02:33 +08:00
a.e.
aa50174066 feat: support push chat log (#3093)
* feat: custom uid/metadata

* to: custom info

* fix: chat push latest

* feat: add chat log envs

* refactor: move timer to pushChatLog

* fix: using precise log

---------

Co-authored-by: Finley Ge <m13203533462@163.com>
2024-11-08 15:35:27 +08:00
heheer
5b2cc097b0 loop node dynamic height (#3092)
* loop node dynamic height

* fix

* fix
2024-11-08 12:10:15 +08:00
Archer
7a933f73b6 fix: http tool response (#3097) 2024-11-08 11:56:18 +08:00
Archer
3e5d7d0d7a fix: workflow file upload refresh (#3088) 2024-11-07 15:04:46 +08:00
Archer
d15ec1ae69 4.8.13 test (#3087)
* fix: image expired

* fix: datacard navbar ui

* perf: build action
2024-11-07 14:01:00 +08:00
heheer
3b82ed0aa1 feat: support sub route config (#3071)
* feat: support sub route config

* dockerfile

* fix upload

* delete unused code
2024-11-07 13:53:23 +08:00
Archer
dc95ab1dc1 4.8.13 test (#3085)
* perf: workflow node ui

* chat iframe url
2024-11-07 12:03:21 +08:00
Archer
fa2fbc1ddd perf: workflow context split (#3083)
* perf: workflow context split

* perf: context
2024-11-07 10:05:03 +08:00
heheer
10421d73f4 add dispatch try catch (#3075) 2024-11-07 10:05:03 +08:00
Archer
a9ee6e6a5e feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077)
* fix: plugin output check

* fix: ui refresh error when continuous file upload

* feat: View will move when workflow check error
2024-11-07 10:05:03 +08:00
heheer
0f1932aadc node pluginoutput check (#3074) 2024-11-07 10:05:02 +08:00
Archer
65a39e80b8 feat: iframe code block;perf: workflow selector type (#3076)
* feat: iframe code block

* perf: workflow selector type
2024-11-07 10:05:02 +08:00
heheer
0db0cbf376 feat: support array reference multi-select (#3041)
* feat: support array reference multi-select

* fix build

* fix

* fix loop multi-select

* adjust condition

* fix get value

* array and non-array conversion

* fix plugin input

* merge func
2024-11-07 10:05:02 +08:00
heheer
f4dbe7c021 fix ui (#3065)
* fix ui

* fix
2024-11-07 10:05:02 +08:00
Archer
07b3a0a35d perf: dockerfile proxy (#3067) 2024-11-07 10:05:01 +08:00
Archer
fd49ad1342 Adapt findLast api;perf: markdown zh format. (#3066)
* perf: context code

* fix: adapt findLast api

* perf: commercial plugin run error

* perf: markdown zh format
2024-11-07 10:05:01 +08:00
Finley Ge
f90803c558 pref: slow query of full text search (#3044) 2024-11-07 10:05:01 +08:00
papapatrick
49cd2d7a3c add chatType (#3060) 2024-11-07 10:05:01 +08:00
papapatrick
727bd7144c feat: add chat history time label (#3024)
* feat:add chat and logs time

* feat: add chat history time label

* code perf

* code perf

---------

Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>
2024-11-07 10:05:01 +08:00
Archer
469858877e New file upload (#3058)
* feat: toolNode aiNode readFileNode adapt new version

* update docker-compose

* update tip

* feat: adapt new file version

* perf: file input

* fix: ts
2024-11-07 10:05:01 +08:00
heheer
7a929db0a5 chore(ui): login page & workflow page (#3046)
* login page & number input & multirow select & llm select

* workflow

* adjust nodes
2024-11-07 10:04:58 +08:00
Carson Yang
0645b274da Docs: add docs for loop node (#3069) 2024-11-05 10:11:59 +08:00
heheer
cf8786b194 fix: node version update flicker (#3052) 2024-11-01 15:54:12 +08:00
tzq84
be6269688b feat(voice): add fallback for browsers without MediaSource support (#3043)
- Add MediaSource support detection function
- Implement fallback solution for browsers that don't support MediaSource:
  - For single audio: Read full stream before playing
  - For segmented audio: Wait until all text is received then play as one audio
- Improve code robustness and browser compatibility
2024-11-01 14:50:49 +08:00
Archer
912b264a47 perf: forbid image to base64 (#3038)
* perf: forbid image to base64

* update file upload path

* feat: support promptCall use image

* fix: echarts load

* update doc
2024-11-01 14:29:20 +08:00
Archer
7ef1821557 Update 4812.md (#3051) 2024-11-01 11:16:27 +08:00
Archer
4061b11922 fix: dataset select check (#3048) 2024-10-31 17:25:02 +08:00
Finley Ge
bc171db945 fix: alldataset get dataset without folders. omit the permission check (#3047) 2024-10-31 17:06:56 +08:00
Jiangween
eb365fef44 Update i18n files and Upload component (#3040)
* Update i18n files and Upload component

* 完善 i18n 和优化 Upload.tsx 文件

* 修改Upload.tsx 文件的问题...
2024-10-31 15:25:00 +08:00
Archer
2e7047cb3b Update 4812.md (#3036) 2024-10-31 00:48:36 +08:00
251 changed files with 6059 additions and 3350 deletions

View File

@@ -90,3 +90,45 @@ jobs:
-t ${Docker_Hub_Tag} \
-t ${Docker_Hub_Latest} \
.
build-fastgpt-images-child-route:
runs-on: ubuntu-20.04
steps:
# Set tag
- name: Set image name and tag
run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
else
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-child-route:latest" >> $GITHUB_ENV
fi
- name: Build and publish image for main branch or tag push event
env:
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
run: |
docker buildx build \
-f projects/app/Dockerfile \
--platform linux/amd64,linux/arm64 \
--build-arg base_url=fastai \
--label "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/FastGPT" \
--label "org.opencontainers.image.description=fastgpt image" \
--push \
--cache-from=type=local,src=/tmp/.buildx-cache \
--cache-to=type=local,dest=/tmp/.buildx-cache \
-t ${Git_Tag} \
-t ${Git_Latest} \
-t ${Ali_Tag} \
-t ${Ali_Latest} \
-t ${Docker_Hub_Tag} \
-t ${Docker_Hub_Latest} \
.

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -1,5 +1,5 @@
---
title: 'V4.8.12(进行中)'
title: 'V4.8.12(需要初始化)'
description: 'FastGPT V4.8.12 更新说明'
icon: 'upgrade'
draft: false
@@ -13,8 +13,8 @@ weight: 812
### 2. 修改镜像
- 更新 FastGPT 镜像 tag: v4.8.12-beta
- 更新 FastGPT 管理端镜像 tag: v4.8.12-beta fastgpt-pro镜像
- 更新 FastGPT 镜像 tag: v4.8.12-fix
- 更新 FastGPT 管理端镜像 tag: v4.8.12 fastgpt-pro镜像
- Sandbox 镜像,可以不更新

View File

@@ -0,0 +1,25 @@
---
title: 'V4.8.13(进行中)'
description: 'FastGPT V4.8.13 更新说明'
icon: 'upgrade'
draft: false
toc: true
weight: 811
---
## 更新说明
1. 新增 - 数组变量选择支持多选,可以选多个数组或对应的单一数据类型,会自动按选择顺序进行合并。
2. 新增 - 文件上传方案调整,节点直接支持接收文件链接,插件自定义变量支持文件上传。
3. 新增 - 对话记录增加时间显示。
4. 新增 - 工作流校验错误时,跳转至错误节点。
5. 新增 - 循环节点增加下标值。
6. 新增 - 部分对话错误提醒增加翻译。
7. 优化 - 合并多个 system 提示词成 1 个,避免部分模型不支持多个 system 提示词。
8. 优化 - 知识库上传文件,优化报错提示。
9. 优化 - 全文检索语句,减少一轮查询。
10. 优化 - 修改 findLast 为 [...array].reverse().find适配旧版浏览器。
11. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。
12. 优化 - 工作流上下文拆分,性能优化。
13. 修复 - Dockerfile pnpm install 支持代理。
14. 修复 - BI 图表生成无法写入文件。

View File

@@ -0,0 +1,297 @@
---
title: "循环运行"
description: "FastGPT 循环运行节点介绍和使用"
icon: "input"
draft: false
toc: true
weight: 366
---
## 节点概述
【**循环运行**】节点是 FastGPT V4.8.11 版本新增的一个重要功能模块。它允许工作流对数组类型的输入数据进行迭代处理,每次处理数组中的一个元素,并自动执行后续节点,直到完成整个数组的处理。
这个节点的设计灵感来自编程语言中的循环结构,但以可视化的方式呈现。
![循环运行节点](/imgs/fastgpt-loop-node.png)
> 在程序中,节点可以理解为一个个 Function 或者接口。可以理解为它就是一个**步骤**。将多个节点一个个拼接起来,即可一步步的去实现最终的 AI 输出。
【**循环运行**】节点本质上也是一个 Function它的主要职责是自动化地重复执行特定的工作流程。
## 核心特性
1. **数组批量处理**
- 支持输入数组类型数据
- 自动遍历数组元素
- 保持处理顺序
- 支持并行处理 (性能优化)
2. **自动迭代执行**
- 自动触发后续节点
- 支持条件终止
- 支持循环计数
- 维护执行上下文
3. **与其他节点协同**
- 支持与 AI 对话节点配合
- 支持与 HTTP 节点配合
- 支持与内容提取节点配合
- 支持与判断器节点配合
## 应用场景
【**循环运行**】节点的主要作用是通过自动化的方式扩展工作流的处理能力,使 FastGPT 能够更好地处理批量任务和复杂的数据处理流程。特别是在处理大规模数据或需要多轮迭代的场景下,循环运行节点能显著提升工作流的效率和自动化程度。
【**循环运行**】节点特别适合以下场景:
1. **批量数据处理**
- 批量翻译文本
- 批量总结文档
- 批量生成内容
2. **数据流水线处理**
- 对搜索结果逐条分析
- 对知识库检索结果逐条处理
- 对 HTTP 请求返回的数组数据逐项处理
3. **递归或迭代任务**
- 长文本分段处理
- 多轮优化内容
- 链式数据处理
## 使用方法
### 输入参数设置
【**循环运行**】节点需要配置两个核心输入参数:
1. **数组 (必填)**:接收一个数组类型的输入,可以是:
- 字符串数组 (`Array<string>`)
- 数字数组 (`Array<number>`)
- 布尔数组 (`Array<boolean>`)
- 对象数组 (`Array<object>`)
2. **循环体 (必填)**:定义每次循环需要执行的节点流程,包含:
- 循环体开始:标记循环开始的位置。
- 循环体结束:标记循环结束的位置,并可选择输出结果变量。
### 循环体配置
![循环体配置](/imgs/fastgpt-loop-node-config.png)
1. 在循环体内部,可以添加任意类型的节点,如:
- AI 对话节点
- HTTP 请求节点
- 内容提取节点
- 文本加工节点等
2. 循环体结束节点配置:
- 通过下拉菜单选择要输出的变量
- 该变量将作为当前循环的结果被收集
- 所有循环的结果将组成一个新的数组作为最终输出
## 场景示例
### 批量处理数组
假设我们有一个包含多个文本的数组,需要对每个文本进行 AI 处理。这是循环运行节点最基础也最常见的应用场景。
#### 实现步骤
1. 准备输入数组
![准备输入数组](/imgs/fastgpt-loop-node-example-1.png)
使用【代码运行】节点创建测试数组:
```javascript
const texts = [
"这是第一段文本",
"这是第二段文本",
"这是第三段文本"
];
return { textArray: texts };
```
2. 配置循环运行节点
![配置循环运行节点](/imgs/fastgpt-loop-node-example-2.png)
- 数组输入:选择上一步代码运行节点的输出变量 `textArray`。
- 循环体内添加一个【AI 对话】节点,用于处理每个文本。这里我们输入的 prompt 为:`请将这段文本翻译成英文`。
- 再添加一个【指定回复】节点,用于输出翻译后的文本。
- 循环体结束节点选择输出变量为 AI 回复内容。
#### 运行流程
![运行流程](/imgs/fastgpt-loop-node-example-3.png)
1. 【代码运行】节点执行,生成测试数组
2. 【循环运行】节点接收数组,开始遍历
3. 对每个数组元素:
- 【AI 对话】节点处理当前元素
- 【指定回复】节点输出翻译后的文本
- 【循环体结束】节点收集处理结果
4. 完成所有元素处理后,输出结果数组
### 长文本翻译
在处理长文本翻译时,我们经常会遇到以下挑战:
- 文本长度超出 LLM 的 token 限制
- 需要保持翻译风格的一致性
- 需要维护上下文的连贯性
- 翻译质量需要多轮优化
【**循环运行**】节点可以很好地解决这些问题。
#### 实现步骤
1. 文本预处理与分段
![文本预处理与分段](/imgs/fastgpt-loop-node-example-4.png)
使用【代码运行】节点进行文本分段,代码如下:
```javascript
const MAX_HEADING_LENGTH = 7; // 最大标题长度
const MAX_HEADING_CONTENT_LENGTH = 200; // 最大标题内容长度
const MAX_HEADING_UNDERLINE_LENGTH = 200; // 最大标题下划线长度
const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100; // 最大HTML标题属性长度
const MAX_LIST_ITEM_LENGTH = 200; // 最大列表项长度
const MAX_NESTED_LIST_ITEMS = 6; // 最大嵌套列表项数
const MAX_LIST_INDENT_SPACES = 7; // 最大列表缩进空格数
const MAX_BLOCKQUOTE_LINE_LENGTH = 200; // 最大块引用行长度
const MAX_BLOCKQUOTE_LINES = 15; // 最大块引用行数
const MAX_CODE_BLOCK_LENGTH = 1500; // 最大代码块长度
const MAX_CODE_LANGUAGE_LENGTH = 20; // 最大代码语言长度
const MAX_INDENTED_CODE_LINES = 20; // 最大缩进代码行数
const MAX_TABLE_CELL_LENGTH = 200; // 最大表格单元格长度
const MAX_TABLE_ROWS = 20; // 最大表格行数
const MAX_HTML_TABLE_LENGTH = 2000; // 最大HTML表格长度
const MIN_HORIZONTAL_RULE_LENGTH = 3; // 最小水平分隔线长度
const MAX_SENTENCE_LENGTH = 400; // 最大句子长度
const MAX_QUOTED_TEXT_LENGTH = 300; // 最大引用文本长度
const MAX_PARENTHETICAL_CONTENT_LENGTH = 200; // 最大括号内容长度
const MAX_NESTED_PARENTHESES = 5; // 最大嵌套括号数
const MAX_MATH_INLINE_LENGTH = 100; // 最大行内数学公式长度
const MAX_MATH_BLOCK_LENGTH = 500; // 最大数学公式块长度
const MAX_PARAGRAPH_LENGTH = 1000; // 最大段落长度
const MAX_STANDALONE_LINE_LENGTH = 800; // 最大独立行长度
const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100; // 最大HTML标签属性长度
const MAX_HTML_TAG_CONTENT_LENGTH = 1000; // 最大HTML标签内容长度
const LOOKAHEAD_RANGE = 100; // 向前查找句子边界的字符数
const AVOID_AT_START = `[\\s\\]})>,']`; // 避免在开头匹配的字符
const PUNCTUATION = `[.!?…]|\\.{3}|[\\u2026\\u2047-\\u2049]|[\\p{Emoji_Presentation}\\p{Extended_Pictographic}]`; // 标点符号
const QUOTE_END = `(?:'(?=\`)|''(?=\`\`))`; // 引号结束
const SENTENCE_END = `(?:${PUNCTUATION}(?<!${AVOID_AT_START}(?=${PUNCTUATION}))|${QUOTE_END})(?=\\S|$)`; // 句子结束
const SENTENCE_BOUNDARY = `(?:${SENTENCE_END}|(?=[\\r\\n]|$))`; // 句子边界
const LOOKAHEAD_PATTERN = `(?:(?!${SENTENCE_END}).){1,${LOOKAHEAD_RANGE}}${SENTENCE_END}`; // 向前查找句子结束的模式
const NOT_PUNCTUATION_SPACE = `(?!${PUNCTUATION}\\s)`; // 非标点符号空格
const SENTENCE_PATTERN = `${NOT_PUNCTUATION_SPACE}(?:[^\\r\\n]{1,{MAX_LENGTH}}${SENTENCE_BOUNDARY}|[^\\r\\n]{1,{MAX_LENGTH}}(?=${PUNCTUATION}|$ {QUOTE_END})(?:${LOOKAHEAD_PATTERN})?)${AVOID_AT_START}*`; // 句子模式
const regex = new RegExp(
"(" +
// 1. Headings (Setext-style, Markdown, and HTML-style, with length constraints)
`(?:^(?:[#*=-]{1,${MAX_HEADING_LENGTH}}|\\w[^\\r\\n]{0,${MAX_HEADING_CONTENT_LENGTH}}\\r?\\n[-=]{2,${MAX_HEADING_UNDERLINE_LENGTH}}|<h[1-6][^>] {0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\\r\\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:</h[1-6]>)?(?:\\r?\\n|$))` +
"|" +
// New pattern for citations
`(?:\\[[0-9]+\\][^\\r\\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` +
"|" +
// 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints)
`(?:(?:^|\\r?\\n)[ \\t]{0,3}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_LIST_ITEM_LENGTH))}` +
`(?:(?:\\r?\\n[ \\t]{2,5}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` +
`(?:\\r?\\n[ \\t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\\d{1,3}\\.\\w\\.|\\[[ xX]\\])[ \\t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` +
"|" +
// 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints)
`(?:(?:^>(?:>|\\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\\r?\\n?){1,$ {MAX_BLOCKQUOTE_LINES}})` +
"|" +
// 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints)
`(?:(?:^|\\r?\\n)(?:\`\`\`|~~~)(?:\\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\\r?\\n[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:\`\`\`|~~~)\\r?\\n?` +
`|(?:(?:^|\\r?\\n)(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\\r?\\n(?: {4}|\\t)[^\\r\\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,$ {MAX_INDENTED_CODE_LINES}}\\r?\\n?)` +
`|(?:<pre>(?:<code>)?[\\s\\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:</code>)?</pre>))` +
"|" +
// 5. Tables (Markdown, grid tables, and HTML tables, with length constraints)
`(?:(?:^|\\r?\\n)(?:\\|[^\\r\\n]{0,${MAX_TABLE_CELL_LENGTH}}\\|(?:\\r?\\n\\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\\|){0,1}(?:\\r?\\n\\|[^\\r\\n]{0,$ {MAX_TABLE_CELL_LENGTH}}\\|){0,${MAX_TABLE_ROWS}}` +
`|<table>[\\s\\S]{0,${MAX_HTML_TABLE_LENGTH}}?</table>))` +
"|" +
// 6. Horizontal rules (Markdown and HTML hr tag)
`(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\\s*$|<hr\\s*/?>)` +
"|" +
// 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints)
`(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String (MAX_STANDALONE_LINE_LENGTH))}(?:</[a-zA-Z]+>)?(?:\\r?\\n|$))` +
"|" +
// 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation)
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` +
"|" +
// 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints)
"(?:" +
`(?<!\\w)\"\"\"[^\"]{0,${MAX_QUOTED_TEXT_LENGTH}}\"\"\"(?!\\w)` +
`|(?<!\\w)(?:['\"\`'"])[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}\\1(?!\\w)` +
`|(?<!\\w)\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}'(?!\\w)` +
`|(?<!\\w)\`\`[^\\r\\n]{0,${MAX_QUOTED_TEXT_LENGTH}}''(?!\\w)` +
`|\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\([^\\r\\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\)[^\\r\\n()]{0,$ {MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\)` +
`|\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\\[[^\\r\\n\\[\\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\\][^\\r\\n\\[\\]]{0,$ {MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\\]` +
`|\\$[^\\r\\n$]{0,${MAX_MATH_INLINE_LENGTH}}\\$` +
`|\`[^\`\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\`` +
")" +
"|" +
// 9. Paragraphs (with length constraints)
`(?!${AVOID_AT_START})(?:(?:^|\\r?\\n\\r?\\n)(?:<p>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:</p>)?(?=\\r? \\n\\r?\\n|$))` +
"|" +
// 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints)
`(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\\s\\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?</[a-zA-Z]+>|\\s*/>))` +
"|" +
// 12. LaTeX-style math expressions (inline and block, with length constraints)
`(?:(?:\\$\\$[\\s\\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\\$\\$)|(?:\\$[^\\$\\r\\n]{0,${MAX_MATH_INLINE_LENGTH}}\\$))` +
"|" +
// 14. Fallback for any remaining content (with length constraints)
`(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` +
")",
"gmu"
);
function main({text}){
const chunks = [];
let currentChunk = '';
const tokens = countToken(text)
const matches = text.match(regex);
if (matches) {
matches.forEach((match) => {
if (currentChunk.length + match.length <= 1000) {
currentChunk += match;
} else {
if (currentChunk) {
chunks.push(currentChunk);
}
currentChunk = match;
}
});
if (currentChunk) {
chunks.push(currentChunk);
}
}
return {chunks, tokens};
}
```
这里我们用到了 [Jina AI 开源的一个强大的正则表达式](https://x.com/JinaAI_/status/1823756993108304135),它能利用所有可能的边界线索和启发式方法来精确切分文本。
2. 配置循环运行节点
![配置循环运行节点](/imgs/fastgpt-loop-node-example-5.png)
- 数组输入:选择上一步代码运行节点的输出变量 `chunks`。
- 循环体内添加一个【代码运行】节点,对源文本进行格式化。
- 添加一个【搜索词库】节点,将专有名词的词库作为知识库,在翻译前进行搜索。
- 添加一个【AI 对话】节点,使用 CoT 思维链,让 LLM 显式地、系统地生成推理链条,展示翻译的完整思考过程。
- 添加一个【代码运行】节点将【AI 对话】节点最后一轮的翻译结果提取出来。
- 添加一个【指定回复】节点,输出翻译后的文本。
- 循环体结束节点选择输出变量为【取出翻译文本】的输出变量 `result`。

View File

@@ -139,6 +139,8 @@ services:
- OPENAI_BASE_URL=http://oneapi:3000/v1
# AI模型的API Key。这里默认填写了OneAPI的快速默认key测试通后务必及时修改
- CHAT_API_KEY=sk-fastgpt
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
- MULTIPLE_DATA_TO_BASE64=false
# 数据库最大连接数
- DB_MAX_LINK=30
# 登录凭证密钥

View File

@@ -97,6 +97,8 @@ services:
- OPENAI_BASE_URL=http://oneapi:3000/v1
# AI模型的API Key。这里默认填写了OneAPI的快速默认key测试通后务必及时修改
- CHAT_API_KEY=sk-fastgpt
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
- MULTIPLE_DATA_TO_BASE64=false
# 数据库最大连接数
- DB_MAX_LINK=30
# 登录凭证密钥

View File

@@ -77,6 +77,8 @@ services:
- OPENAI_BASE_URL=http://oneapi:3000/v1
# AI模型的API Key。这里默认填写了OneAPI的快速默认key测试通后务必及时修改
- CHAT_API_KEY=sk-fastgpt
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
- MULTIPLE_DATA_TO_BASE64=false
# 数据库最大连接数
- DB_MAX_LINK=30
# 登录凭证密钥

View File

@@ -16,6 +16,8 @@ export const bucketNameMap = {
}
};
export const ReadFileBaseUrl = `${process.env.FE_DOMAIN || ''}/api/common/file/read`;
export const ReadFileBaseUrl = `${process.env.FE_DOMAIN || ''}${process.env.NEXT_PUBLIC_BASE_URL}/api/common/file/read`;
export const documentFileType = '.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx';
export const imageFileType =
'.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg, .tiff, .tif, .ico, .heic, .heif, .avif';

View File

@@ -1,4 +1,7 @@
import { detect } from 'jschardet';
import { documentFileType, imageFileType } from './constants';
import { ChatFileTypeEnum } from '../../core/chat/constants';
import { UserChatItemValueItemType } from '../../core/chat/type';
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
@@ -13,3 +16,40 @@ export const formatFileSize = (bytes: number): string => {
export const detectFileEncoding = (buffer: Buffer) => {
return detect(buffer.slice(0, 200))?.encoding?.toLocaleLowerCase();
};
// Url => user upload file type
export const parseUrlToFileType = (url: string): UserChatItemValueItemType['file'] | undefined => {
if (typeof url !== 'string') return;
const parseUrl = new URL(url, 'https://locaohost:3000');
const filename = (() => {
// Old version file url: https://xxx.com/file/read?filename=xxx.pdf
const filenameQuery = parseUrl.searchParams.get('filename');
if (filenameQuery) return filenameQuery;
// Common file https://xxx.com/xxx.pdf?xxxx=xxx
const pathname = parseUrl.pathname;
if (pathname) return pathname.split('/').pop();
})();
if (!filename) return;
const extension = filename.split('.').pop()?.toLowerCase() || '';
if (!extension) return;
if (documentFileType.includes(extension)) {
return {
type: ChatFileTypeEnum.file,
name: filename,
url
};
}
if (imageFileType.includes(extension)) {
return {
type: ChatFileTypeEnum.image,
name: filename,
url
};
}
};

View File

@@ -2,6 +2,7 @@ import dayjs from 'dayjs';
import cronParser from 'cron-parser';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { i18nT } from '../../../web/i18n/utils';
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -23,31 +24,51 @@ export const formatTimeToChatTime = (time: Date) => {
// 如果传入时间小于60秒返回刚刚
if (now.diff(target, 'second') < 60) {
return '刚刚';
return i18nT('common:just_now');
}
// 如果时间是今天,展示几时:几分
//用#占位i18n生效后replace成:
if (now.isSame(target, 'day')) {
return target.format('HH : mm');
return target.format('HH#mm');
}
// 如果是昨天,展示昨天
if (now.subtract(1, 'day').isSame(target, 'day')) {
return '昨天';
}
// 如果是前天,展示前天
if (now.subtract(2, 'day').isSame(target, 'day')) {
return '前天';
return i18nT('common:yesterday');
}
// 如果是今年,展示某月某日
if (now.isSame(target, 'year')) {
return target.format('MM/DD');
return target.format('MM-DD');
}
// 如果是更久之前,展示某年某月某日
return target.format('YYYY/M/D');
return target.format('YYYY-M-D');
};
export const formatTimeToChatItemTime = (time: Date) => {
const now = dayjs();
const target = dayjs(time);
const detailTime = target.format('HH#mm');
// 如果时间是今天,展示几时:几分
if (now.isSame(target, 'day')) {
return detailTime;
}
// 如果是昨天,展示昨天+几时:几分
if (now.subtract(1, 'day').isSame(target, 'day')) {
return i18nT('common:yesterday_detail_time');
}
// 如果是今年,展示某月某日+几时:几分
if (now.isSame(target, 'year')) {
return target.format('MM-DD') + ' ' + detailTime;
}
// 如果是更久之前,展示某年某月某日+几时:几分
return target.format('YYYY-M-D') + ' ' + detailTime;
};
/* cron time parse */

View File

@@ -207,8 +207,8 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
];
// Document quote prompt
export const Prompt_DocumentQuote = `将 <Reference></Reference> 中的内容作为本次对话的参考:
<Reference>
export const Prompt_DocumentQuote = `将 <FilesContent></FilesContent> 中的内容作为本次对话的参考:
<FilesContent>
{{quote}}
</Reference>
</FilesContent>
`;

View File

@@ -14,7 +14,6 @@ import type {
ChatCompletionToolMessageParam
} from '../../core/ai/type.d';
import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants';
const GPT2Chat = {
[ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System,
[ChatCompletionRequestMessageRoleEnum.User]: ChatRoleEnum.Human,
@@ -61,14 +60,14 @@ export const chats2GPTMessages = ({
return {
type: 'image_url',
image_url: {
url: item.file?.url || ''
url: item.file.url
}
};
} else if (item.file?.type === ChatFileTypeEnum.file) {
return {
type: 'file_url',
name: item.file?.name || '',
url: item.file?.url || ''
url: item.file.url
};
}
}

View File

@@ -126,6 +126,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
moduleName?: string;
ttsBuffer?: Uint8Array;
responseData?: ChatHistoryItemResType[];
time?: Date;
} & ChatBoxInputType &
ResponseTagItemType;

View File

@@ -30,7 +30,8 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue
// Keep the first n and last n characters
export const getHistoryPreview = (
completeMessages: ChatItemType[],
size = 100
size = 100,
useVision = false
): {
obj: `${ChatRoleEnum}`;
value: string;
@@ -48,7 +49,8 @@ export const getHistoryPreview = (
item.value
?.map((item) => {
if (item?.text?.content) return item?.text?.content;
if (item.file?.type === 'image') return 'Input an image';
if (item.file?.type === 'image' && useVision)
return `![Input an image](${item.file.url.slice(0, 100)}...)`;
return '';
})
.filter(Boolean)

View File

@@ -201,6 +201,7 @@ export enum NodeInputKeyEnum {
nodeHeight = 'nodeHeight',
// loop start
loopStartInput = 'loopStartInput',
loopStartIndex = 'loopStartIndex',
// loop end
loopEndInput = 'loopEndInput',
@@ -256,9 +257,9 @@ export enum NodeOutputKeyEnum {
// loop
loopArray = 'loopArray',
// loop start
loopStartInput = 'loopStartInput',
loopStartIndex = 'loopStartIndex',
// form input
formInputResult = 'formInputResult'
@@ -334,3 +335,21 @@ export enum ContentTypes {
xml = 'xml',
raw = 'raw-text'
}
export const ArrayTypeMap: Record<WorkflowIOValueTypeEnum, WorkflowIOValueTypeEnum> = {
[WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString,
[WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber,
[WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean,
[WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.arrayString,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.arrayNumber,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.arrayBoolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.chatHistory]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.datasetQuote]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.dynamic]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.selectDataset]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.selectApp]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.arrayAny,
[WorkflowIOValueTypeEnum.any]: WorkflowIOValueTypeEnum.arrayAny
};

View File

@@ -27,7 +27,9 @@ export enum FlowNodeInputTypeEnum { // render ui
settingDatasetQuotePrompt = 'settingDatasetQuotePrompt',
hidden = 'hidden',
custom = 'custom'
custom = 'custom',
fileSelect = 'fileSelect'
}
export const FlowNodeInputMap: Record<
FlowNodeInputTypeEnum,
@@ -85,6 +87,9 @@ export const FlowNodeInputMap: Record<
},
[FlowNodeInputTypeEnum.textarea]: {
icon: 'core/workflow/inputType/textarea'
},
[FlowNodeInputTypeEnum.fileSelect]: {
icon: 'core/workflow/inputType/file'
}
};
@@ -137,43 +142,43 @@ export enum FlowNodeTypeEnum {
// node IO value type
export const FlowValueTypeMap = {
[WorkflowIOValueTypeEnum.string]: {
label: 'string',
label: 'String',
value: WorkflowIOValueTypeEnum.string
},
[WorkflowIOValueTypeEnum.number]: {
label: 'number',
label: 'Number',
value: WorkflowIOValueTypeEnum.number
},
[WorkflowIOValueTypeEnum.boolean]: {
label: 'boolean',
label: 'Boolean',
value: WorkflowIOValueTypeEnum.boolean
},
[WorkflowIOValueTypeEnum.object]: {
label: 'object',
label: 'Object',
value: WorkflowIOValueTypeEnum.object
},
[WorkflowIOValueTypeEnum.arrayString]: {
label: 'array<string>',
label: 'Array<string>',
value: WorkflowIOValueTypeEnum.arrayString
},
[WorkflowIOValueTypeEnum.arrayNumber]: {
label: 'array<number>',
label: 'Array<number>',
value: WorkflowIOValueTypeEnum.arrayNumber
},
[WorkflowIOValueTypeEnum.arrayBoolean]: {
label: 'array<boolean>',
label: 'Array<boolean>',
value: WorkflowIOValueTypeEnum.arrayBoolean
},
[WorkflowIOValueTypeEnum.arrayObject]: {
label: 'array<object>',
label: 'Array<object>',
value: WorkflowIOValueTypeEnum.arrayObject
},
[WorkflowIOValueTypeEnum.arrayAny]: {
label: 'array',
label: 'Array',
value: WorkflowIOValueTypeEnum.arrayAny
},
[WorkflowIOValueTypeEnum.any]: {
label: 'any',
label: 'Any',
value: WorkflowIOValueTypeEnum.any
},
[WorkflowIOValueTypeEnum.chatHistory]: {

View File

@@ -135,6 +135,9 @@ export type DispatchNodeResponseType = {
extensionResult?: string;
extensionTokens?: number;
// dataset concat
concatLength?: number;
// cq
cqList?: ClassifyQuestionAgentItemType[];
cqResult?: string;
@@ -216,5 +219,7 @@ export type AIChatNodeProps = {
[NodeInputKeyEnum.aiChatQuoteTemplate]?: string;
[NodeInputKeyEnum.aiChatQuotePrompt]?: string;
[NodeInputKeyEnum.aiChatVision]?: boolean;
[NodeInputKeyEnum.stringQuoteText]?: string;
[NodeInputKeyEnum.fileUrlList]?: string[];
};

View File

@@ -5,8 +5,8 @@ import { StoreNodeItemType } from '../type/node';
import { StoreEdgeItemType } from '../type/edge';
import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type';
import { VARIABLE_NODE_ID } from '../constants';
import { isReferenceValue } from '../utils';
import { FlowNodeOutputItemType, ReferenceValueProps } from '../type/io';
import { isValidReferenceValueFormat } from '../utils';
import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
@@ -34,7 +34,7 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
2. Check that the workflow starts at the interaction node
*/
export const getLastInteractiveValue = (histories: ChatItemType[]) => {
const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI);
const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI);
if (lastAIMessage) {
const lastValue = lastAIMessage.value[lastAIMessage.value.length - 1];
@@ -225,37 +225,129 @@ export const checkNodeRunStatus = ({
return 'wait';
};
/*
Get the value of the reference variable/node output
1. [string,string]
2. [string,string][]
*/
export const getReferenceVariableValue = ({
value,
nodes,
variables
}: {
value: ReferenceValueProps;
value?: ReferenceValueType;
nodes: RuntimeNodeItemType[];
variables: Record<string, any>;
}) => {
const nodeIds = nodes.map((node) => node.nodeId);
if (!isReferenceValue(value, nodeIds)) {
return value;
}
const sourceNodeId = value[0];
const outputId = value[1];
if (!value) return value;
if (sourceNodeId === VARIABLE_NODE_ID && outputId) {
return variables[outputId];
// handle single reference value
if (isValidReferenceValueFormat(value)) {
const sourceNodeId = value[0];
const outputId = value[1];
if (sourceNodeId === VARIABLE_NODE_ID) {
if (!outputId) return undefined;
return variables[outputId];
}
const node = nodes.find((node) => node.nodeId === sourceNodeId);
if (!node) {
return value;
}
return node.outputs.find((output) => output.id === outputId)?.value;
}
const node = nodes.find((node) => node.nodeId === sourceNodeId);
// handle reference array
if (
Array.isArray(value) &&
value.length > 0 &&
value.every((item) => isValidReferenceValueFormat(item))
) {
const result = value.map<any>((val) => {
return getReferenceVariableValue({
value: val,
nodes,
variables
});
});
if (!node) {
return undefined;
return result.flat().filter((item) => item !== undefined);
}
const outputValue = node.outputs.find((output) => output.id === outputId)?.value;
return outputValue;
return value;
};
// replace {{$xx.xx$}} variables for text
export function replaceEditorVariable({
text,
nodes,
variables,
runningNode
}: {
text: any;
nodes: RuntimeNodeItemType[];
variables: Record<string, any>; // global variables
runningNode: RuntimeNodeItemType;
}) {
if (typeof text !== 'string') return text;
const globalVariables = Object.keys(variables).map((key) => {
return {
nodeId: VARIABLE_NODE_ID,
id: key,
value: variables[key]
};
});
// Upstream node outputs
const nodeVariables = nodes
.map((node) => {
return node.outputs.map((output) => {
return {
nodeId: node.nodeId,
id: output.id,
value: output.value
};
});
})
.flat();
// Get runningNode inputs(Will be replaced with reference)
const customInputs = runningNode.inputs.flatMap((item) => {
return [
{
id: item.key,
value: getReferenceVariableValue({
value: item.value,
nodes,
variables
}),
nodeId: runningNode.nodeId
}
];
});
const allVariables = [...globalVariables, ...nodeVariables, ...customInputs];
// Replace {{$xxx.xxx$}} to value
for (const key in allVariables) {
const variable = allVariables[key];
const val = variable.value;
const formatVal = (() => {
if (val === undefined) return '';
if (val === null) return 'null';
return typeof val === 'object' ? JSON.stringify(val) : String(val);
})();
const regex = new RegExp(`\\{\\{\\$(${variable.nodeId}\\.${variable.id})\\$\\}\\}`, 'g');
text = text.replace(regex, formatVal);
}
return text || '';
}
export const textAdaptGptResponse = ({
text,
model = '',

View File

@@ -75,10 +75,17 @@ export const Input_Template_Text_Quote: FlowNodeInputItemType = {
description: i18nT('app:document_quote_tip'),
valueType: WorkflowIOValueTypeEnum.string
};
export const Input_Template_File_Link_Prompt: FlowNodeInputItemType = {
key: NodeInputKeyEnum.fileUrlList,
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.input],
label: i18nT('app:file_quote_link'),
debugLabel: i18nT('app:file_quote_link'),
valueType: WorkflowIOValueTypeEnum.arrayString
};
export const Input_Template_File_Link: FlowNodeInputItemType = {
key: NodeInputKeyEnum.fileUrlList,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: true,
label: i18nT('app:workflow.user_file_input'),
debugLabel: i18nT('app:workflow.user_file_input'),
description: i18nT('app:workflow.user_file_input_desc'),
@@ -104,7 +111,7 @@ export const Input_Template_Node_Height: FlowNodeInputItemType = {
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.number,
label: '',
value: 900
value: 600
};
export const Input_Template_Stream_MODE: FlowNodeInputItemType = {

View File

@@ -17,7 +17,8 @@ import {
Input_Template_History,
Input_Template_System_Prompt,
Input_Template_UserChatInput,
Input_Template_Text_Quote
Input_Template_Text_Quote,
Input_Template_File_Link_Prompt
} from '../../input';
import { chatNodeSystemPromptTip, systemPromptTip } from '../../tip';
import { getHandleConfig } from '../../utils';
@@ -55,7 +56,7 @@ export const AiChatModule: FlowNodeTemplateType = {
showStatus: true,
isTool: true,
courseUrl: '/docs/workflow/modules/ai_chat/',
version: '481',
version: '4813',
inputs: [
Input_Template_SettingAiModel,
// --- settings modal
@@ -89,7 +90,7 @@ export const AiChatModule: FlowNodeTemplateType = {
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
valueType: WorkflowIOValueTypeEnum.boolean,
value: false
value: true
},
// settings modal ---
{
@@ -100,7 +101,7 @@ export const AiChatModule: FlowNodeTemplateType = {
},
Input_Template_History,
Input_Template_Dataset_Quote,
Input_Template_Text_Quote,
Input_Template_File_Link_Prompt,
{ ...Input_Template_UserChatInput, toolDescription: i18nT('workflow:user_question') }
],

View File

@@ -25,7 +25,7 @@ export const getOneQuoteInputTemplate = ({
}): FlowNodeInputItemType => ({
key,
renderTypeList: [FlowNodeInputTypeEnum.reference],
label: `${i18nT('workflow:quote_num')},{ num: ${index} }`,
label: `${i18nT('workflow:quote_num')}-${index}`,
debugLabel: i18nT('workflow:knowledge_base_reference'),
canEdit: true,
valueType: WorkflowIOValueTypeEnum.datasetQuote

View File

@@ -1,9 +1,9 @@
import { ReferenceValueProps } from 'core/workflow/type/io';
import { ReferenceItemValueType } from '../../../type/io';
import { VariableConditionEnum } from './constant';
export type IfElseConditionType = 'AND' | 'OR';
export type ConditionListItemType = {
variable?: ReferenceValueProps;
variable?: ReferenceItemValueType;
condition?: VariableConditionEnum;
value?: string;
};

View File

@@ -1,8 +1,13 @@
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '../../../node/constant';
import { FlowNodeTemplateType } from '../../../type/node.d';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../../constants';
import { getHandleConfig } from '../../utils';
@@ -28,7 +33,21 @@ export const LoopStartNode: FlowNodeTemplateType = {
label: '',
required: true,
value: ''
},
{
key: NodeInputKeyEnum.loopStartIndex,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.number,
label: i18nT('workflow:Array_element_index')
}
],
outputs: []
outputs: [
{
id: NodeOutputKeyEnum.loopStartIndex,
key: NodeOutputKeyEnum.loopStartIndex,
label: i18nT('workflow:Array_element_index'),
type: FlowNodeOutputTypeEnum.static,
valueType: WorkflowIOValueTypeEnum.number
}
]
};

View File

@@ -23,7 +23,7 @@ export const ReadFilesNode: FlowNodeTemplateType = {
name: i18nT('app:workflow.read_files'),
intro: i18nT('app:workflow.read_files_tip'),
showStatus: true,
version: '489',
version: '4812',
isTool: true,
inputs: [
{

View File

@@ -24,17 +24,8 @@ export const TextEditorNode: FlowNodeTemplateType = {
name: i18nT('workflow:text_concatenation'),
intro: i18nT('workflow:intro_text_concatenation'),
courseUrl: '/docs/workflow/modules/text_editor/',
version: '486',
version: '4813',
inputs: [
{
...Input_Template_DynamicInput,
description: i18nT('workflow:dynamic_input_description_concat'),
customInputConfig: {
selectValueTypeList: Object.values(WorkflowIOValueTypeEnum),
showDescription: false,
showDefaultValue: false
}
},
{
key: NodeInputKeyEnum.textareaInput,
renderTypeList: [FlowNodeInputTypeEnum.textarea],

View File

@@ -20,6 +20,7 @@ import { chatNodeSystemPromptTip, systemPromptTip } from '../tip';
import { LLMModelTypeEnum } from '../../../ai/constants';
import { getHandleConfig } from '../utils';
import { i18nT } from '../../../../../web/i18n/utils';
import { Input_Template_File_Link_Prompt } from '../input';
export const ToolModule: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.tools,
@@ -32,7 +33,7 @@ export const ToolModule: FlowNodeTemplateType = {
intro: i18nT('workflow:template.tool_call_intro'),
showStatus: true,
courseUrl: '/docs/workflow/modules/tool/',
version: '481',
version: '4813',
inputs: [
{
...Input_Template_SettingAiModel,
@@ -67,6 +68,7 @@ export const ToolModule: FlowNodeTemplateType = {
placeholder: chatNodeSystemPromptTip
},
Input_Template_History,
Input_Template_File_Link_Prompt,
Input_Template_UserChatInput
],
outputs: [

View File

@@ -1,10 +1,10 @@
import { FlowNodeInputTypeEnum } from '../../../node/constant';
import { ReferenceValueProps } from '../../..//type/io';
import { ReferenceItemValueType, ReferenceValueType } from '../../..//type/io';
import { WorkflowIOValueTypeEnum } from '../../../constants';
export type TUpdateListItem = {
variable?: ReferenceValueProps;
value: ReferenceValueProps;
variable?: ReferenceItemValueType;
value?: ReferenceValueType; // input: ['',value], reference: [nodeId,outputId]
valueType?: WorkflowIOValueTypeEnum;
renderType: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference;
};

View File

@@ -43,6 +43,3 @@ export const WorkflowStart: FlowNodeTemplateType = {
}
]
};
export const isWorkflowStartOutput = (key?: string) =>
!!WorkflowStart.outputs.find((output) => output.key === key);

View File

@@ -56,6 +56,11 @@ export type FlowNodeInputItemType = InputComponentPropsType & {
canEdit?: boolean; // dynamic inputs
isPro?: boolean; // Pro version field
isToolOutput?: boolean;
// file
canSelectFile?: boolean;
canSelectImg?: boolean;
maxFiles?: number;
};
export type FlowNodeOutputItemType = {
@@ -75,4 +80,6 @@ export type FlowNodeOutputItemType = {
customFieldConfig?: CustomFieldConfigType;
};
export type ReferenceValueProps = [string, string | undefined];
export type ReferenceItemValueType = [string, string | undefined];
export type ReferenceArrayValueType = ReferenceItemValueType[];
export type ReferenceValueType = ReferenceItemValueType | ReferenceArrayValueType;

View File

@@ -12,7 +12,12 @@ import {
VARIABLE_NODE_ID,
NodeOutputKeyEnum
} from './constants';
import { FlowNodeInputItemType, FlowNodeOutputItemType, ReferenceValueProps } from './type/io.d';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType,
ReferenceArrayValueType,
ReferenceItemValueType
} from './type/io.d';
import { StoreNodeItemType } from './type/node';
import type {
VariableItemType,
@@ -30,8 +35,8 @@ import {
} from '../app/constants';
import { IfElseResultEnum } from './template/system/ifElse/constant';
import { RuntimeNodeItemType } from './runtime/type';
import { getReferenceVariableValue } from './runtime/utils';
import {
Input_Template_File_Link,
Input_Template_History,
Input_Template_Stream_MODE,
Input_Template_UserChatInput
@@ -261,8 +266,10 @@ export const appData2FlowNodeIO = ({
inputs: [
Input_Template_Stream_MODE,
Input_Template_History,
...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg
? [Input_Template_File_Link]
: []),
Input_Template_UserChatInput,
// ...(showFileLink ? [Input_Template_File_Link] : []),
...variableInput
],
outputs: [
@@ -298,9 +305,37 @@ export const formatEditorVariablePickerIcon = (
}));
};
export const isReferenceValue = (value: any, nodeIds: string[]): boolean => {
const validIdList = [VARIABLE_NODE_ID, ...nodeIds];
return Array.isArray(value) && value.length === 2 && validIdList.includes(value[0]);
// Check the value is a valid reference value format: [variableId, outputId]
export const isValidReferenceValueFormat = (value: any): value is ReferenceItemValueType => {
return Array.isArray(value) && value.length === 2 && typeof value[0] === 'string';
};
/*
Check whether the value([variableId, outputId]) value is a valid reference value:
1. The value must be an array of length 2
2. The first item of the array must be one of VARIABLE_NODE_ID or nodeIds
*/
export const isValidReferenceValue = (
value: any,
nodeIds: string[]
): value is ReferenceItemValueType => {
if (!isValidReferenceValueFormat(value)) return false;
const validIdSet = new Set([VARIABLE_NODE_ID, ...nodeIds]);
return validIdSet.has(value[0]);
};
/*
Check whether the value([variableId, outputId][]) value is a valid reference value array:
1. The value must be an array
2. The array must contain at least one element
3. Each element in the array must be a valid reference value
*/
export const isValidArrayReferenceValue = (
value: any,
nodeIds: string[]
): value is ReferenceArrayValueType => {
if (!Array.isArray(value)) return false;
return value.every((item) => isValidReferenceValue(item, nodeIds));
};
export const getElseIFLabel = (i: number) => {
@@ -342,79 +377,6 @@ export const updatePluginInputByVariables = (
);
};
// replace {{$xx.xx$}} variables for text
export function replaceEditorVariable({
text,
nodes,
variables,
runningNode
}: {
text: any;
nodes: RuntimeNodeItemType[];
variables: Record<string, any>; // global variables
runningNode: RuntimeNodeItemType;
}) {
if (typeof text !== 'string') return text;
const globalVariables = Object.keys(variables).map((key) => {
return {
nodeId: VARIABLE_NODE_ID,
id: key,
value: variables[key]
};
});
// Upstream node outputs
const nodeVariables = nodes
.map((node) => {
return node.outputs.map((output) => {
return {
nodeId: node.nodeId,
id: output.id,
value: output.value
};
});
})
.flat();
// Get runningNode inputs(Will be replaced with reference)
const customInputs = runningNode.inputs.flatMap((item) => {
if (Array.isArray(item.value)) {
return [
{
id: item.key,
value: getReferenceVariableValue({
value: item.value as ReferenceValueProps,
nodes,
variables
}),
nodeId: runningNode.nodeId
}
];
}
return [
{
id: item.key,
value: item.value,
nodeId: runningNode.nodeId
}
];
});
const allVariables = [...globalVariables, ...nodeVariables, ...customInputs];
// Replace {{$xxx.xxx$}} to value
for (const key in allVariables) {
const variable = allVariables[key];
const val = variable.value;
const formatVal = typeof val === 'object' ? JSON.stringify(val) : String(val);
const regex = new RegExp(`\\{\\{\\$(${variable.nodeId}\\.${variable.id})\\$\\}\\}`, 'g');
text = text.replace(regex, formatVal);
}
return text || '';
}
/* Get plugin runtime input user query */
export const getPluginRunUserQuery = ({
pluginInputs,

View File

@@ -1,7 +1,5 @@
import * as echarts from 'echarts';
import json5 from 'json5';
import { getFileSavePath } from '../../../utils';
import * as fs from 'fs';
import { SystemPluginSpecialResponse } from '../../../type.d';
type Props = {
@@ -82,25 +80,23 @@ const generateChart = async (title: string, xAxis: string, yAxis: string, chartT
chart.setOption(option);
// 生成 Base64 图像
const base64Image = chart.getDataURL();
const svgData = decodeURIComponent(base64Image.split(',')[1]);
const base64Image = chart.getDataURL({
type: 'png',
pixelRatio: 2 // 可以设置更高的像素比以获得更清晰的图像
});
const svgContent = decodeURIComponent(base64Image.split(',')[1]);
const base64 = `data:image/svg+xml;base64,${Buffer.from(svgContent).toString('base64')}`;
const fileName = `chart_${Date.now()}.svg`;
const filePath = getFileSavePath(fileName);
fs.writeFileSync(filePath, svgData);
// 释放图表实例
chart.dispose();
return filePath;
return base64;
};
const main = async ({ title, xAxis, yAxis, chartType }: Props): Response => {
const filePath = await generateChart(title, xAxis, yAxis, chartType);
const base64 = await generateChart(title, xAxis, yAxis, chartType);
return {
result: {
type: 'SYSTEM_PLUGIN_FILE',
path: filePath,
contentType: 'image/svg+xml'
type: 'SYSTEM_PLUGIN_BASE64',
value: base64,
extension: 'svg'
}
};
};

View File

@@ -4,9 +4,9 @@ import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type
export type SystemPluginResponseType = Promise<Record<string, any>>;
export type SystemPluginSpecialResponse = {
type: 'SYSTEM_PLUGIN_FILE';
path: string;
contentType: string;
type: 'SYSTEM_PLUGIN_BASE64';
value: string;
extension: string;
};
declare global {

View File

@@ -1,15 +0,0 @@
import path from 'path';
import * as fs from 'fs';
const isProduction = process.env.NODE_ENV === 'production';
export const getFileSavePath = (name: string) => {
if (isProduction) {
return `/app/plugin_file/${name}`;
}
const filePath = path.join(process.cwd(), 'local', 'plugin_file', name);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
return filePath;
};

View File

@@ -12,6 +12,7 @@ import { gridFsStream2Buffer, stream2Encoding } from './utils';
import { addLog } from '../../system/log';
import { readFromSecondary } from '../../mongo/utils';
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
import { Readable } from 'stream';
export function getGFSCollection(bucket: `${BucketNameEnum}`) {
MongoDatasetFileSchema;
@@ -76,6 +77,59 @@ export async function uploadFile({
return String(stream.id);
}
export async function uploadFileFromBase64Img({
bucketName,
teamId,
tmbId,
base64,
filename,
metadata = {}
}: {
bucketName: `${BucketNameEnum}`;
teamId: string;
tmbId: string;
base64: string;
filename: string;
metadata?: Record<string, any>;
}) {
if (!base64) return Promise.reject(`filePath is empty`);
if (!filename) return Promise.reject(`filename is empty`);
const base64Data = base64.split(',')[1];
const contentType = base64.split(',')?.[0]?.split?.(':')?.[1];
const buffer = Buffer.from(base64Data, 'base64');
const readableStream = new Readable({
read() {
this.push(buffer);
this.push(null);
}
});
const { stream: readStream, encoding } = await stream2Encoding(readableStream);
// Add default metadata
metadata.teamId = teamId;
metadata.tmbId = tmbId;
metadata.encoding = encoding;
// create a gridfs bucket
const bucket = getGridBucket(bucketName);
const stream = bucket.openUploadStream(filename, {
metadata,
contentType
});
// save to gridfs
await new Promise((resolve, reject) => {
readStream
.pipe(stream as any)
.on('finish', resolve)
.on('error', reject);
});
return String(stream.id);
}
export async function getFileById({
bucketName,

View File

@@ -9,6 +9,7 @@ import type { ReadFileResponse } from '../../../worker/readFile/type';
import axios from 'axios';
import { addLog } from '../../system/log';
import { batchRun } from '@fastgpt/global/common/fn/utils';
import { addHours } from 'date-fns';
export type readRawTextByLocalFileParams = {
teamId: string;
@@ -111,6 +112,7 @@ export const readRawContentByFileBuffer = async ({
type: MongoImageTypeEnum.collectionImage,
base64Img: `data:${item.mime};base64,${item.base64}`,
teamId,
expiredTime: addHours(new Date(), 1),
metadata: {
...metadata,
mime: item.mime

View File

@@ -7,14 +7,20 @@ import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
1. Commercial plugin: n points per times
2. Other plugin: sum of children points
*/
export const computedPluginUsage = async (
plugin: PluginRuntimeType,
childrenUsage: ChatNodeUsageType[]
) => {
export const computedPluginUsage = async ({
plugin,
childrenUsage,
error
}: {
plugin: PluginRuntimeType;
childrenUsage: ChatNodeUsageType[];
error?: boolean;
}) => {
const { source } = await splitCombinePluginId(plugin.id);
// Commercial plugin: n points per times
if (source === PluginSourceEnum.commercial) {
if (error) return 0;
return plugin.currentCost ?? 0;
}

View File

@@ -0,0 +1,155 @@
import { addLog } from '../../common/system/log';
import { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema';
import axios from 'axios';
import { AIChatItemType, ChatItemType } from '@fastgpt/global/core/chat/type';
export type Metadata = {
[key: string]: {
label: string;
value: string;
};
};
export const pushChatLog = ({
chatId,
chatItemIdHuman,
chatItemIdAi,
appId,
metadata
}: {
chatId: string;
chatItemIdHuman: string;
chatItemIdAi: string;
appId: string;
metadata?: Metadata;
}) => {
const interval = Number(process.env.CHAT_LOG_INTERVAL);
const url = process.env.CHAT_LOG_URL;
if (interval > 0 && url) {
addLog.info(`[ChatLogPush] push chat log after ${interval}ms`, {
appId,
chatItemIdHuman,
chatItemIdAi
});
setTimeout(() => {
pushChatLogInternal({ chatId, chatItemIdHuman, chatItemIdAi, appId, url, metadata });
}, interval);
}
};
type ChatItem = ChatItemType & {
userGoodFeedback?: string;
userBadFeedback?: string;
chatId: string;
responseData: {
moduleType: string;
runningTime: number; //s
historyPreview: { obj: string; value: string }[];
}[];
time: Date;
};
type ChatLog = {
title: string;
feedback: 'like' | 'dislike' | null;
chatItemId: string;
uid: string;
question: string;
answer: string;
chatId: string;
responseTime: number;
metadata: string;
sourceName: string;
createdAt: number;
sourceId: string;
};
const pushChatLogInternal = async ({
chatId,
chatItemIdHuman,
chatItemIdAi,
appId,
url,
metadata
}: {
chatId: string;
chatItemIdHuman: string;
chatItemIdAi: string;
appId: string;
url: string;
metadata?: Metadata;
}) => {
try {
const [chatItemHuman, chatItemAi] = await Promise.all([
MongoChatItem.findById(chatItemIdHuman).lean(),
MongoChatItem.findById(chatItemIdAi).lean() as Promise<AIChatItemType>
]);
if (!chatItemHuman || !chatItemAi) {
return;
}
const chat = await MongoChat.findOne({ chatId }).lean();
// addLog.warn('ChatLogDebug', chat);
// addLog.warn('ChatLogDebug', { chatItemHuman, chatItemAi });
if (!chat) {
return;
}
const metadataString = JSON.stringify(metadata ?? {});
const uid = chat.outLinkUid || chat.tmbId;
// Pop last two items
const question = chatItemHuman.value[chatItemHuman.value.length - 1]?.text?.content;
const answer = chatItemAi.value[chatItemAi.value.length - 1]?.text?.content;
if (!question || !answer) {
addLog.error('[ChatLogPush] question or answer is empty', {
question: chatItemHuman.value,
answer: chatItemAi.value
});
return;
}
const responseData = chatItemAi.responseData;
const responseTime =
responseData?.reduce((acc, item) => acc + (item?.runningTime ?? 0), 0) || 0;
const sourceIdPrefix = process.env.SOURCE_ID_PREFIX ?? '';
const chatLog: ChatLog = {
title: chat.title,
feedback: (() => {
if (chatItemAi.userGoodFeedback) {
return 'like';
} else if (chatItemAi.userBadFeedback) {
return 'dislike';
} else {
return null;
}
})(),
chatItemId: `${chatItemIdHuman},${chatItemIdAi}`,
uid,
question,
answer,
chatId,
responseTime: responseTime * 1000,
metadata: metadataString,
sourceName: chat.source ?? '-',
// @ts-ignore
createdAt: new Date(chatItemAi.time).getTime(),
sourceId: `${sourceIdPrefix}${appId}`
};
await axios
.post(`${url}/api/chat/push`, chatLog)
.then((res) => {
addLog.info('[ChatLogPush] push success', res.data);
})
.catch((e) => {
addLog.error('[ChatLogPush] push failed', { e, resData: e.response?.data });
});
} catch (e) {
addLog.error('[ChatLogPush] error', e);
}
};

View File

@@ -1,4 +1,9 @@
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d';
import type {
AIChatItemType,
ChatItemType,
UserChatItemType
} from '@fastgpt/global/core/chat/type.d';
import axios from 'axios';
import { MongoApp } from '../app/schema';
import {
ChatItemValueTypeEnum,
@@ -13,6 +18,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { pushChatLog } from './pushChatLog';
type Props = {
chatId: string;
@@ -67,7 +73,7 @@ export async function saveChat({
});
await mongoSessionRun(async (session) => {
await MongoChatItem.insertMany(
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany(
content.map((item) => ({
chatId,
teamId,
@@ -105,6 +111,13 @@ export async function saveChat({
upsert: true
}
);
pushChatLog({
chatId,
chatItemIdHuman: String(chatItemIdHuman),
chatItemIdAi: String(chatItemIdAi),
appId
});
});
if (isUpdateUseTime) {

View File

@@ -104,9 +104,12 @@ export const loadRequestMessages = async ({
}) => {
// Load image to base64
const loadImageToBase64 = async (messages: ChatCompletionContentPart[]) => {
if (process.env.MULTIPLE_DATA_TO_BASE64 === 'false') {
return messages;
}
return Promise.all(
messages.map(async (item) => {
if (item.type === 'image_url') {
if (item.type === 'image_url' && process.env.MULTIPLE_DATA_TO_BASE64 === 'true') {
// Remove url origin
const imgUrl = (() => {
if (origin && item.image_url.url.startsWith(origin)) {
@@ -115,38 +118,51 @@ export const loadRequestMessages = async ({
return item.image_url.url;
})();
// If imgUrl is a local path, load image from local, and set url to base64
if (imgUrl.startsWith('/')) {
addLog.debug('Load image from local server', {
baseUrl: serverRequestBaseUrl,
requestUrl: imgUrl
});
const response = await axios.get(imgUrl, {
baseURL: serverRequestBaseUrl,
responseType: 'arraybuffer',
proxy: false
});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const imageType =
getFileContentTypeFromHeader(response.headers['content-type']) ||
guessBase64ImageType(base64);
try {
// If imgUrl is a local path, load image from local, and set url to base64
if (imgUrl.startsWith('/')) {
addLog.debug('Load image from local server', {
baseUrl: serverRequestBaseUrl,
requestUrl: imgUrl
});
const response = await axios.get(imgUrl, {
baseURL: serverRequestBaseUrl,
responseType: 'arraybuffer',
proxy: false
});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const imageType =
getFileContentTypeFromHeader(response.headers['content-type']) ||
guessBase64ImageType(base64);
return {
...item,
image_url: {
...item.image_url,
url: `data:${imageType};base64,${base64}`
}
};
return {
...item,
image_url: {
...item.image_url,
url: `data:${imageType};base64,${base64}`
}
};
}
// 检查下这个图片是否可以被访问,如果不行的话,则过滤掉
const response = await axios.head(imgUrl, {
timeout: 10000
});
if (response.status < 200 || response.status >= 400) {
addLog.info(`Filter invalid image: ${imgUrl}`);
return;
}
} catch (error) {
return;
}
}
return item;
})
);
).then((res) => res.filter(Boolean) as ChatCompletionContentPart[]);
};
// Split question text and image
const parseStringWithImages = (input: string): ChatCompletionContentPart[] => {
if (!useVision) {
if (!useVision || input.length > 500) {
return [{ type: 'text', text: input || '' }];
}
@@ -167,8 +183,8 @@ export const loadRequestMessages = async ({
});
});
// Too many images or too long text, return text
if (httpsImages.length > 4 || input.length > 1000) {
// Too many images return text
if (httpsImages.length > 4) {
return [{ type: 'text', text: input || '' }];
}
@@ -176,7 +192,7 @@ export const loadRequestMessages = async ({
result.push({ type: 'text', text: input });
return result;
};
// Parse user content(text and img)
// Parse user content(text and img) Store history => api messages
const parseUserContent = async (content: string | ChatCompletionContentPart[]) => {
if (typeof content === 'string') {
return loadImageToBase64(parseStringWithImages(content));

View File

@@ -12,7 +12,7 @@ import {
DatasetDataWithCollectionType,
SearchDataResponseItemType
} from '@fastgpt/global/core/dataset/type';
import { DatasetColCollectionName, MongoDatasetCollection } from '../collection/schema';
import { MongoDatasetCollection } from '../collection/schema';
import { reRankRecall } from '../../../core/ai/rerank';
import { countPromptTokens } from '../../../common/string/tiktoken/index';
import { datasetSearchResultConcat } from '@fastgpt/global/core/dataset/search/utils';
@@ -320,11 +320,13 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
const fullTextRecall = async ({
query,
limit,
filterCollectionIdList
filterCollectionIdList,
forbidCollectionIdList
}: {
query: string;
limit: number;
filterCollectionIdList?: string[];
forbidCollectionIdList: string[];
}): Promise<{
fullTextRecallResults: SearchDataResponseItemType[];
tokenLen: number;
@@ -351,6 +353,13 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
$in: filterCollectionIdList.map((id) => new Types.ObjectId(id))
}
}
: {}),
...(forbidCollectionIdList && forbidCollectionIdList.length > 0
? {
collectionId: {
$nin: forbidCollectionIdList.map((id) => new Types.ObjectId(id))
}
}
: {})
}
},
@@ -367,31 +376,6 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
{
$limit: limit
},
{
$lookup: {
from: DatasetColCollectionName,
let: { collectionId: '$collectionId' },
pipeline: [
{
$match: {
$expr: { $eq: ['$_id', '$$collectionId'] },
forbid: { $eq: true } // 匹配被禁用的数据
}
},
{
$project: {
_id: 1 // 只需要_id字段来确认匹配
}
}
],
as: 'collection'
}
},
{
$match: {
collection: { $eq: [] } // 没有 forbid=true 的数据
}
},
{
$project: {
_id: 1,
@@ -509,7 +493,8 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
fullTextRecall({
query,
limit: fullTextLimit,
filterCollectionIdList
filterCollectionIdList,
forbidCollectionIdList
})
]);
totalTokens += tokens;

View File

@@ -28,6 +28,7 @@ import { computedMaxToken, llmCompletionsBodyFormat } from '../../../../ai/utils
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { i18nT } from '../../../../../../web/i18n/utils';
type FunctionRunResponseType = {
toolRunResponse: DispatchFlowResponse;
@@ -549,7 +550,7 @@ async function streamResponse({
}
if (!textAnswer && functionCalls.length === 0) {
return Promise.reject('LLM api response empty');
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
}
return { answer: textAnswer, functionCalls };

View File

@@ -25,45 +25,16 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { getMultiplePrompt, Prompt_Tool_Call } from './constants';
import { filterToolResponseToPreview } from './utils';
import { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { getFileContentFromLinks, getHistoryFileLinks } from '../../tools/readFiles';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { Prompt_DocumentQuote } from '@fastgpt/global/core/ai/prompt/AIChat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.answerText]: string;
[DispatchNodeResponseKeyEnum.interactive]?: InteractiveNodeResponseType;
}>;
/*
Tool call auth add file prompt to question。
Guide the LLM to call tool.
*/
export const toolCallMessagesAdapt = ({
userInput
}: {
userInput: UserChatItemValueItemType[];
}) => {
const files = userInput.filter((item) => item.type === 'file');
if (files.length > 0) {
return userInput.map((item) => {
if (item.type === 'text') {
const filesCount = files.filter((file) => file.file?.type === 'file').length;
const imgCount = files.filter((file) => file.file?.type === 'image').length;
const text = item.text?.content || '';
return {
...item,
text: {
content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text })
}
};
}
return item;
});
}
return userInput;
};
export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<Response> => {
const {
node: { nodeId, name, isEntry },
@@ -71,11 +42,21 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
runtimeEdges,
histories,
query,
params: { model, systemPrompt, userChatInput, history = 6 }
requestOrigin,
chatConfig,
runningAppInfo: { teamId },
params: {
model,
systemPrompt,
userChatInput,
history = 6,
fileUrlList: fileLinks,
aiChatVision
}
} = props;
const toolModel = getLLMModel(model);
const useVision = aiChatVision && toolModel.vision;
const chatHistories = getHistories(history, histories);
const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges });
@@ -109,18 +90,43 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
}
})();
props.node.isEntry = false;
const hasReadFilesTool = toolNodes.some(
(item) => item.flowNodeType === FlowNodeTypeEnum.readFiles
);
const globalFiles = chatValue2RuntimePrompt(query).files;
const { documentQuoteText, userFiles } = await getMultiInput({
histories: chatHistories,
requestOrigin,
maxFiles: chatConfig?.fileSelectConfig?.maxFiles || 20,
teamId,
fileLinks,
inputFiles: globalFiles
});
const concatenateSystemPrompt = [
toolModel.defaultSystemChatPrompt,
systemPrompt,
documentQuoteText
? replaceVariable(Prompt_DocumentQuote, {
quote: documentQuoteText
})
: ''
]
.filter(Boolean)
.join('\n\n===---===---===\n\n');
const messages: ChatItemType[] = (() => {
const value: ChatItemType[] = [
...getSystemPrompt_ChatItemType(toolModel.defaultSystemChatPrompt),
...getSystemPrompt_ChatItemType(systemPrompt),
...getSystemPrompt_ChatItemType(concatenateSystemPrompt),
// Add file input prompt to histories
...chatHistories.map((item) => {
if (item.obj === ChatRoleEnum.Human) {
return {
...item,
value: toolCallMessagesAdapt({
userInput: item.value
userInput: item.value,
skip: !hasReadFilesTool
})
};
}
@@ -129,9 +135,10 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
{
obj: ChatRoleEnum.Human,
value: toolCallMessagesAdapt({
skip: !hasReadFilesTool,
userInput: runtimePrompt2ChatsValue({
text: userChatInput,
files: chatValue2RuntimePrompt(query).files
files: userFiles
})
})
}
@@ -185,7 +192,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
// array, replace last element
const lastText = lastMessage.content[lastMessage.content.length - 1];
if (lastText.type === 'text') {
lastMessage.content = replaceVariable(Prompt_Tool_Call, {
lastText.text = replaceVariable(Prompt_Tool_Call, {
question: lastText.text
});
} else {
@@ -237,7 +244,11 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
childTotalPoints: flatUsages.reduce((sum, item) => sum + item.totalPoints, 0),
model: modelName,
query: userChatInput,
historyPreview: getHistoryPreview(GPTMessages2Chats(completeMessages, false), 10000),
historyPreview: getHistoryPreview(
GPTMessages2Chats(completeMessages, false),
10000,
useVision
),
toolDetail: childToolResponse,
mergeSignId: nodeId
},
@@ -253,3 +264,88 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
[DispatchNodeResponseKeyEnum.interactive]: toolWorkflowInteractiveResponse
};
};
const getMultiInput = async ({
histories,
fileLinks,
requestOrigin,
maxFiles,
teamId,
inputFiles
}: {
histories: ChatItemType[];
fileLinks?: string[];
requestOrigin?: string;
maxFiles: number;
teamId: string;
inputFiles: UserChatItemValueItemType['file'][];
}) => {
// Not file quote
if (!fileLinks) {
return {
documentQuoteText: '',
userFiles: inputFiles
};
}
const filesFromHistories = getHistoryFileLinks(histories);
const urls = [...fileLinks, ...filesFromHistories];
if (urls.length === 0) {
return {
documentQuoteText: '',
userFiles: []
};
}
// Get files from histories
const { text } = await getFileContentFromLinks({
// Concat fileUrlList and filesFromHistories; remove not supported files
urls,
requestOrigin,
maxFiles,
teamId
});
return {
documentQuoteText: text,
userFiles: fileLinks.map((url) => parseUrlToFileType(url))
};
};
/*
Tool call auth add file prompt to question。
Guide the LLM to call tool.
*/
const toolCallMessagesAdapt = ({
userInput,
skip
}: {
userInput: UserChatItemValueItemType[];
skip?: boolean;
}) => {
if (skip) return userInput;
const files = userInput.filter((item) => item.type === 'file');
if (files.length > 0) {
return userInput.map((item) => {
if (item.type === 'text') {
const filesCount = files.filter((file) => file.file?.type === 'file').length;
const imgCount = files.filter((file) => file.file?.type === 'image').length;
const text = item.text?.content || '';
return {
...item,
text: {
content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text })
}
};
}
return item;
});
}
return userInput;
};

View File

@@ -29,6 +29,7 @@ import { WorkflowResponseType } from '../../type';
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { i18nT } from '../../../../../../web/i18n/utils';
type FunctionCallCompletion = {
id: string;
@@ -176,17 +177,29 @@ export const runToolWithPromptCall = async (
);
const lastMessage = messages[messages.length - 1];
if (typeof lastMessage.content !== 'string') {
if (typeof lastMessage.content === 'string') {
lastMessage.content = replaceVariable(lastMessage.content, {
toolsPrompt
});
} else if (Array.isArray(lastMessage.content)) {
// array, replace last element
const lastText = lastMessage.content[lastMessage.content.length - 1];
if (lastText.type === 'text') {
lastText.text = replaceVariable(lastText.text, {
toolsPrompt
});
} else {
return Promise.reject('Prompt call invalid input');
}
} else {
return Promise.reject('Prompt call invalid input');
}
lastMessage.content = replaceVariable(lastMessage.content, {
toolsPrompt
});
const filterMessages = await filterGPTMessageByMaxTokens({
messages,
maxTokens: toolModel.maxContext - 500 // filter token. not response maxToken
});
const [requestMessages, max_tokens] = await Promise.all([
loadRequestMessages({
messages: filterMessages,
@@ -398,11 +411,27 @@ export const runToolWithPromptCall = async (
: undefined;
// get the next user prompt
lastMessage.content += `${replaceAnswer}
if (typeof lastMessage.content === 'string') {
lastMessage.content += `${replaceAnswer}
TOOL_RESPONSE: """
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
"""
ANSWER: `;
} else if (Array.isArray(lastMessage.content)) {
// array, replace last element
const lastText = lastMessage.content[lastMessage.content.length - 1];
if (lastText.type === 'text') {
lastText.text += `${replaceAnswer}
TOOL_RESPONSE: """
${workflowInteractiveResponseItem ? `{{${INTERACTIVE_STOP_SIGNAL}}}` : toolsRunResponse.toolResponsePrompt}
"""
ANSWER: `;
} else {
return Promise.reject('Prompt call invalid input');
}
} else {
return Promise.reject('Prompt call invalid input');
}
const runTimes = (response?.runTimes || 0) + toolsRunResponse.toolResponse.runTimes;
const toolNodeTokens = response?.toolNodeTokens ? response.toolNodeTokens + tokens : tokens;
@@ -509,7 +538,7 @@ async function streamResponse({
}
if (!textAnswer) {
return Promise.reject('LLM api response empty');
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
}
return { answer: textAnswer.trim() };
}

View File

@@ -28,6 +28,7 @@ import { addLog } from '../../../../../common/system/log';
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { i18nT } from '../../../../../../web/i18n/utils';
type ToolRunResponseType = {
toolRunResponse: DispatchFlowResponse;
@@ -268,7 +269,7 @@ export const runToolWithToolChoice = async (
},
toolModel
);
// console.log(JSON.stringify(requestMessages, null, 2), '==requestBody');
// console.log(JSON.stringify(requestBody, null, 2), '==requestBody');
/* Run llm */
const ai = getAIApi({
timeout: 480000
@@ -656,7 +657,7 @@ async function streamResponse({
}
if (!textAnswer && toolCalls.length === 0) {
return Promise.reject('LLM api response empty');
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
}
return { answer: textAnswer, toolCalls };

View File

@@ -21,6 +21,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.aiChatTemperature]: number;
[NodeInputKeyEnum.aiChatMaxToken]: number;
[NodeInputKeyEnum.aiChatVision]?: boolean;
[NodeInputKeyEnum.fileUrlList]?: string[];
}> & {
messages: ChatCompletionMessageParam[];
toolNodes: ToolNodeItemType[];

View File

@@ -5,11 +5,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { getAIApi } from '../../../ai/config';
import type {
ChatCompletion,
ChatCompletionMessageParam,
StreamChatType
} from '@fastgpt/global/core/ai/type.d';
import type { ChatCompletion, StreamChatType } from '@fastgpt/global/core/ai/type.d';
import { formatModelChars2Points } from '../../../../support/wallet/usage/utils';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { postTextCensor } from '../../../../common/api/requestPlusApi';
@@ -46,6 +42,9 @@ import { WorkflowResponseType } from '../type';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { AiChatQuoteRoleType } from '@fastgpt/global/core/workflow/template/system/aiChat/type';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { getFileContentFromLinks, getHistoryFileLinks } from '../tools/readFiles';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { i18nT } from '../../../../../web/i18n/utils';
export type ChatProps = ModuleDispatchProps<
AIChatNodeProps & {
@@ -69,7 +68,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
histories,
node: { name },
query,
runningAppInfo: { teamId },
workflowStreamResponse,
chatConfig,
params: {
model,
temperature = 0,
@@ -83,14 +84,12 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
quoteTemplate,
quotePrompt,
aiChatVision,
stringQuoteText
fileUrlList: fileLinks, // node quote file links
stringQuoteText //abandon
}
} = props;
const { files: inputFiles } = chatValue2RuntimePrompt(query);
const { files: inputFiles } = chatValue2RuntimePrompt(query); // Chat box input files
if (!userChatInput && inputFiles.length === 0) {
return Promise.reject('Question is empty');
}
stream = stream && isResponseAnswerText;
const chatHistories = getHistories(history, histories);
@@ -100,11 +99,26 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
return Promise.reject('The chat model is undefined, you need to select a chat model.');
}
const { datasetQuoteText } = await filterDatasetQuote({
quoteQA,
model: modelConstantsData,
quoteTemplate
});
const [{ datasetQuoteText }, { documentQuoteText, userFiles }] = await Promise.all([
filterDatasetQuote({
quoteQA,
model: modelConstantsData,
quoteTemplate
}),
getMultiInput({
histories: chatHistories,
inputFiles,
fileLinks,
stringQuoteText,
requestOrigin,
maxFiles: chatConfig?.fileSelectConfig?.maxFiles || 20,
teamId
})
]);
if (!userChatInput && !documentQuoteText && userFiles.length === 0) {
return Promise.reject(i18nT('chat:AI_input_is_empty'));
}
const [{ filterMessages }] = await Promise.all([
getChatMessages({
@@ -115,9 +129,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
aiChatQuoteRole,
datasetQuotePrompt: quotePrompt,
userChatInput,
inputFiles,
systemPrompt,
stringQuoteText
userFiles,
documentQuoteText
}),
(() => {
// censor model and system key
@@ -132,22 +146,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
})()
]);
// Get the request messages
const concatMessages = [
...(modelConstantsData.defaultSystemChatPrompt
? [
{
role: ChatCompletionRequestMessageRoleEnum.System,
content: modelConstantsData.defaultSystemChatPrompt
}
]
: []),
...filterMessages
] as ChatCompletionMessageParam[];
const [requestMessages, max_tokens] = await Promise.all([
loadRequestMessages({
messages: concatMessages,
messages: filterMessages,
useVision: modelConstantsData.vision && aiChatVision,
origin: requestOrigin
}),
@@ -195,7 +196,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
});
if (!answer) {
throw new Error('LLM model response empty');
return Promise.reject(i18nT('chat:LLM_model_response_empty'));
}
return {
@@ -242,7 +243,11 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
tokens,
query: `${userChatInput}`,
maxToken: max_tokens,
historyPreview: getHistoryPreview(chatCompleteMessages, 10000),
historyPreview: getHistoryPreview(
chatCompleteMessages,
10000,
modelConstantsData.vision && aiChatVision
),
contextTotalLen: completeMessages.length
},
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
@@ -302,7 +307,70 @@ async function filterDatasetQuote({
datasetQuoteText
};
}
async function getMultiInput({
histories,
inputFiles,
fileLinks,
stringQuoteText,
requestOrigin,
maxFiles,
teamId
}: {
histories: ChatItemType[];
inputFiles: UserChatItemValueItemType['file'][];
fileLinks?: string[];
stringQuoteText?: string; // file quote
requestOrigin?: string;
maxFiles: number;
teamId: string;
}) {
// 旧版本适配====>
if (stringQuoteText) {
return {
documentQuoteText: stringQuoteText,
userFiles: inputFiles
};
}
// 没有引用文件参考,但是可能用了图片识别
if (!fileLinks) {
return {
documentQuoteText: '',
userFiles: inputFiles
};
}
// 旧版本适配<====
// If fileLinks params is not empty, it means it is a new version, not get the global file.
// Get files from histories
const filesFromHistories = getHistoryFileLinks(histories);
const urls = [...fileLinks, ...filesFromHistories];
if (urls.length === 0) {
return {
documentQuoteText: '',
userFiles: []
};
}
const { text } = await getFileContentFromLinks({
// Concat fileUrlList and filesFromHistories; remove not supported files
urls,
requestOrigin,
maxFiles,
teamId
});
return {
documentQuoteText: text,
userFiles: fileLinks.map((url) => parseUrlToFileType(url))
};
}
async function getChatMessages({
model,
aiChatQuoteRole,
datasetQuotePrompt = '',
datasetQuoteText,
@@ -310,10 +378,10 @@ async function getChatMessages({
histories = [],
systemPrompt,
userChatInput,
inputFiles,
model,
stringQuoteText
userFiles,
documentQuoteText
}: {
model: LLMModelItemType;
// dataset quote
aiChatQuoteRole: AiChatQuoteRoleType; // user: replace user prompt; system: replace system prompt
datasetQuotePrompt?: string;
@@ -323,10 +391,11 @@ async function getChatMessages({
histories: ChatItemType[];
systemPrompt: string;
userChatInput: string;
inputFiles: UserChatItemValueItemType['file'][];
model: LLMModelItemType;
stringQuoteText?: string; // file quote
userFiles: UserChatItemValueItemType['file'][];
documentQuoteText?: string; // document quote
}) {
// Dataset prompt ====>
// User role or prompt include question
const quoteRole =
aiChatQuoteRole === 'user' || datasetQuotePrompt.includes('{{question}}') ? 'user' : 'system';
@@ -337,6 +406,7 @@ async function getChatMessages({
? Prompt_userQuotePromptList[0].value
: Prompt_systemQuotePromptList[0].value;
// Reset user input, add dataset quote to user input
const replaceInputValue =
useDatasetQuote && quoteRole === 'user'
? replaceVariable(datasetQuotePromptTemplate, {
@@ -344,31 +414,33 @@ async function getChatMessages({
question: userChatInput
})
: userChatInput;
// Dataset prompt <====
const replaceSystemPrompt =
// Concat system prompt
const concatenateSystemPrompt = [
model.defaultSystemChatPrompt,
systemPrompt,
useDatasetQuote && quoteRole === 'system'
? `${systemPrompt ? systemPrompt + '\n\n------\n\n' : ''}${replaceVariable(
datasetQuotePromptTemplate,
{
quote: datasetQuoteText
}
)}`
: systemPrompt;
? replaceVariable(datasetQuotePromptTemplate, {
quote: datasetQuoteText
})
: '',
documentQuoteText
? replaceVariable(Prompt_DocumentQuote, {
quote: documentQuoteText
})
: ''
]
.filter(Boolean)
.join('\n\n===---===---===\n\n');
const messages: ChatItemType[] = [
...getSystemPrompt_ChatItemType(replaceSystemPrompt),
...(stringQuoteText // file quote
? getSystemPrompt_ChatItemType(
replaceVariable(Prompt_DocumentQuote, {
quote: stringQuoteText
})
)
: []),
...getSystemPrompt_ChatItemType(concatenateSystemPrompt),
...histories,
{
obj: ChatRoleEnum.Human,
value: runtimePrompt2ChatsValue({
files: inputFiles,
files: userFiles,
text: replaceInputValue
})
}

View File

@@ -1,17 +1,21 @@
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import type {
DispatchNodeResultType,
ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { datasetSearchResultConcat } from '@fastgpt/global/core/dataset/search/utils';
import { filterSearchResultsByMaxChars } from '../../utils';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
type DatasetConcatProps = ModuleDispatchProps<
{
[NodeInputKeyEnum.datasetMaxTokens]: number;
} & { [key: string]: SearchDataResponseItemType[] }
>;
type DatasetConcatResponse = {
type DatasetConcatResponse = DispatchNodeResultType<{
[NodeOutputKeyEnum.datasetQuoteQA]: SearchDataResponseItemType[];
};
}>;
export async function dispatchDatasetConcat(
props: DatasetConcatProps
@@ -30,6 +34,12 @@ export async function dispatchDatasetConcat(
);
return {
[NodeOutputKeyEnum.datasetQuoteQA]: await filterSearchResultsByMaxChars(rrfConcatResults, limit)
[NodeOutputKeyEnum.datasetQuoteQA]: await filterSearchResultsByMaxChars(
rrfConcatResults,
limit
),
[DispatchNodeResponseKeyEnum.nodeResponse]: {
concatLength: rrfConcatResults.length
}
};
}

View File

@@ -16,6 +16,7 @@ import { datasetSearchQueryExtension } from '../../../dataset/search/utils';
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { checkTeamReRankPermission } from '../../../../support/permission/teamLimit';
import { MongoDataset } from '../../../dataset/schema';
import { i18nT } from '../../../../../web/i18n/utils';
type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType;
@@ -56,15 +57,15 @@ export async function dispatchDatasetSearch(
} = props as DatasetSearchProps;
if (!Array.isArray(datasets)) {
return Promise.reject('Quote type error');
return Promise.reject(i18nT('chat:dataset_quote_type error'));
}
if (datasets.length === 0) {
return Promise.reject('core.chat.error.Select dataset empty');
return Promise.reject(i18nT('common:core.chat.error.Select dataset empty'));
}
if (!userChatInput) {
return Promise.reject('core.chat.error.User input empty');
return Promise.reject(i18nT('common:core.chat.error.User input empty'));
}
// query extension

View File

@@ -23,7 +23,6 @@ import {
} from '@fastgpt/global/core/workflow/node/constant';
import { getNanoid, replaceVariable } from '@fastgpt/global/common/string/tools';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
import { dispatchWorkflowStart } from './init/workflowStart';
import { dispatchChatCompletion } from './chat/oneapi';
@@ -38,11 +37,12 @@ import { dispatchQueryExtension } from './tools/queryExternsion';
import { dispatchRunPlugin } from './plugin/run';
import { dispatchPluginInput } from './plugin/runInput';
import { dispatchPluginOutput } from './plugin/runOutput';
import { removeSystemVariable, valueTypeFormat } from './utils';
import { formatHttpError, removeSystemVariable, valueTypeFormat } from './utils';
import {
filterWorkflowEdges,
checkNodeRunStatus,
textAdaptGptResponse
textAdaptGptResponse,
replaceEditorVariable
} from '@fastgpt/global/core/workflow/runtime/utils';
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { dispatchRunTools } from './agent/runTool/index';
@@ -72,6 +72,7 @@ import { dispatchLoopEnd } from './loop/runLoopEnd';
import { dispatchLoopStart } from './loop/runLoopStart';
import { dispatchFormInput } from './interactive/formInput';
import { dispatchToolParams } from './agent/runTool/toolParams';
import { responseWrite } from '../../../common/response';
const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
@@ -386,6 +387,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
node,
runtimeEdges
});
const nodeRunResult = await (() => {
if (status === 'run') {
nodeRunBeforeHook(node);
@@ -481,8 +483,16 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
: {};
node.inputs.forEach((input) => {
// Special input, not format
if (input.key === dynamicInput?.key) return;
// Skip some special key
if (input.key === NodeInputKeyEnum.childrenNodeIdList) {
params[input.key] = input.value;
return;
}
// replace {{xx}} variables
let value = replaceVariable(input.value, variables);
@@ -505,7 +515,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
if (input.canEdit && dynamicInput && params[dynamicInput.key]) {
params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType);
}
params[input.key] = valueTypeFormat(value, input.valueType);
});
@@ -548,7 +557,21 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
// run module
const dispatchRes: Record<string, any> = await (async () => {
if (callbackMap[node.flowNodeType]) {
return callbackMap[node.flowNodeType](dispatchData);
try {
return await callbackMap[node.flowNodeType](dispatchData);
} catch (error) {
// Get source handles of outgoing edges
const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId);
const skipHandleIds = targetEdges.map((item) => item.sourceHandle);
// Skip all edges and return error
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
error: formatHttpError(error)
},
[DispatchNodeResponseKeyEnum.skipHandleId]: skipHandleIds
};
}
}
return {};
})();

View File

@@ -25,7 +25,7 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
user,
node: { name }
} = props;
const { loopInputArray = [], childrenNodeIdList } = params;
const { loopInputArray = [], childrenNodeIdList = [] } = params;
if (!Array.isArray(loopInputArray)) {
return Promise.reject('Input value is not an array');
@@ -43,23 +43,24 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
let totalPoints = 0;
let newVariables: Record<string, any> = props.variables;
for await (const item of loopInputArray) {
let index = 0;
for await (const item of loopInputArray.filter(Boolean)) {
runtimeNodes.forEach((node) => {
if (
childrenNodeIdList.includes(node.nodeId) &&
node.flowNodeType === FlowNodeTypeEnum.loopStart
) {
node.isEntry = true;
node.inputs = node.inputs.map((input) =>
input.key === NodeInputKeyEnum.loopStartInput
? {
...input,
value: item
}
: input
);
node.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.loopStartInput) {
input.value = item;
} else if (input.key === NodeInputKeyEnum.loopStartIndex) {
input.value = index++;
}
});
}
});
const response = await dispatchWorkFlow({
...props,
runtimeEdges: cloneDeep(runtimeEdges)
@@ -69,11 +70,13 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
(res) => res.moduleType === FlowNodeTypeEnum.loopEnd
)?.loopOutputValue;
// Concat runtime response
outputValueArr.push(loopOutputValue);
loopDetail.push(...response.flowResponses);
assistantResponses.push(...response.assistantResponses);
totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
// Concat new variables
newVariables = {
...newVariables,
...response.newVariables

View File

@@ -7,9 +7,11 @@ import {
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopStartInput]: any;
[NodeInputKeyEnum.loopStartIndex]: number;
}>;
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.loopStartInput]: any;
[NodeOutputKeyEnum.loopStartIndex]: number;
}>;
export const dispatchLoopStart = async (props: Props): Promise<Response> => {
@@ -18,6 +20,7 @@ export const dispatchLoopStart = async (props: Props): Promise<Response> => {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
loopInputValue: params.loopStartInput
},
[NodeOutputKeyEnum.loopStartInput]: params.loopStartInput
[NodeOutputKeyEnum.loopStartInput]: params.loopStartInput,
[NodeOutputKeyEnum.loopStartIndex]: params.loopStartIndex
};
};

View File

@@ -112,7 +112,11 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
output.moduleLogo = plugin.avatar;
}
const usagePoints = await computedPluginUsage(plugin, flowUsages);
const usagePoints = await computedPluginUsage({
plugin,
childrenUsage: flowUsages,
error: !!output?.pluginOutput?.error
});
return {
// 嵌套运行时,如果 childApp stream=false实际上不会有任何内容输出给用户所以不需要存储

View File

@@ -17,12 +17,14 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty
import { authAppByTmbId } from '../../../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppVersionById } from '../../../app/version/controller';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number;
[NodeInputKeyEnum.fileUrlList]?: string[];
[NodeInputKeyEnum.forbidStream]?: boolean;
[NodeInputKeyEnum.fileUrlList]?: string[];
}>;
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.answerText]: string;
@@ -40,8 +42,24 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
variables
} = props;
const { system_forbid_stream = false, userChatInput, history, ...childrenAppVariables } = params;
if (!userChatInput) {
const {
system_forbid_stream = false,
userChatInput,
history,
fileUrlList,
...childrenAppVariables
} = params;
const { files } = chatValue2RuntimePrompt(query);
const userInputFiles = (() => {
if (fileUrlList) {
return fileUrlList.map((url) => parseUrlToFileType(url));
}
// Adapt version 4.8.13 upgrade
return files;
})();
if (!userChatInput && !userInputFiles) {
return Promise.reject('Input is empty');
}
if (!appId) {
@@ -72,7 +90,6 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
}
const chatHistories = getHistories(history, histories);
const { files } = chatValue2RuntimePrompt(query);
// Rewrite children app variables
const systemVariables = filterSystemVariables(variables);
@@ -102,7 +119,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
histories: chatHistories,
variables: childrenRunVariables,
query: runtimePrompt2ChatsValue({
files,
files: userInputFiles,
text: userChatInput
}),
chatConfig

View File

@@ -1,4 +1,5 @@
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
@@ -11,6 +12,26 @@ export const dispatchPluginInput = (props: PluginInputProps) => {
const { params, query } = props;
const { files } = chatValue2RuntimePrompt(query);
/*
对 params 中文件类型数据进行处理
* 插件单独运行时,这里会是一个特殊的数组
* 插件调用的话,这个参数是一个 string[] 不会进行处理
* 硬性要求API 单独调用插件时,要避免这种特殊类型冲突
TODO: 需要 filter max files
*/
for (const key in params) {
const val = params[key];
if (
Array.isArray(val) &&
val.every(
(item) => item.type === ChatFileTypeEnum.file || item.type === ChatFileTypeEnum.image
)
) {
params[key] = val.map((item) => item.url);
}
}
return {
...params,
[DispatchNodeResponseKeyEnum.nodeResponse]: {},

View File

@@ -14,15 +14,17 @@ import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools';
import { addLog } from '../../../../common/system/log';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import {
textAdaptGptResponse,
replaceEditorVariable
} from '@fastgpt/global/core/workflow/runtime/utils';
import { getSystemPluginCb } from '../../../../../plugins/register';
import { ContentTypes } from '@fastgpt/global/core/workflow/constants';
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
import { uploadFile } from '../../../../common/file/gridfs/controller';
import { uploadFileFromBase64Img } from '../../../../common/file/gridfs/controller';
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { createFileToken } from '../../../../support/permission/controller';
import { removeFilesByPaths } from '../../../../common/file/utils';
import { JSONPath } from 'jsonpath-plus';
import type { SystemPluginSpecialResponse } from '../../../../../plugins/type';
type PropsArrType = {
key: string;
@@ -235,7 +237,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
node.outputs
.filter(
(item) =>
item.key !== NodeOutputKeyEnum.error && item.key !== NodeOutputKeyEnum.httpRawResponse
item.id !== NodeOutputKeyEnum.error &&
item.id !== NodeOutputKeyEnum.httpRawResponse &&
item.id !== NodeOutputKeyEnum.addOutputParam
)
.forEach((item) => {
const key = item.key.startsWith('$') ? item.key : `$.${item.key}`;
@@ -376,27 +380,25 @@ async function replaceSystemPluginResponse({
tmbId: string;
}) {
for await (const key of Object.keys(response)) {
if (typeof response[key] === 'object' && response[key].type === 'SYSTEM_PLUGIN_FILE') {
const fileObj = response[key];
const filename = fileObj.path.split('/').pop() || `${tmbId}-${Date.now()}`;
if (typeof response[key] === 'object' && response[key].type === 'SYSTEM_PLUGIN_BASE64') {
const fileObj = response[key] as SystemPluginSpecialResponse;
const filename = `${tmbId}-${Date.now()}.${fileObj.extension}`;
try {
const fileId = await uploadFile({
const fileId = await uploadFileFromBase64Img({
teamId,
tmbId,
bucketName: 'chat',
path: fileObj.path,
base64: fileObj.value,
filename,
contentType: fileObj.contentType,
metadata: {}
});
response[key] = `${ReadFileBaseUrl}?filename=${filename}&token=${await createFileToken({
response[key] = `${ReadFileBaseUrl}/${filename}?token=${await createFileToken({
bucketName: 'chat',
teamId,
tmbId,
fileId
})}`;
} catch (error) {}
removeFilesByPaths([fileObj.path]);
}
}
return response;

View File

@@ -2,16 +2,15 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { documentFileType } from '@fastgpt/global/common/file/constants';
import axios from 'axios';
import { serverRequestBaseUrl } from '../../../../common/api/serverRequest';
import { MongoRawTextBuffer } from '../../../../common/buffer/rawText/schema';
import { readFromSecondary } from '../../../../common/mongo/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
import { detectFileEncoding, parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { readRawContentByFileBuffer } from '../../../../common/file/read/utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
type Props = ModuleDispatchProps<{
@@ -48,12 +47,41 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
runningAppInfo: { teamId },
histories,
chatConfig,
node: { version },
params: { fileUrlList = [] }
} = props;
const maxFiles = chatConfig?.fileSelectConfig?.maxFiles || 20;
// Get files from histories
const filesFromHistories = histories
const filesFromHistories = version !== '489' ? [] : getHistoryFileLinks(histories);
const { text, readFilesResult } = await getFileContentFromLinks({
// Concat fileUrlList and filesFromHistories; remove not supported files
urls: [...fileUrlList, ...filesFromHistories],
requestOrigin,
maxFiles,
teamId
});
return {
[NodeOutputKeyEnum.text]: text,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
readFiles: readFilesResult.map((item) => ({
name: item?.filename || '',
url: item?.url || ''
})),
readFilesResult: readFilesResult
.map((item) => item?.nodeResponsePreviewText ?? '')
.join('\n******\n')
},
[DispatchNodeResponseKeyEnum.toolResponses]: {
fileContent: text
}
};
};
export const getHistoryFileLinks = (histories: ChatItemType[]) => {
return histories
.filter((item) => {
if (item.obj === ChatRoleEnum.Human) {
return item.value.filter((value) => value.type === 'file');
@@ -70,28 +98,38 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
return files;
})
.flat();
};
// Concat fileUrlList and filesFromHistories; remove not supported files
const parseUrlList = [...fileUrlList, ...filesFromHistories]
export const getFileContentFromLinks = async ({
urls,
requestOrigin,
maxFiles,
teamId
}: {
urls: string[];
requestOrigin?: string;
maxFiles: number;
teamId: string;
}) => {
const parseUrlList = urls
// Remove invalid urls
.filter((url) => {
if (typeof url !== 'string') return false;
// 检查相对路径
const validPrefixList = ['/', 'http', 'ws'];
if (validPrefixList.some((prefix) => url.startsWith(prefix))) {
return true;
}
return false;
})
// Just get the document type file
.filter((url) => parseUrlToFileType(url)?.type === 'file')
.map((url) => {
try {
// Avoid "/api/xxx" file error.
const origin = requestOrigin ?? 'http://localhost:3000';
// Check is system upload file
if (url.startsWith('/') || (requestOrigin && url.startsWith(requestOrigin))) {
// Parse url, get filename query. Keep only documents that can be parsed
const parseUrl = new URL(url, origin);
const filenameQuery = parseUrl.searchParams.get('filename');
// Not document
if (filenameQuery) {
const extensionQuery = filenameQuery.split('.').pop()?.toLowerCase() || '';
if (!documentFileType.includes(extensionQuery)) {
return '';
}
}
// Remove the origin(Make intranet requests directly)
if (requestOrigin && url.startsWith(requestOrigin)) {
url = url.replace(requestOrigin, '');
@@ -123,7 +161,7 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
}
try {
// Get file buffer
// Get file buffer data
const response = await axios.get(url, {
baseURL: serverRequestBaseUrl,
responseType: 'arraybuffer'
@@ -197,18 +235,7 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
const text = readFilesResult.map((item) => item?.text ?? '').join('\n******\n');
return {
[NodeOutputKeyEnum.text]: text,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
readFiles: readFilesResult.map((item) => ({
name: item?.filename || '',
url: item?.url || ''
})),
readFilesResult: readFilesResult
.map((item) => item?.nodeResponsePreviewText ?? '')
.join('\n******\n')
},
[DispatchNodeResponseKeyEnum.toolResponses]: {
fileContent: text
}
text,
readFilesResult
};
};

View File

@@ -4,11 +4,14 @@ import {
SseResponseEventEnum
} from '@fastgpt/global/core/workflow/runtime/constants';
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
import {
getReferenceVariableValue,
replaceEditorVariable
} from '@fastgpt/global/core/workflow/runtime/utils';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import { removeSystemVariable, valueTypeFormat } from '../utils';
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.updateList]: TUpdateListItem[];
@@ -19,15 +22,24 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
const { params, variables, runtimeNodes, workflowStreamResponse, node } = props;
const { updateList } = params;
const result = updateList.map((item) => {
const varNodeId = item.variable?.[0];
const varKey = item.variable?.[1];
const nodeIds = runtimeNodes.map((node) => node.nodeId);
if (!varNodeId || !varKey) {
const result = updateList.map((item) => {
const variable = item.variable;
if (!isValidReferenceValue(variable, nodeIds)) {
return null;
}
const varNodeId = variable[0];
const varKey = variable[1];
if (!varKey) {
return null;
}
const value = (() => {
// If first item is empty, it means it is a input value
if (!item.value?.[0]) {
const formatValue = valueTypeFormat(item.value?.[1], item.valueType);
@@ -48,6 +60,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
}
})();
// Update node output
// Global variable
if (varNodeId === VARIABLE_NODE_ID) {
variables[varKey] = value;
@@ -72,6 +85,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
});
return {
[DispatchNodeResponseKeyEnum.newVariables]: variables,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
updateVarResult: result
}

View File

@@ -1,14 +1,5 @@
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { countPromptTokens } from '../../common/string/tiktoken/index';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import {
getPluginInputsFromStoreNodes,
getPluginRunContent
} from '@fastgpt/global/core/app/plugin/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { RuntimeUserPromptType, UserChatItemType } from '@fastgpt/global/core/chat/type';
import { runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt';
/* filter search result */
export const filterSearchResultsByMaxChars = async (

View File

@@ -9,3 +9,12 @@ export const getUserFingerprint = async () => {
export const hasHttps = () => {
return window.location.protocol === 'https:';
};
export const getWebReqUrl = (url: string = '') => {
if (!url) return '/';
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
if (!baseUrl) return url;
if (!url.startsWith('/') || url.startsWith(baseUrl)) return url;
return `${baseUrl}${url}`;
};

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Box, Flex, Image } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import type { ImageProps } from '@chakra-ui/react';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import MyIcon from '../Icon';
import { iconPaths } from '../Icon/constants';
import MyImage from '../Image/MyImage';
const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
// @ts-ignore
@@ -14,7 +15,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
<MyIcon name={src as any} w={w} borderRadius={props.borderRadius} />
</Box>
) : (
<Image
<MyImage
fallbackSrc={LOGO_ICON}
fallbackStrategy={'onError'}
objectFit={'contain'}

View File

@@ -4,6 +4,7 @@ export const iconPaths = {
book: () => import('./icons/book.svg'),
change: () => import('./icons/change.svg'),
chatSend: () => import('./icons/chatSend.svg'),
check: () => import('./icons/check.svg'),
closeSolid: () => import('./icons/closeSolid.svg'),
collectionLight: () => import('./icons/collectionLight.svg'),
collectionSolid: () => import('./icons/collectionSolid.svg'),
@@ -59,7 +60,6 @@ export const iconPaths = {
'common/playFill': () => import('./icons/common/playFill.svg'),
'common/playLight': () => import('./icons/common/playLight.svg'),
'common/publishFill': () => import('./icons/common/publishFill.svg'),
'common/questionLight': () => import('./icons/common/questionLight.svg'),
'common/refreshLight': () => import('./icons/common/refreshLight.svg'),
'common/resultLight': () => import('./icons/common/resultLight.svg'),
'common/retryLight': () => import('./icons/common/retryLight.svg'),
@@ -217,6 +217,7 @@ export const iconPaths = {
'core/workflow/template/FileRead': () => import('./icons/core/workflow/template/FileRead.svg'),
'core/workflow/template/aiChat': () => import('./icons/core/workflow/template/aiChat.svg'),
'core/workflow/template/baseChart': () => import('./icons/core/workflow/template/baseChart.svg'),
'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'),
'core/workflow/template/codeRun': () => import('./icons/core/workflow/template/codeRun.svg'),
'core/workflow/template/customFeedback': () =>
import('./icons/core/workflow/template/customFeedback.svg'),
@@ -224,18 +225,16 @@ export const iconPaths = {
import('./icons/core/workflow/template/datasetConcat.svg'),
'core/workflow/template/datasetSearch': () =>
import('./icons/core/workflow/template/datasetSearch.svg'),
'core/workflow/template/datasource': () =>
import('./icons/core/workflow/template/datasource.svg'),
'core/workflow/template/duckduckgo': () =>
import('./icons/core/workflow/template/duckduckgo.svg'),
'core/workflow/template/extractJson': () =>
import('./icons/core/workflow/template/extractJson.svg'),
'core/workflow/template/wiki': () => import('./icons/core/workflow/template/wiki.svg'),
'core/workflow/template/datasource': () =>
import('./icons/core/workflow/template/datasource.svg'),
'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'),
'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'),
'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'),
'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'),
'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'),
'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'),
'core/workflow/template/httpRequest': () =>
import('./icons/core/workflow/template/httpRequest.svg'),
'core/workflow/template/ifelse': () => import('./icons/core/workflow/template/ifelse.svg'),
@@ -271,6 +270,7 @@ export const iconPaths = {
'core/workflow/template/variable': () => import('./icons/core/workflow/template/variable.svg'),
'core/workflow/template/variableUpdate': () =>
import('./icons/core/workflow/template/variableUpdate.svg'),
'core/workflow/template/wiki': () => import('./icons/core/workflow/template/wiki.svg'),
'core/workflow/template/workflowStart': () =>
import('./icons/core/workflow/template/workflowStart.svg'),
'core/workflow/touchTable': () => import('./icons/core/workflow/touchTable.svg'),
@@ -280,6 +280,7 @@ export const iconPaths = {
date: () => import('./icons/date.svg'),
delete: () => import('./icons/delete.svg'),
drag: () => import('./icons/drag.svg'),
edgeAdd: () => import('./icons/edgeAdd.svg'),
edit: () => import('./icons/edit.svg'),
empty: () => import('./icons/empty.svg'),
export: () => import('./icons/export.svg'),
@@ -302,6 +303,7 @@ export const iconPaths = {
'file/pdf': () => import('./icons/file/pdf.svg'),
'file/qaImport': () => import('./icons/file/qaImport.svg'),
'file/uploadFile': () => import('./icons/file/uploadFile.svg'),
help: () => import('./icons/help.svg'),
history: () => import('./icons/history.svg'),
infoRounded: () => import('./icons/infoRounded.svg'),
kbTest: () => import('./icons/kbTest.svg'),
@@ -331,7 +333,6 @@ export const iconPaths = {
save: () => import('./icons/save.svg'),
stop: () => import('./icons/stop.svg'),
'support/account/loginoutLight': () => import('./icons/support/account/loginoutLight.svg'),
'support/account/passwordLogin': () => import('./icons/support/account/passwordLogin.svg'),
'support/account/plans': () => import('./icons/support/account/plans.svg'),
'support/account/promotionLight': () => import('./icons/support/account/promotionLight.svg'),
'support/bill/extraDatasetsize': () => import('./icons/support/bill/extraDatasetsize.svg'),

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 17" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5587 3.69438C13.9492 3.30386 14.5824 3.30386 14.9729 3.69438C15.3634 4.08491 15.3634 4.71807 14.9729 5.1086L7.63956 12.4419C7.24904 12.8325 6.61587 12.8325 6.22535 12.4419L2.89201 9.1086C2.50149 8.71807 2.50149 8.08491 2.89201 7.69438C3.28254 7.30386 3.9157 7.30386 4.30623 7.69438L6.93245 10.3206L13.5587 3.69438Z"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@@ -1 +1,4 @@
<?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="1704259732773" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4183" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M319.20128 974.56128L348.16 1003.52l655.36-655.36-28.95872-28.95872-655.36 655.36zM675.84 1003.52l327.68-327.68-28.95872-28.95872-327.68 327.68L675.84 1003.52z" fill="#000000" p-id="4184"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9" fill="none">
<path d="M0.950928 8.44922L8.32385 1.07629" stroke="#8A95A7" stroke-linecap="round"/>
<path d="M4.3418 8.46167L8.32373 4.47974" stroke="#8A95A7" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -1,6 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
<path
d="M4.25845 0.738983C4.25845 0.606996 4.15146 0.5 4.01947 0.5C2.11725 0.5 0.575195 2.04205 0.575195 3.94428V12.0557C0.575195 13.9579 2.11725 15.5 4.01947 15.5H12.1309C14.0331 15.5 15.5752 13.9579 15.5752 12.0557V3.94428C15.5752 2.04205 14.0331 0.5 12.1309 0.5H10.4017C9.98744 0.5 9.65166 0.835786 9.65166 1.25C9.65166 1.66421 9.98744 2 10.4017 2H12.1309C13.2047 2 14.0752 2.87048 14.0752 3.94428V12.0557C14.0752 13.1295 13.2047 14 12.1309 14H4.01947C2.94568 14 2.0752 13.1295 2.0752 12.0557V3.94428C2.0752 2.87048 2.94568 2 4.01947 2C4.15146 2 4.25845 1.893 4.25845 1.76102V0.738983Z" />
<path
d="M7.38092 4.3543C7.26179 3.52006 7.01223 2.99621 6.59273 2.70009C5.59427 1.99531 4.85314 2.00002 4.74413 2.00072L4.7369 2.00075H3.9869V0.500749H4.7369C5.00097 0.500749 6.0952 0.512848 7.45775 1.47464C8.37881 2.12479 8.7247 3.15378 8.86586 4.14224C8.98099 4.94841 8.97457 5.85452 8.96865 6.68934C8.96737 6.86971 8.96612 7.04676 8.96612 7.21874C8.96612 7.70483 8.95571 8.16141 8.94035 8.57069L9.57153 7.93951C9.86442 7.64661 10.3393 7.64661 10.6322 7.93951C10.9251 8.2324 10.9251 8.70727 10.6322 9.00017L8.65543 10.9769C8.49629 11.1361 8.28342 11.2087 8.07521 11.1949C7.86701 11.2087 7.65414 11.1361 7.495 10.9769L5.51824 9.00017C5.22535 8.70727 5.22535 8.2324 5.51824 7.93951C5.81113 7.64661 6.28601 7.64661 6.5789 7.93951L7.33793 8.69854L7.42997 8.79058C7.45074 8.33162 7.46612 7.79657 7.46612 7.21874C7.46612 7.02039 7.46735 6.82532 7.46856 6.63372C7.47382 5.80363 7.47866 5.03868 7.38092 4.3543Z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M4.67391 1.54593C4.67391 1.4286 4.5788 1.3335 4.46148 1.3335C2.77062 1.3335 1.3999 2.70421 1.3999 4.39507V11.6053C1.3999 13.2961 2.77062 14.6668 4.46148 14.6668H11.6717C13.3625 14.6668 14.7332 13.2961 14.7332 11.6053V4.39507C14.7332 2.70421 13.3625 1.3335 11.6717 1.3335H10.1345C9.76634 1.3335 9.46786 1.63197 9.46786 2.00016C9.46786 2.36835 9.76634 2.66683 10.1345 2.66683H11.6717C12.6261 2.66683 13.3999 3.44059 13.3999 4.39507V11.6053C13.3999 12.5597 12.6261 13.3335 11.6717 13.3335H4.46148C3.507 13.3335 2.73324 12.5597 2.73324 11.6053V4.39507C2.73324 3.44059 3.507 2.66683 4.46148 2.66683C4.5788 2.66683 4.67391 2.57172 4.67391 2.4544V1.54593Z"/>
<path d="M7.44944 4.75954C7.34354 4.01799 7.12171 3.55235 6.74882 3.28913C5.8613 2.66266 5.20252 2.66685 5.10562 2.66747L5.0992 2.6675H4.43253V1.33416H5.0992C5.33393 1.33416 6.30657 1.34492 7.51773 2.19984C8.33645 2.77775 8.6439 3.69241 8.76938 4.57104C8.87172 5.28764 8.86601 6.09307 8.86075 6.83513C8.85961 6.99546 8.8585 7.15284 8.8585 7.30571C8.8585 7.73778 8.84925 8.14363 8.83559 8.50744L9.39664 7.94639C9.65699 7.68604 10.0791 7.68604 10.3395 7.94639C10.5998 8.20674 10.5998 8.62885 10.3395 8.8892L8.58233 10.6463C8.44087 10.7878 8.25166 10.8524 8.06659 10.8401C7.88151 10.8524 7.6923 10.7878 7.55084 10.6463L5.79372 8.8892C5.53337 8.62885 5.53337 8.20674 5.79372 7.94639C6.05407 7.68604 6.47618 7.68604 6.73653 7.94639L7.41122 8.62109L7.49304 8.7029C7.5115 8.29493 7.52517 7.81934 7.52517 7.30571C7.52517 7.1294 7.52626 6.956 7.52734 6.78569C7.53201 6.04784 7.53631 5.36788 7.44944 4.75954Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.99997 2.196C5.24222 2.196 2.19596 5.24225 2.19596 9C2.19596 12.7577 5.24222 15.804 8.99997 15.804C12.7577 15.804 15.804 12.7577 15.804 9C15.804 5.24225 12.7577 2.196 8.99997 2.196ZM0.529297 9C0.529297 4.32178 4.32174 0.529331 8.99997 0.529331C13.6782 0.529331 17.4706 4.32178 17.4706 9C17.4706 13.6782 13.6782 17.4707 8.99997 17.4707C4.32174 17.4707 0.529297 13.6782 0.529297 9ZM9.18533 6.03224C8.846 5.97403 8.49702 6.0378 8.20019 6.21224C7.90337 6.38669 7.67786 6.66056 7.56361 6.98534C7.41089 7.41949 6.93512 7.64764 6.50097 7.49491C6.06681 7.34218 5.83866 6.86642 5.99139 6.43226C6.23625 5.73619 6.71956 5.14923 7.35572 4.77536C7.99188 4.40148 8.73983 4.26481 9.4671 4.38956C10.1944 4.5143 10.854 4.89241 11.3292 5.45692C11.8043 6.0213 12.0644 6.73558 12.0634 7.4733C12.063 8.67899 11.1697 9.46897 10.5467 9.88431C10.2098 10.1089 9.8788 10.2738 9.63531 10.382C9.51239 10.4367 9.4088 10.4782 9.33398 10.5067C9.2965 10.5209 9.26605 10.532 9.24377 10.54L9.21658 10.5495L9.20781 10.5525L9.20467 10.5535L9.20342 10.554C9.20317 10.554 9.20239 10.5543 8.93887 9.76373L9.20239 10.5543C8.76577 10.6998 8.29384 10.4639 8.1483 10.0273C8.00281 9.59078 8.23857 9.11902 8.67491 8.97331L8.68542 8.9696C8.69671 8.96559 8.71548 8.95878 8.74065 8.94919C8.79114 8.92996 8.86654 8.89986 8.95842 8.85902C9.14454 8.7763 9.38634 8.65481 9.62222 8.49756C10.1447 8.14925 10.3967 7.79388 10.3967 7.47253L10.3967 7.47129C10.3972 7.127 10.2759 6.79364 10.0542 6.53025C9.83245 6.26686 9.52467 6.09044 9.18533 6.03224ZM8.16663 12.8187C8.16663 12.3584 8.53973 11.9853 8.99997 11.9853H9.0076C9.46784 11.9853 9.84094 12.3584 9.84094 12.8187C9.84094 13.2789 9.46784 13.652 9.0076 13.652H8.99997C8.53973 13.652 8.16663 13.2789 8.16663 12.8187Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5.42469 4.94872C5.16434 5.20906 5.16434 5.63117 5.42469 5.89152C5.68504 6.15187 6.10715 6.15187 6.3675 5.89152L8.00002 4.25901L9.63253 5.89152C9.89288 6.15187 10.315 6.15187 10.5753 5.89152C10.8357 5.63117 10.8357 5.20906 10.5753 4.94872L8.47142 2.8448C8.21107 2.58445 7.78896 2.58445 7.52861 2.8448L5.42469 4.94872ZM5.42469 10.8375C5.16434 11.0979 5.16434 11.52 5.42469 11.7803L7.52861 13.8843C7.56115 13.9168 7.59623 13.9453 7.63319 13.9697C7.89196 14.1405 8.24361 14.1121 8.47142 13.8843L10.5753 11.7803C10.8357 11.52 10.8357 11.0979 10.5753 10.8375C10.315 10.5772 9.89288 10.5772 9.63253 10.8375L8.00002 12.47L6.3675 10.8375C6.10715 10.5772 5.68504 10.5772 5.42469 10.8375Z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.42475 4.58336C5.1644 4.84371 5.1644 5.26582 5.42475 5.52617C5.6851 5.78652 6.10721 5.78652 6.36756 5.52617L8.00008 3.89366L9.63259 5.52617C9.89294 5.78652 10.315 5.78652 10.5754 5.52617C10.8357 5.26582 10.8357 4.84371 10.5754 4.58336L8.47148 2.47944C8.21113 2.21909 7.78902 2.21909 7.52867 2.47944L5.42475 4.58336ZM5.42475 10.4722C5.1644 10.7325 5.1644 11.1546 5.42475 11.415L7.52867 13.5189C7.56122 13.5514 7.59629 13.5799 7.63325 13.6043C7.89202 13.7752 8.24367 13.7467 8.47148 13.5189L10.5754 11.415C10.8357 11.1546 10.8357 10.7325 10.5754 10.4722C10.315 10.2118 9.89294 10.2118 9.63259 10.4722L8.00008 12.1047L6.36756 10.4722C6.10721 10.2118 5.6851 10.2118 5.42475 10.4722Z" />
</svg>

Before

Width:  |  Height:  |  Size: 823 B

After

Width:  |  Height:  |  Size: 804 B

View File

@@ -1,11 +1,10 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_10832_16007)"/>
<path d="M13.9673 10.3122C13.1868 10.8479 13.1935 11.9491 13.863 12.6185C14.3986 13.1541 15.2613 13.1384 15.9008 12.7325C16.5477 12.3219 17.268 12.0318 18.0272 11.8808C19.2374 11.6401 20.4919 11.7636 21.6319 12.2358C22.7719 12.708 23.7463 13.5077 24.4318 14.5337C25.1174 15.5597 25.4833 16.7659 25.4833 17.9999C25.4833 19.2338 25.1174 20.4401 24.4318 21.4661C23.7463 22.4921 22.7719 23.2917 21.6319 23.7639C20.4919 24.2361 19.2374 24.3597 18.0272 24.119C17.268 23.968 16.5477 23.6779 15.9008 23.2673C15.2613 22.8614 14.3986 22.8457 13.863 23.3812C13.1935 24.0507 13.1868 25.1518 13.9673 25.6876C15.0044 26.3995 16.1799 26.8976 17.4252 27.1453C19.234 27.5051 21.1088 27.3204 22.8127 26.6147C24.5165 25.9089 25.9728 24.7138 26.9974 23.1803C28.022 21.6469 28.5689 19.8441 28.5689 17.9999C28.5689 16.1557 28.022 14.3528 26.9974 12.8194C25.9728 11.286 24.5165 10.0908 22.8127 9.38509C21.1088 8.67933 19.234 8.49468 17.4252 8.85447C16.1799 9.10217 15.0044 9.60029 13.9673 10.3122Z" fill="white"/>
<path d="M19.2443 22.5497C17.3579 22.5497 15.7397 21.4017 15.0501 19.7663H9.19726C8.22171 19.7663 7.43087 18.9754 7.43087 17.9999C7.43087 17.0243 8.22171 16.2335 9.19726 16.2335H15.0501C15.7397 14.598 17.3579 13.45 19.2443 13.45C21.7571 13.45 23.7942 15.4871 23.7942 17.9999C23.7942 20.5127 21.7571 22.5497 19.2443 22.5497Z" fill="white"/>
<rect width="36" height="36" fill="url(#paint0_linear_11_1507)"/>
<path d="M9.22505 16.905C8.38096 17.102 7.87904 17.9714 8.13047 18.8009L9.96306 24.8467C10.3439 26.1032 12.0411 26.2986 12.6976 25.1616L13.5842 23.6259C15.0589 24.1044 16.6267 24.2674 18.1843 24.0937C20.4842 23.8372 22.6439 22.8596 24.3544 21.301C26.0649 19.7424 27.2384 17.6826 27.7071 15.4164C28.074 13.6422 27.9941 11.8127 27.4853 10.0898C27.2506 9.29527 26.3514 8.96094 25.5963 9.30173L23.6663 10.1728C22.9113 10.5136 22.597 11.404 22.7351 12.2208C22.8554 12.9325 22.8437 13.6647 22.6957 14.38C22.4458 15.5886 21.8199 16.6872 20.9077 17.5184C19.9954 18.3497 18.8436 18.871 17.617 19.0078C17.1616 19.0586 16.7045 19.0555 16.2549 19.0002L17.0171 17.6799C17.6736 16.5428 16.6558 15.1707 15.3772 15.4692L9.22505 16.905Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_10832_16007" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#68C0FF"/>
<stop offset="1" stop-color="#52A2FF"/>
<linearGradient id="paint0_linear_11_1507" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF8BFD"/>
<stop offset="1" stop-color="#7394FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,11 +1,10 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_10829_15860)"/>
<path d="M22.0325 10.3122C22.813 10.8479 22.8062 11.9491 22.1368 12.6185C21.6012 13.1541 20.7384 13.1384 20.099 12.7325C19.4521 12.3219 18.7317 12.0318 17.9726 11.8808C16.7623 11.6401 15.5079 11.7636 14.3679 12.2358C13.2279 12.708 12.2535 13.5077 11.5679 14.5337C10.8824 15.5597 10.5165 16.7659 10.5165 17.9999C10.5165 19.2338 10.8824 20.4401 11.5679 21.4661C12.2535 22.4921 13.2279 23.2917 14.3679 23.7639C15.5079 24.2361 16.7624 24.3597 17.9726 24.119C18.7317 23.968 19.4521 23.6779 20.099 23.2673C20.7384 22.8614 21.6012 22.8457 22.1368 23.3812C22.8062 24.0507 22.813 25.1518 22.0325 25.6876C20.9954 26.3995 19.8199 26.8976 18.5746 27.1453C16.7658 27.5051 14.8909 27.3204 13.1871 26.6147C11.4832 25.9089 10.0269 24.7138 9.00232 23.1803C7.97772 21.6469 7.43085 19.8441 7.43085 17.9999C7.43085 16.1557 7.97772 14.3528 9.00232 12.8194C10.0269 11.286 11.4832 10.0908 13.1871 9.38509C14.8909 8.67933 16.7658 8.49468 18.5746 8.85447C19.8199 9.10217 20.9954 9.60029 22.0325 10.3122Z" fill="white"/>
<path d="M16.7554 22.5497C18.6418 22.5497 20.2601 21.4017 20.9497 19.7663H26.8025C27.778 19.7663 28.5689 18.9754 28.5689 17.9999C28.5689 17.0243 27.778 16.2335 26.8025 16.2335H20.9497C20.2601 14.598 18.6418 13.45 16.7554 13.45C14.2426 13.45 12.2056 15.4871 12.2056 17.9999C12.2056 20.5127 14.2426 22.5497 16.7554 22.5497Z" fill="white"/>
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_11_1494)"/>
<path d="M26.7748 18.1758C27.6189 17.9788 28.1208 17.1095 27.8693 16.2799L26.0368 10.2341C25.6559 8.97762 23.9587 8.7822 23.3022 9.91926L22.4156 11.4549C20.9409 10.9764 19.3731 10.8134 17.8155 10.9871C15.5157 11.2437 13.3559 12.2212 11.6454 13.7798C9.93493 15.3384 8.76137 17.3983 8.29272 19.6644C7.92581 21.4386 8.00571 23.2681 8.51454 24.991C8.74918 25.7855 9.64845 26.1199 10.4035 25.7791L12.3335 24.908C13.0886 24.5672 13.4028 23.6768 13.2647 22.86C13.1444 22.1483 13.1562 21.4161 13.3041 20.7008C13.554 19.4922 14.1799 18.3936 15.0922 17.5624C16.0044 16.7311 17.1562 16.2098 18.3828 16.073C18.8382 16.0222 19.2953 16.0254 19.7449 16.0807L18.9827 17.4009C18.3262 18.538 19.344 19.9101 20.6226 19.6117L26.7748 18.1758Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_10829_15860" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#68C0FF"/>
<stop offset="1" stop-color="#52A2FF"/>
<linearGradient id="paint0_linear_11_1494" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF8BFD"/>
<stop offset="1" stop-color="#7394FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 10 9" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09993 1.21182C6.09993 0.604303 5.60744 0.111816 4.99993 0.111816C4.39242 0.111816 3.89993 0.604303 3.89993 1.21182L3.89993 3.11182H1.9999C1.39239 3.11182 0.899902 3.6043 0.899902 4.21182C0.899902 4.81933 1.39239 5.31182 1.9999 5.31182H3.89993L3.89993 7.21182C3.89993 7.81933 4.39242 8.31182 4.99993 8.31182C5.60744 8.31182 6.09993 7.81933 6.09993 7.21182V5.31182H7.9999C8.60742 5.31182 9.0999 4.81933 9.0999 4.21182C9.0999 3.6043 8.60742 3.11182 7.9999 3.11182H6.09993V1.21182Z" />
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99968 2.55678C4.99348 2.55678 2.55648 4.99379 2.55648 7.99998C2.55648 11.0062 4.99348 13.4432 7.99968 13.4432C11.0059 13.4432 13.4429 11.0062 13.4429 7.99998C13.4429 4.99379 11.0059 2.55678 7.99968 2.55678ZM1.22314 7.99998C1.22314 4.25741 4.2571 1.22345 7.99968 1.22345C11.7423 1.22345 14.7762 4.25741 14.7762 7.99998C14.7762 11.7426 11.7423 14.7765 7.99968 14.7765C4.2571 14.7765 1.22314 11.7426 1.22314 7.99998ZM8.14797 5.62577C7.87651 5.57921 7.59732 5.63022 7.35986 5.76978C7.1224 5.90934 6.942 6.12843 6.8506 6.38825C6.72842 6.73558 6.34781 6.9181 6.00048 6.79591C5.65315 6.67373 5.47064 6.29312 5.59282 5.9458C5.78871 5.38894 6.17536 4.91937 6.68428 4.62027C7.19321 4.32117 7.79157 4.21184 8.37338 4.31163C8.9552 4.41143 9.48292 4.71392 9.86308 5.16552C10.2432 5.61702 10.4512 6.18845 10.4504 6.77862C10.4501 7.74318 9.73548 8.37516 9.23708 8.70743C8.96754 8.88713 8.70274 9.01905 8.50796 9.10562C8.40962 9.14933 8.32674 9.18252 8.26689 9.20532C8.23691 9.21674 8.21254 9.22562 8.19473 9.23195L8.17297 9.23957L8.16595 9.24197L8.16345 9.24282L8.16245 9.24315C8.16225 9.24322 8.16162 9.24343 7.9508 8.61097L8.16162 9.24343C7.81232 9.35986 7.43478 9.17109 7.31835 8.82179C7.20195 8.47261 7.39056 8.0952 7.73963 7.97863L7.74805 7.97567C7.75708 7.97246 7.77209 7.96701 7.79223 7.95934C7.83262 7.94395 7.89294 7.91987 7.96644 7.8872C8.11534 7.82103 8.30878 7.72383 8.49748 7.59803C8.91545 7.31938 9.11709 7.03509 9.11709 6.77801L9.1171 6.77702C9.11751 6.50159 9.02042 6.2349 8.84305 6.02419C8.66567 5.81347 8.41944 5.67234 8.14797 5.62577ZM7.33301 11.0549C7.33301 10.6867 7.63149 10.3883 7.99968 10.3883H8.00579C8.37398 10.3883 8.67246 10.6867 8.67246 11.0549C8.67246 11.4231 8.37398 11.7216 8.00579 11.7216H7.99968C7.63149 11.7216 7.33301 11.4231 7.33301 11.0549Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,6 +0,0 @@
<svg t="1709471698048" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4242"
width="128" height="128">
<path
d="M855.158154 945.664H168.999385c-28.081231 0-50.845538-22.843077-50.845539-51.003077V486.833231C118.153846 458.673231 129.457231 433.230769 157.538462 433.230769h708.923076c28.081231 0 39.502769 25.442462 39.50277 53.602462v407.827692c0 28.16-22.764308 51.003077-50.806154 51.003077z m-340.913231-376.595692a99.761231 99.761231 0 0 0-99.603692 99.958154c0 40.251077 23.827692 74.712615 57.974154 90.54523V827.076923a39.384615 39.384615 0 0 0 78.76923 0v-65.417846a99.879385 99.879385 0 0 0 62.424616-92.632615 99.761231 99.761231 0 0 0-99.564308-99.958154z m0.551385-396.524308c-104.841846 0-189.794462 81.329231-197.159385 184.123077H217.718154C229.060923 201.334154 358.321231 78.769231 516.489846 78.769231s287.428923 122.564923 298.732308 277.897846h-103.266462c-7.364923-102.793846-92.317538-184.123077-197.159384-184.123077z"
fill="#3B9BF8" p-id="4243"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { Image, ImageProps } from '@chakra-ui/react';
import { getWebReqUrl } from '../../../common/system/utils';
const MyImage = (props: ImageProps) => {
return <Image {...props} src={getWebReqUrl(props.src)} alt={props.alt || ''} />;
};
export default React.memo(MyImage);

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { PhotoProvider, PhotoView } from 'react-photo-view';
import 'react-photo-view/dist/react-photo-view.css';
import { Box, Image, ImageProps } from '@chakra-ui/react';
import { ImageProps } from '@chakra-ui/react';
import { useSystem } from '../../../hooks/useSystem';
import Loading from '../MyLoading';
import MyImage from './MyImage';
const MyPhotoView = ({ ...props }: ImageProps) => {
const { isPc } = useSystem();
@@ -15,7 +16,7 @@ const MyPhotoView = ({ ...props }: ImageProps) => {
loadingElement={<Loading fixed={false} />}
>
<PhotoView src={props.src}>
<Image cursor={'pointer'} {...props} />
<MyImage cursor={'pointer'} {...props} />
</PhotoView>
</PhotoProvider>
);

View File

@@ -7,28 +7,50 @@ import {
NumberInputProps
} from '@chakra-ui/react';
import React from 'react';
import MyIcon from '../../Icon';
import { UseFormRegister } from 'react-hook-form';
type Props = Omit<NumberInputProps, 'onChange'> & {
onChange: (e?: number) => any;
onChange?: (e?: number) => any;
placeholder?: string;
register?: UseFormRegister<any>;
name?: string;
bg?: string;
};
const MyNumberInput = (props: Props) => {
const { register, name, onChange, placeholder, bg, ...restProps } = props;
return (
<NumberInput
{...props}
{...restProps}
onChange={(e) => {
if (!onChange) return;
if (isNaN(Number(e))) {
props?.onChange();
onChange();
} else {
props?.onChange(Number(e));
onChange(Number(e));
}
}}
>
<NumberInputField placeholder={props?.placeholder} />
<NumberInputField
bg={bg}
placeholder={placeholder}
{...(register && name
? register(name, {
required: props.isRequired,
min: props.min,
max: props.max
})
: {})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
<NumberIncrementStepper>
<MyIcon name={'core/chat/chevronUp'} width={'12px'} />
</NumberIncrementStepper>
<NumberDecrementStepper>
<MyIcon name={'core/chat/chevronDown'} width={'12px'} />
</NumberDecrementStepper>
</NumberInputStepper>
</NumberInput>
);

View File

@@ -10,7 +10,14 @@ const FormLabel = ({
children: React.ReactNode;
}) => {
return (
<Box color={'myGray.900'} fontSize={'sm'} position={'relative'} flexShrink={0} {...props}>
<Box
color={'myGray.900'}
fontWeight={'medium'}
fontSize={'sm'}
position={'relative'}
flexShrink={0}
{...props}
>
{required && (
<Box color={'red.600'} position={'absolute'} top={'-4px'} left={'-6px'}>
*

View File

@@ -1,7 +1,7 @@
import React from 'react';
import MyIcon from '../Icon';
import { Flex, Image, Box, CloseButton, FlexProps } from '@chakra-ui/react';
import { Flex, Box, CloseButton, FlexProps } from '@chakra-ui/react';
import { useLoading } from '../../../hooks/useLoading';
import Avatar from '../Avatar';
type Props = FlexProps & {
onClose: () => void;
@@ -50,15 +50,7 @@ const CustomRightDrawer = ({
py={'10px'}
px={5}
>
{iconSrc && (
<>
{iconSrc.startsWith('/') ? (
<Image mr={3} objectFit={'contain'} alt="" src={iconSrc} w={'20px'} />
) : (
<MyIcon mr={3} name={iconSrc as any} w={'20px'} />
)}
</>
)}
{iconSrc && <Avatar mr={3} w={'20px'} src={iconSrc} />}
<Box flex={'1'} fontSize={'md'}>
{title}
</Box>

View File

@@ -13,6 +13,7 @@ import {
Box
} from '@chakra-ui/react';
import { useLoading } from '../../../hooks/useLoading';
import Avatar from '../Avatar';
type Props = DrawerContentProps & {
onClose: () => void;
@@ -52,15 +53,7 @@ const MyRightDrawer = ({
py={'10px'}
px={5}
>
{iconSrc && (
<>
{iconSrc.startsWith('/') ? (
<Image mr={3} objectFit={'contain'} alt="" src={iconSrc} w={'20px'} />
) : (
<MyIcon mr={3} name={iconSrc as any} w={'20px'} />
)}
</>
)}
{iconSrc && <Avatar mr={3} w={'20px'} src={iconSrc} />}
<Box flex={'1'} fontSize={'md'}>
{title}
</Box>

View File

@@ -148,7 +148,6 @@ const MyMenu = ({
color={isOpen ? 'primary.600' : ''}
w="fit-content"
p="1"
bgColor={isOpen ? 'myGray.50' : ''}
h="fit-content"
borderRadius="sm"
>

View File

@@ -1,11 +1,11 @@
import React, { useRef, useCallback, useState } from 'react';
import { Button, useDisclosure, Box, Flex, useOutsideClick } from '@chakra-ui/react';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { MultipleSelectProps } from './type';
import React, { useRef, useCallback, useState, useMemo } from 'react';
import { Button, useDisclosure, Box, Flex, useOutsideClick, Checkbox } from '@chakra-ui/react';
import { ListItemType, MultipleArraySelectProps, MultipleSelectProps } from './type';
import EmptyTip from '../EmptyTip';
import { useTranslation } from 'next-i18next';
import MyIcon from '../../common/Icon';
const MultipleRowSelect = ({
export const MultipleRowSelect = ({
placeholder,
label,
value = [],
@@ -106,17 +106,25 @@ const MultipleRowSelect = ({
<Button
justifyContent={'space-between'}
width={'100%'}
rightIcon={<ChevronDownIcon />}
variant={'whiteBase'}
variant={'whitePrimaryOutline'}
size={'lg'}
fontSize={'sm'}
px={3}
outline={'none'}
rightIcon={<MyIcon name={'core/chat/chevronDown'} w="1rem" color={'myGray.500'} />}
_active={{
transform: 'none'
}}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'primary.500'
borderColor: 'primary.600',
color: 'primary.700',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
}
: {})}
: {
borderColor: 'myGray.200',
boxShadow: 'none'
})}
{...styles}
onClick={() => (isOpen ? onClose() : onOpenSelect())}
>
@@ -127,10 +135,194 @@ const MultipleRowSelect = ({
position={'absolute'}
{...(popDirection === 'top'
? {
bottom: '45px'
transform: 'translateY(-105%)',
top: '0'
}
: {
top: '45px'
transform: 'translateY(105%)',
bottom: '0'
})}
py={2}
bg={'white'}
border={'1px solid #fff'}
boxShadow={'5'}
borderRadius={'md'}
zIndex={1}
minW={'100%'}
w={'max-content'}
>
<Flex>
<RenderList list={list} index={0} />
</Flex>
</Box>
)}
</Box>
);
};
export const MultipleRowArraySelect = ({
placeholder,
label,
value = [],
list,
emptyTip,
maxH = 300,
onSelect,
popDirection = 'bottom',
styles
}: MultipleArraySelectProps) => {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const [navigationPath, setNavigationPath] = useState<string[]>([]);
const formatValue = useMemo(() => {
return Array.isArray(value) ? value : [];
}, [value]);
// Close when clicking outside
useOutsideClick({
ref: ref,
handler: onClose
});
const RenderList = useCallback(
({ index, list }: { index: number; list: MultipleSelectProps['list'] }) => {
const currentNavValue = navigationPath[index];
const selectedIndex = list.findIndex((item) => item.value === currentNavValue);
const children = list[selectedIndex]?.children || [];
const hasChildren = list.some((item) => item.children && item.children?.length > 0);
const handleSelect = (item: ListItemType) => {
// Has children, set parent value
if (hasChildren) {
const newPath = [...navigationPath];
newPath[index] = item.value;
setNavigationPath(newPath);
} else {
const parentValue = navigationPath[0];
const newValues = [...formatValue];
const newValue = [parentValue, item.value];
if (newValues.some((v) => v[0] === parentValue && v[1] === item.value)) {
onSelect(newValues.filter((v) => !(v[0] === parentValue && v[1] === item.value)));
} else {
onSelect([...newValues, newValue]);
}
}
};
return (
<>
<Box
className="nowheel"
flex={'1 0 auto'}
px={2}
borderLeft={index !== 0 ? 'base' : 'none'}
maxH={`${maxH}px`}
overflowY={'auto'}
whiteSpace={'nowrap'}
>
{list.map((item) => {
const isSelected = item.value === currentNavValue;
const showCheckbox = !hasChildren;
const isChecked =
showCheckbox &&
formatValue.some((v) => v[1] === item.value && v[0] === navigationPath[0]);
return (
<Flex
key={item.value}
py={2}
cursor={'pointer'}
px={2}
borderRadius={'md'}
_hover={{
bg: 'primary.50',
color: 'primary.600'
}}
onClick={() => handleSelect(item)}
{...(isSelected ? { color: 'primary.600' } : {})}
>
{showCheckbox && (
<Checkbox
isChecked={isChecked}
icon={<MyIcon name={'common/check'} w={'12px'} />}
mr={1}
/>
)}
<Box>{item.label}</Box>
</Flex>
);
})}
{list.length === 0 && (
<EmptyTip
text={emptyTip ?? t('common:common.MultipleRowSelect.No data')}
pt={1}
pb={3}
/>
)}
</Box>
{children.length > 0 && <RenderList list={children} index={index + 1} />}
</>
);
},
[navigationPath, formatValue, onSelect]
);
const onOpenSelect = useCallback(() => {
setNavigationPath([]);
onOpen();
}, []);
return (
<Box ref={ref} position={'relative'}>
<Button
width={'100%'}
variant={'whitePrimaryOutline'}
size={'lg'}
fontSize={'sm'}
px={3}
outline={'none'}
rightIcon={<MyIcon name={'core/chat/chevronDown'} w="1rem" color={'myGray.500'} />}
iconSpacing={2}
h={'auto'}
_active={{
transform: 'none'
}}
_hover={{
borderColor: 'primary.500'
}}
{...(isOpen
? {
borderColor: 'primary.600',
color: 'primary.700',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
}
: {
borderColor: 'myGray.200',
boxShadow: 'none'
})}
{...styles}
onClick={() => (isOpen ? onClose() : onOpenSelect())}
className="nowheel"
>
<Box w={'100%'} textAlign={'left'}>
{label ?? placeholder}
</Box>
</Button>
{isOpen && (
<Box
position={'absolute'}
{...(popDirection === 'top'
? {
transform: 'translateY(-105%)',
top: '0'
}
: {
transform: 'translateY(105%)',
bottom: '0'
})}
py={2}
bg={'white'}

View File

@@ -59,10 +59,11 @@ const MySelect = <T = any,>(
display: 'flex',
alignItems: 'center',
_hover: {
backgroundColor: 'myWhite.600'
backgroundColor: 'myGray.100',
color: 'primary.700'
},
_notLast: {
mb: 2
mb: 1
}
};
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -107,16 +108,19 @@ const MySelect = <T = any,>(
ref={ButtonRef}
width={width}
px={3}
rightIcon={<ChevronDownIcon />}
variant={'whitePrimary'}
rightIcon={<MyIcon name={'core/chat/chevronDown'} w={4} color={'myGray.500'} />}
variant={'whitePrimaryOutline'}
size={'lg'}
fontSize={'sm'}
textAlign={'left'}
_active={{
transform: 'none'
}}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'primary.500'
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
borderColor: 'primary.600',
color: 'primary.700'
}
: {})}
{...props}
@@ -157,7 +161,7 @@ const MySelect = <T = any,>(
{...(value === item.value
? {
ref: SelectedItemRef,
color: 'primary.600',
color: 'primary.700',
bg: 'myGray.100'
}
: {

View File

@@ -4,9 +4,9 @@ type ListItemType = {
value: any;
children?: ListItemType[];
};
export type MultipleSelectProps<T = any> = {
export type MultipleSelectProps = {
label?: string | React.ReactNode;
value: any[];
value?: any[];
placeholder?: string;
list: ListItemType[];
emptyTip?: string;
@@ -15,3 +15,7 @@ export type MultipleSelectProps<T = any> = {
styles?: ButtonProps;
popDirection?: 'top' | 'bottom';
};
export type MultipleArraySelectProps = Omit<MultipleSelectProps, 'value'> & {
value?: any[][];
onSelect: (val: any[][]) => void;
};

View File

@@ -1,6 +1,7 @@
import React from 'react';
import MyTooltip from '.';
import { IconProps, QuestionOutlineIcon } from '@chakra-ui/icons';
import { IconProps } from '@chakra-ui/icons';
import MyIcon from '../Icon';
type Props = IconProps & {
label?: string | React.ReactNode;
@@ -9,7 +10,7 @@ type Props = IconProps & {
const QuestionTip = ({ label, maxW, ...props }: Props) => {
return (
<MyTooltip label={label} maxW={maxW}>
<QuestionOutlineIcon w={'0.9rem'} {...props} />
<MyIcon name={'help' as any} w={'16px'} color={'myGray.500'} {...props} />
</MyTooltip>
);
};

View File

@@ -72,11 +72,11 @@ const LightRowTabs = <ValueType = string,>({
_hover={{
color: activeColor
}}
fontWeight={'medium'}
{...(value === item.value
? {
color: activeColor,
cursor: 'default',
fontWeight: 'bold',
borderBottomColor: activeColor
}
: {

View File

@@ -2,9 +2,10 @@ import React, { useCallback, useRef, useState } from 'react';
import Editor, { Monaco, loader } from '@monaco-editor/react';
import { Box, BoxProps } from '@chakra-ui/react';
import MyIcon from '../../Icon';
import { getWebReqUrl } from '../../../../common/system/utils';
loader.config({
paths: { vs: '/js/monaco-editor.0.45.0/vs' }
paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') }
});
type EditorVariablePickerType = {

View File

@@ -4,9 +4,10 @@ import { Box, BoxProps } from '@chakra-ui/react';
import MyIcon from '../../Icon';
import { useToast } from '../../../../hooks/useToast';
import { useTranslation } from 'next-i18next';
import { getWebReqUrl } from '../../../../common/system/utils';
loader.config({
paths: { vs: '/js/monaco-editor.0.45.0/vs' }
paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') }
});
type EditorVariablePickerType = {

View File

@@ -105,10 +105,10 @@ export default function Editor({
left={0}
right={0}
bottom={0}
py={3}
px={4}
py={2}
px={3}
pointerEvents={'none'}
overflow={'overlay'}
overflow={'hidden'}
>
<Box
color={'myGray.400'}
@@ -146,12 +146,12 @@ export default function Editor({
<Box
zIndex={10}
position={'absolute'}
bottom={0}
bottom={-1}
right={2}
cursor={'pointer'}
onClick={onOpenModal}
>
<MyIcon name={'common/fullScreenLight'} w={'14px'} color={'myGray.600'} />
<MyIcon name={'common/fullScreenLight'} w={'14px'} color={'myGray.500'} />
</Box>
)}
</Box>

View File

@@ -9,6 +9,45 @@
font-size: var(--chakra-fontSizes-sm);
overflow-y: auto;
&:hover {
border-color: var(--chakra-colors-primary-300);
}
&::-webkit-scrollbar {
color: var(--chakra-colors-myGray-100);
}
&::-webkit-scrollbar-thumb {
background-color: var(--chakra-colors-myGray-200) !important;
cursor: pointer;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--chakra-colors-myGray-250) !important;
}
}
.contentEditable_isFlow {
position: relative;
height: 100%;
width: 100%;
border: 1px solid var(--chakra-colors-myGray-200);
border-radius: var(--chakra-radii-sm);
padding: 6px 8px;
font-size: var(--chakra-fontSizes-sm);
overflow-y: auto;
&:hover {
border-color: var(--chakra-colors-primary-300);
}
&::-webkit-scrollbar {
color: var(--chakra-colors-myGray-100);
}
&::-webkit-scrollbar-thumb {
background-color: var(--chakra-colors-myGray-200) !important;
cursor: pointer;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--chakra-colors-myGray-250) !important;
}
}
.contentEditable:focus {

View File

@@ -1,4 +1,4 @@
import { Box, Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import { editorStateToText } from './utils';
import Editor from './Editor';

View File

@@ -1,97 +0,0 @@
import { Box, Button, Image } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
export default function ComfirmVar({
newVariables,
onCancel,
onConfirm
}: {
newVariables: string[];
onCancel: () => void;
onConfirm: () => void;
}) {
const { t } = useTranslation();
return (
<>
<Box
background={'rgba(35, 56, 118, 0.2)'}
rounded={'sm'}
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
/>
<Box
position={'absolute'}
top={'50%'}
left={'50%'}
transform={'translate(-50%, -50%)'}
w={'70%'}
h={'70%'}
bg={'white'}
rounded={'lg'}
boxShadow={'0 2px 4px rgba(0, 0, 0, 0.1)'}
display={'flex'}
flexDirection={'column'}
justifyContent={'space-between'}
pb={4}
>
<Box display={'flex'} mt={4} mr={4}>
<Box
w={'36px'}
h={'36px'}
minW={'36px'}
boxShadow={'0 4px 8px rgba(0, 0, 0, 0.1)'}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
rounded={'md'}
border={'1px solid rgba(0, 0, 0, 0.1)'}
mx={4}
>
<Image alt={''} src={'/imgs/workflow/variable.png'} objectFit={'contain'} w={'20px'} />
</Box>
<Box>{t('common:undefined_var')}</Box>
</Box>
<Box
ml={16}
mt={4}
fontSize={'sm'}
color={'rgb(28,100,242)'}
display={'flex'}
whiteSpace={'wrap'}
>
{newVariables.map((item, index) => (
<Box
key={index}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
bg={'rgb(237,242,250)'}
px={1}
h={6}
rounded={'md'}
mr={2}
>
<span>
<span style={{ opacity: '60%' }}>{`{{`}</span>
<span>{item}</span>
<span style={{ opacity: '60%' }}>{`}}`}</span>
</span>
</Box>
))}
</Box>
<Box>
<Box display={'flex'} justifyContent={'flex-end'} mt={4} mr={4}>
<Button size={'sm'} variant={'ghost'} onClick={onCancel}>
{t('common:common.Cancel')}
</Button>
<Button size={'sm'} variant={'primary'} ml={4} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</Box>
</Box>
</Box>
</>
);
}

View File

@@ -150,12 +150,20 @@ const NodeInputSelect = ({
trigger="click"
Button={
<Button
size={'xs'}
leftIcon={<MyIcon name={renderTypeData.icon as any} w={'0.8rem'} color={'primary.600'} />}
rightIcon={<MyIcon name={'common/select'} w={'0.8rem'} color={'myGray.500'} />}
leftIcon={
<MyIcon name={renderTypeData.icon as any} w={'14px'} color={'primary.600'} mr={-0.5} />
}
rightIcon={<MyIcon name={'common/select'} w={'0.8rem'} color={'myGray.500'} ml={-1} />}
variant={'grayBase'}
border={theme.borders.base}
borderRadius={'xs'}
borderColor={'myGray.200'}
borderRadius={'sm'}
px={'10px'}
py={'6px'}
fontSize={'mini'}
color={'myGray.600'}
h={'28px'}
bg={'myGray.100'}
>
<Box fontWeight={'medium'}>{renderTypeData.title}</Box>
</Button>

View File

@@ -1,5 +1,8 @@
{
"Role_setting": "Role setting",
"Run": "Execute",
"Team Tags Set": "Team tags",
"Team_Tags": "Team tags",
"ai_settings": "AI Configuration",
"all_apps": "All Applications",
"app.Version name": "Version Name",
@@ -27,6 +30,7 @@
"cron.every_month": "Run Monthly",
"cron.every_week": "Run Weekly",
"cron.interval": "Run at Intervals",
"dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.",
"day": "Day",
"document_quote": "Document Reference",
"document_quote_tip": "Usually used to accept user-uploaded document content (requires document parsing), and can also be used to reference other string data.",
@@ -37,6 +41,7 @@
"export_config_successful": "Configuration copied, some sensitive information automatically filtered. Please check for any remaining sensitive data.",
"export_configs": "Export Configurations",
"feedback_count": "User Feedback",
"file_quote_link": "Files",
"file_recover": "File will overwrite current content",
"file_upload": "File Upload",
"file_upload_tip": "Once enabled, documents/images can be uploaded. Documents are retained for 7 days, images for 15 days. Using this feature may incur additional costs. To ensure a good experience, please choose an AI model with a larger context length when using this feature.",
@@ -44,7 +49,7 @@
"go_to_chat": "Go to Conversation",
"go_to_run": "Go to Execution",
"image_upload": "Image Upload",
"image_upload_tip": "Please ensure to select a vision model that can process images.",
"image_upload_tip": "How to activate model image recognition capabilities",
"import_configs": "Import Configurations",
"import_configs_failed": "Import configuration failed, please ensure the configuration is correct!",
"import_configs_success": "Import Successful",
@@ -58,7 +63,7 @@
"intro": "A comprehensive model application orchestration system that offers out-of-the-box data processing and model invocation capabilities. It allows for rapid Dataset construction and workflow orchestration through Flow visualization, enabling complex Dataset scenarios!",
"llm_not_support_vision": "This model does not support image recognition",
"llm_use_vision": "Enable Image Recognition",
"llm_use_vision_tip": "Once image recognition is enabled, this model will automatically receive images uploaded from the 'dialog box' and image links in 'user questions'.",
"llm_use_vision_tip": "After clicking on the model selection, you can see whether the model supports image recognition and the ability to control whether to start image recognition. \nAfter starting image recognition, the model will read the image content in the file link, and if the user question is less than 500 words, it will automatically parse the image in the user question.",
"logs_chat_user": "user",
"logs_empty": "No logs yet~",
"logs_message_total": "Total Messages",
@@ -71,6 +76,7 @@
"month.unit": "Day",
"move_app": "Move Application",
"not_json_file": "Please select a JSON file",
"open_vision_function_tip": "Models with icon switches have image recognition capabilities. \nAfter being turned on, the model will parse the pictures in the file link and automatically parse the pictures in the user's question (user question ≤ 500 words).",
"or_drag_JSON": "or drag in JSON file",
"paste_config": "Paste Configuration",
"permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.",
@@ -125,14 +131,14 @@
"type.Simple bot": "Simple App",
"type.Workflow bot": "Workflow",
"upload_file_max_amount": "Maximum File Quantity",
"upload_file_max_amount_tip": "1. The maximum number of files that can be uploaded at one time.\n2. The maximum number of files remembered by the chat window: each round of dialogue will automatically retrieve files from history, files beyond the range will be forgotten.",
"upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation",
"variable.select type_desc": "You can define a global variable that does not need to be filled in by the user.\n\nThe value of this variable can come from the API interface, the Query of the shared link, or assigned through the [Variable Update] module.",
"variable.textarea_type_desc": "Allows users to input up to 4000 characters in the dialogue box.",
"version.Revert success": "Revert Successful",
"version_back": "Revert to Original State",
"version_copy": "Duplicate",
"version_initial_copy": "Duplicate - Original State",
"vision_model_title": "Enable Image Recognition",
"vision_model_title": "Image recognition ability",
"week.Friday": "Friday",
"week.Monday": "Monday",
"week.Saturday": "Saturday",
@@ -149,7 +155,7 @@
"workflow.read_files": "Document Parsing",
"workflow.read_files_result": "Document Parsing Result",
"workflow.read_files_result_desc": "Original document text, consisting of file names and document content, separated by hyphens between multiple files.",
"workflow.read_files_tip": "Parse all uploaded documents in the dialogue and return the corresponding document content.",
"workflow.read_files_tip": "Parse the documents uploaded in this round of dialogue and return the corresponding document content",
"workflow.select_description": "Description Text",
"workflow.select_description_placeholder": "For example: \nAre there tomatoes in the fridge?",
"workflow.select_description_tip": "You can add a description text to explain the meaning of each option to the user.",
@@ -159,4 +165,4 @@
"workflow.user_file_input_desc": "Links to documents and images uploaded by users.",
"workflow.user_select": "User Selection",
"workflow.user_select_tip": "This module can configure multiple options for selection during the dialogue. Different options can lead to different workflow branches."
}
}

View File

@@ -1,5 +1,7 @@
{
"AI_input_is_empty": "The content passed to the AI node is empty",
"Delete_all": "Clear All Lexicon",
"LLM_model_response_empty": "The model flow response is empty, please check whether the model flow output is normal.",
"chat_history": "Conversation History",
"chat_input_guide_lexicon_is_empty": "Lexicon not configured yet",
"citations": "{{num}} References",
@@ -12,6 +14,7 @@
"contextual_preview": "Contextual Preview {{num}} Items",
"csv_input_lexicon_tip": "Only CSV batch import is supported, click to download the template",
"custom_input_guide_url": "Custom Lexicon URL",
"dataset_quote_type error": "Knowledge base reference type is wrong, correct type: { datasetId: string }[]",
"delete_all_input_guide_confirm": "Are you sure you want to clear the input guide lexicon?",
"empty_directory": "This directory is empty~",
"file_amount_over": "Exceeded maximum file quantity {{max}}",
@@ -29,15 +32,18 @@
"multiple_AI_conversations": "Multiple AI Conversations",
"new_input_guide_lexicon": "New Lexicon",
"no_workflow_response": "No workflow data",
"not_select_file": "No file selected",
"plugins_output": "Plugin Output",
"question_tip": "From top to bottom, the response order of each module",
"response.dataset_concat_length": "Combined total",
"response.node_inputs": "Node Inputs",
"select": "Select",
"select_file": "Upload File",
"select_file_img": "Upload file / image",
"select_img": "Upload Image",
"stream_output": "Stream Output",
"unsupported_file_type": "Unsupported file types",
"upload": "Upload",
"view_citations": "View References",
"web_site_sync": "Web Site Sync"
}
}

View File

@@ -69,8 +69,12 @@
"code_error.system_error.community_version_num_limit": "Exceeded Open Source Version Limit, Please Upgrade to Commercial Version: https://tryfastgpt.ai",
"code_error.team_error.ai_points_not_enough": "Insufficient AI Points",
"code_error.team_error.app_amount_not_enough": "Application Limit Reached",
"code_error.team_error.cannot_delete_default_group": "Cannot delete default group",
"code_error.team_error.dataset_amount_not_enough": "Dataset Limit Reached",
"code_error.team_error.dataset_size_not_enough": "Insufficient Dataset Capacity, Please Expand",
"code_error.team_error.group_name_duplicate": "Duplicate group name",
"code_error.team_error.group_name_empty": "Group name cannot be empty",
"code_error.team_error.group_not_exist": "Group does not exist",
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached",
"code_error.team_error.re_rank_not_enough": "Unauthorized to Use Re-Rank",
@@ -184,6 +188,7 @@
"common.Update Successful": "Updated Successfully",
"common.Username": "Username",
"common.Waiting": "Waiting",
"common.Error": "Error",
"common.Warning": "Warning",
"common.Website": "Website",
"common.all_result": "Full Results",
@@ -569,10 +574,12 @@
"core.dataset.import.Select source": "Select Source",
"core.dataset.import.Source name": "Source Name",
"core.dataset.import.Sources list": "Source List",
"core.dataset.import.Continue upload": "Continue upload",
"core.dataset.import.Upload complete": "Upload complete",
"core.dataset.import.Start upload": "Start Upload",
"core.dataset.import.Total files": "Total {{total}} Files",
"core.dataset.import.Training mode": "Training Mode",
"core.dataset.import.Upload data": "Upload Data",
"core.dataset.import.Upload data": "Confirm Upload",
"core.dataset.import.Upload file progress": "File Upload Progress",
"core.dataset.import.Upload status": "Status",
"core.dataset.import.Web link": "Web Link",
@@ -1112,7 +1119,6 @@
"tag_list": "Tag List",
"team_tag": "Team Tag",
"textarea_variable_picker_tip": "Enter \"/\" to select a variable",
"undefined_var": "Referenced an undefined variable, add it automatically?",
"unit.character": "Character",
"unit.minute": "Minute",
"unusable_variable": "No Usable Variables",
@@ -1197,5 +1203,7 @@
"user.type": "Type",
"verification": "Verification",
"xx_search_result": "{{key}} Search Results",
"yes": "Yes"
"yes": "Yes",
"yesterday": "yesterday",
"yesterday_detail_time": "Yesterday {{time}}"
}

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