Plugin support select file (#2756)

* feat: plugin support upload files (#2716)

* feat: plugin support file upload

* file history

* fix history & chattest

* chore: code

* plugin config icon & file preview padding

* perf: undefined fn

* fix: plugin file numbers & plugin template add config (#2743)

* perf: run plugin without human message (#2749)

* perf: run plugin without human message

* fix build

* fix build

* rename node

* perf: ui

* perf: plugin rerun with last params & plugin log add file (#2755)

* perf: plugin run with last params & plugin log add file

* delete console

* perf: plugin refresh code

* fix: ts

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2024-09-20 14:12:58 +08:00
committed by GitHub
parent 5e7c97b7b8
commit 75af549c7f
59 changed files with 2032 additions and 854 deletions

View File

@@ -12,7 +12,7 @@
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
@@ -68,7 +68,7 @@
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",

View File

@@ -12,7 +12,7 @@
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
@@ -26,7 +26,7 @@
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",

View File

@@ -14,7 +14,7 @@
"nodes": [
{
"nodeId": "lmpb9v2lo2lk",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
@@ -71,7 +71,7 @@
},
{
"nodeId": "i7uow4wj2wdp",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",

View File

@@ -10,7 +10,7 @@
"modules": [
{
"moduleId": "w90mfp",
"name": "自定义插件输入",
"name": "插件开始",
"flowType": "pluginInput",
"showStatus": false,
"position": {

View File

@@ -10,7 +10,7 @@
"modules": [
{
"moduleId": "m8dupj",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/module/input.png",
"flowType": "pluginInput",
@@ -48,7 +48,7 @@
},
{
"moduleId": "bjsa7r",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/module/output.png",
"flowType": "pluginOutput",

View File

@@ -10,7 +10,7 @@
"modules": [
{
"moduleId": "w90mfp",
"name": "自定义插件输入",
"name": "插件开始",
"flowType": "pluginInput",
"showStatus": false,
"position": {
@@ -94,7 +94,7 @@
},
{
"moduleId": "tze1ju",
"name": "自定义插件输出",
"name": "插件输出",
"flowType": "pluginOutput",
"showStatus": false,
"position": {

View File

@@ -11,7 +11,7 @@
"modules": [
{
"moduleId": "w90mfp",
"name": "自定义插件输入",
"name": "插件开始",
"flowType": "pluginInput",
"showStatus": false,
"position": {
@@ -78,7 +78,7 @@
},
{
"moduleId": "tze1ju",
"name": "自定义插件输出",
"name": "插件输出",
"flowType": "pluginOutput",
"showStatus": false,
"position": {

View File

@@ -9,14 +9,14 @@
"nodes": [
{
"nodeId": "pluginInput",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "可以配置插件需要哪些输入,利用这些输入来运行插件",
"avatar": "core/workflow/template/workflowStart",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 351.2046235980429,
"y": -77.41739975794749
"x": 503.3030871469042,
"y": -91.64434154072819
},
"version": "481",
"inputs": [
@@ -44,14 +44,14 @@
},
{
"nodeId": "pluginOutput",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "core/workflow/template/pluginOutput",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1983.6911708285384,
"y": -95.86447885674228
"x": 1876.2082565873427,
"y": -110.14434154072819
},
"version": "481",
"inputs": [
@@ -95,7 +95,7 @@
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input",
"description": "common:core.module.input.description.HTTP Dynamic Input",
"customInputConfig": {
"selectValueTypeList": [
"string",
@@ -106,6 +106,7 @@
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
@@ -115,7 +116,9 @@
],
"showDescription": false,
"showDefaultValue": true
}
},
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpMethod",
@@ -123,17 +126,33 @@
"valueType": "string",
"label": "",
"value": "POST",
"required": true
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpTimeout",
"renderTypeList": ["custom"],
"valueType": "number",
"label": "",
"value": 30,
"min": 5,
"max": 600,
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpReqUrl",
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"description": "common:core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "https://fal.run/fal-ai/flux-pro"
"value": "https://fal.run/fal-ai/flux-pro",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpHeader",
@@ -147,9 +166,11 @@
}
],
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false
"description": "common:core.module.input.description.Http Request Header",
"placeholder": "common:core.module.input.description.Http Request Header",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpParams",
@@ -157,7 +178,9 @@
"valueType": "any",
"value": [],
"label": "",
"required": false
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpJsonBody",
@@ -165,7 +188,29 @@
"valueType": "any",
"value": "{\n \"prompt\": \"{{prompt}}\",\n \"image_size\": \"landscape_4_3\",\n \"num_inference_steps\": 28,\n \"guidance_scale\": 3.5\n}",
"label": "",
"required": false
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpFormBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpContentType",
"renderTypeList": ["hidden"],
"valueType": "string",
"value": "json",
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"renderTypeList": ["reference"],
@@ -183,6 +228,7 @@
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
@@ -201,7 +247,7 @@
{
"id": "error",
"key": "error",
"label": "请求错误",
"label": "workflow:request_error",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
@@ -209,8 +255,8 @@
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"required": true,
"label": "workflow:raw_response",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
@@ -250,6 +296,20 @@
"label": "images[0].url"
}
]
},
{
"nodeId": "lSYsc889IXDr",
"name": "系统配置",
"intro": "",
"avatar": "core/workflow/template/systemConfig",
"flowNodeType": "pluginConfig",
"position": {
"x": 45.52914573588026,
"y": -110.14434154072819
},
"version": "4811",
"inputs": [],
"outputs": []
}
],
"edges": [

View File

@@ -9,14 +9,14 @@
"nodes": [
{
"nodeId": "pluginInput",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "可以配置插件需要哪些输入,利用这些输入来运行插件",
"avatar": "core/workflow/template/workflowStart",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 412.7756423516722,
"y": -99.80686112290361
"x": 421.97302886868476,
"y": -89.7785530936485
},
"version": "481",
"inputs": [
@@ -44,14 +44,14 @@
},
{
"nodeId": "pluginOutput",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "core/workflow/template/pluginOutput",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1822.7195641525896,
"y": -193.54601587659562
"x": 1785.9300180845394,
"y": -108.2785530936485
},
"version": "481",
"inputs": [
@@ -95,7 +95,7 @@
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input",
"description": "common:core.module.input.description.HTTP Dynamic Input",
"customInputConfig": {
"selectValueTypeList": [
"string",
@@ -106,6 +106,7 @@
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
@@ -115,7 +116,9 @@
],
"showDescription": false,
"showDefaultValue": true
}
},
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpMethod",
@@ -123,17 +126,33 @@
"valueType": "string",
"label": "",
"value": "POST",
"required": true
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpTimeout",
"renderTypeList": ["custom"],
"valueType": "number",
"label": "",
"value": 30,
"min": 5,
"max": 600,
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpReqUrl",
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"description": "common:core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "https://api.openai.com/v1/images/generations"
"value": "https://api.openai.com/v1/images/generations",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpHeader",
@@ -147,9 +166,11 @@
}
],
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false
"description": "common:core.module.input.description.Http Request Header",
"placeholder": "common:core.module.input.description.Http Request Header",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpParams",
@@ -157,7 +178,9 @@
"valueType": "any",
"value": [],
"label": "",
"required": false
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpJsonBody",
@@ -165,7 +188,29 @@
"valueType": "any",
"value": "{\n \"model\": \"dall-e-3\",\n \"prompt\": \"{{prompt}}\",\n \"n\": 1,\n \"size\": \"1024x1024\"\n}",
"label": "",
"required": false
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpFormBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpContentType",
"renderTypeList": ["hidden"],
"valueType": "string",
"value": "json",
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"renderTypeList": ["reference"],
@@ -183,6 +228,7 @@
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
@@ -201,7 +247,7 @@
{
"id": "error",
"key": "error",
"label": "请求错误",
"label": "workflow:request_error",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
@@ -209,8 +255,8 @@
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"required": true,
"label": "workflow:raw_response",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
@@ -250,6 +296,20 @@
"label": "data[0].url"
}
]
},
{
"nodeId": "c7tRU2qAQoAf",
"name": "系统配置",
"intro": "",
"avatar": "core/workflow/template/systemConfig",
"flowNodeType": "pluginConfig",
"position": {
"x": -46.476647046261974,
"y": -89.7785530936485
},
"version": "4811",
"inputs": [],
"outputs": []
}
],
"edges": [

View File

@@ -9,14 +9,14 @@
"nodes": [
{
"nodeId": "pluginInput",
"name": "自定义插件输入",
"name": "插件开始",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "core/workflow/template/workflowStart",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 517.5620777851774,
"y": -173.55711888178655
"x": 535.7465806305546,
"y": -201.26482361861054
},
"version": "481",
"inputs": [
@@ -79,14 +79,14 @@
},
{
"nodeId": "pluginOutput",
"name": "自定义插件输出",
"name": "插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"avatar": "core/workflow/template/pluginOutput",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1668.9410524554828,
"y": -153.47815316221283
"x": 1776.027569211593,
"y": -58.264823618610535
},
"version": "481",
"inputs": [],
@@ -111,7 +111,116 @@
"valueType": "dynamic",
"label": "",
"required": false,
"description": "core.module.input.description.HTTP Dynamic Input"
"description": "common:core.module.input.description.HTTP Dynamic Input",
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
},
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpMethod",
"renderTypeList": ["custom"],
"valueType": "string",
"label": "",
"value": "POST",
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpTimeout",
"renderTypeList": ["custom"],
"valueType": "number",
"label": "",
"value": 30,
"min": 5,
"max": 600,
"required": true,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpReqUrl",
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "common:core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "{{url}}",
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpHeader",
"renderTypeList": ["custom"],
"valueType": "any",
"value": [],
"label": "",
"description": "common:core.module.input.description.Http Request Header",
"placeholder": "common:core.module.input.description.Http Request Header",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpParams",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpJsonBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": "{\r\n \"msg_type\": \"text\",\r\n \"content\": {\r\n \"text\": \"{{text}}\"\r\n }\r\n}",
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpFormBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "system_httpContentType",
"renderTypeList": ["hidden"],
"valueType": "string",
"value": "json",
"label": "",
"required": false,
"debugLabel": "",
"toolDescription": ""
},
{
"key": "text",
@@ -124,7 +233,28 @@
"key": true,
"valueType": true
},
"value": ["pluginInput", "p0m68Dv5KaIp"]
"value": ["pluginInput", "p0m68Dv5KaIp"],
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
},
{
"key": "url",
@@ -137,54 +267,48 @@
"key": true,
"valueType": true
},
"value": ["pluginInput", "mv52BrPVE6bm"]
},
{
"key": "system_httpMethod",
"renderTypeList": ["custom"],
"valueType": "string",
"label": "",
"value": "POST",
"required": true
},
{
"key": "system_httpReqUrl",
"renderTypeList": ["hidden"],
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"value": "{{url}}"
},
{
"key": "system_httpHeader",
"renderTypeList": ["custom"],
"valueType": "any",
"value": [],
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false
},
{
"key": "system_httpParams",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": [],
"label": "",
"required": false
},
{
"key": "system_httpJsonBody",
"renderTypeList": ["hidden"],
"valueType": "any",
"value": "{\r\n \"msg_type\": \"text\",\r\n \"content\": {\r\n \"text\": \"{{text}}\"\r\n }\r\n}",
"label": "",
"required": false
"value": ["pluginInput", "mv52BrPVE6bm"],
"customInputConfig": {
"selectValueTypeList": [
"string",
"number",
"boolean",
"object",
"arrayString",
"arrayNumber",
"arrayBoolean",
"arrayObject",
"arrayAny",
"any",
"chatHistory",
"datasetQuote",
"dynamic",
"selectApp",
"selectDataset"
],
"showDescription": false,
"showDefaultValue": true
}
}
],
"outputs": [
{
"id": "error",
"key": "error",
"label": "workflow:request_error",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
},
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"required": true,
"label": "workflow:raw_response",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
@@ -195,25 +319,22 @@
"key": true,
"valueType": true
}
},
{
"id": "error",
"key": "error",
"label": "请求错误",
"description": "HTTP请求错误信息成功时返回空",
"valueType": "object",
"type": "static"
},
{
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"required": true,
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
}
]
},
{
"nodeId": "q3ccNXiZIHoS",
"name": "系统配置",
"intro": "",
"avatar": "core/workflow/template/systemConfig",
"flowNodeType": "pluginConfig",
"position": {
"x": 99.73879703925843,
"y": -201.26482361861054
},
"version": "4811",
"inputs": [],
"outputs": []
}
],
"edges": [

View File

@@ -8,7 +8,8 @@ import {
useDisclosure,
HStack,
Switch,
ModalFooter
ModalFooter,
BoxProps
} from '@chakra-ui/react';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
@@ -25,8 +26,9 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const FileSelect = ({
forbidVision = false,
value = defaultAppSelectFileConfig,
onChange
}: {
onChange,
...labelStyle
}: Omit<BoxProps, 'onChange'> & {
forbidVision?: boolean;
value?: AppFileSelectConfigType;
onChange: (e: AppFileSelectConfigType) => void;
@@ -57,7 +59,7 @@ const FileSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/file'} mr={2} w={'20px'} />
<FormLabel>{t('app:file_upload')}</FormLabel>
<FormLabel {...labelStyle}>{t('app:file_upload')}</FormLabel>
<ChatFunctionTip type={'file'} />
<Box flex={1} />
<MyTooltip label={t('app:config_file_upload')}>

View File

@@ -1,34 +1,21 @@
import { useSpeech } from '@/web/common/hooks/useSpeech';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, CircularProgress, Flex, HStack, Image, Spinner, Textarea } from '@chakra-ui/react';
import { Box, Flex, Spinner, Textarea } from '@chakra-ui/react';
import React, { useRef, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { uploadFile2DB } from '@/web/common/file/controller';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
ChatBoxInputFormType,
ChatBoxInputType,
SendPromptFnType,
UserInputFileItemType
} from '../type';
import { ChatBoxInputFormType, ChatBoxInputType, SendPromptFnType } from '../type';
import { textareaMinH } from '../constants';
import { UseFormReturn, useFieldArray } from 'react-hook-form';
import { UseFormReturn } from 'react-hook-form';
import { ChatBoxContext } from '../Provider';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { documentFileType } from '@fastgpt/global/common/file/constants';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { clone } from 'lodash';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getErrText } from '@fastgpt/global/common/error/utils';
import FilePreview from '../../components/FilePreview';
import { useFileUpload } from '../hooks/useFileUpload';
import ComplianceTip from '@/components/common/ComplianceTip/index';
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
@@ -56,21 +43,10 @@ const ChatInput = ({
appId: string;
}) => {
const { isPc } = useSystem();
const { toast } = useToast();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { setValue, watch, control } = chatForm;
const inputValue = watch('input');
const {
update: updateFiles,
remove: removeFiles,
fields: fileList,
replace: replaceFiles
} = useFieldArray({
control,
name: 'files'
});
const {
chatId,
@@ -82,86 +58,32 @@ const ChatInput = ({
fileSelectConfig
} = useContextSelector(ChatBoxContext, (v) => v);
const {
File,
onOpenSelectFile,
fileList,
onSelectFile,
uploadFiles,
selectFileIcon,
selectFileLabel,
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig,
control
});
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
const canSendMessage = havInput && !hasFileUploading;
const showSelectFile = fileSelectConfig.canSelectFile;
const showSelectImg = fileSelectConfig.canSelectImg;
const maxSelectFiles = fileSelectConfig.maxFiles ?? 10;
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
const { icon: selectFileIcon, tooltip: selectFileTip } = useMemo(() => {
if (showSelectFile) {
return {
icon: 'core/chat/fileSelect',
tooltip: t('chat:select_file')
};
} else if (showSelectImg) {
return {
icon: 'core/chat/fileSelect',
tooltip: t('chat:select_img')
};
}
return {};
}, [showSelectFile, showSelectImg, t]);
/* file selector and upload */
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
multiple: true,
maxCount: maxSelectFiles
});
// Upload files
useRequest2(
async () => {
const filterFiles = fileList.filter((item) => item.status === 0);
if (filterFiles.length === 0) return;
replaceFiles(fileList.map((item) => ({ ...item, status: 1 })));
let errorFileIndex: number[] = [];
await Promise.allSettled(
filterFiles.map(async (file) => {
const copyFile = clone(file);
copyFile.status = 1;
if (!copyFile.rawFile) return;
try {
const fileIndex = fileList.findIndex((item) => item.id === file.id)!;
// Start upload and update process
const { previewUrl } = await uploadFile2DB({
file: copyFile.rawFile,
bucketName: 'chat',
outLinkAuthData,
metadata: {
chatId
},
percentListen(e) {
copyFile.process = e;
if (!copyFile.url) {
updateFiles(fileIndex, copyFile);
}
}
});
// Update file url
copyFile.url = `${location.origin}${previewUrl}`;
updateFiles(fileIndex, copyFile);
} catch (error) {
errorFileIndex.push(fileList.findIndex((item) => item.id === file.id)!);
toast({
status: 'warning',
title: t(
getErrText(error, t('common:error.upload_file_error_filename', { name: file.name }))
)
});
}
})
);
removeFiles(errorFileIndex);
uploadFiles();
},
{
manual: false,
@@ -169,78 +91,6 @@ const ChatInput = ({
refreshDeps: [fileList, outLinkAuthData, chatId]
}
);
const onSelectFile = useCallback(
async (files: File[]) => {
if (!files || files.length === 0) {
return;
}
// filter max files
if (fileList.length + files.length > maxSelectFiles) {
files = files.slice(0, maxSelectFiles - fileList.length);
toast({
status: 'warning',
title: t('chat:file_amount_over', { max: maxSelectFiles })
});
}
const filterFilesByMaxSize = files.filter((file) => file.size <= maxSize);
if (filterFilesByMaxSize.length < files.length) {
toast({
status: 'warning',
title: t('file:some_file_size_exceeds_limit', { maxSize: formatFileSize(maxSize) })
});
}
const loadFiles = await Promise.all(
filterFilesByMaxSize.map(
(file) =>
new Promise<UserInputFileItemType>((resolve, reject) => {
if (file.type.includes('image')) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const item: UserInputFileItemType = {
id: getNanoid(6),
rawFile: file,
type: ChatFileTypeEnum.image,
name: file.name,
icon: reader.result as string,
status: 0
};
resolve(item);
};
reader.onerror = () => {
reject(reader.error);
};
} else {
resolve({
id: getNanoid(6),
rawFile: file,
type: ChatFileTypeEnum.file,
name: file.name,
icon: getFileIcon(file.name),
status: 0
});
}
})
)
);
// Document, image
const concatFileList = clone(
fileList.concat(loadFiles).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
})
);
replaceFiles(concatFileList);
},
[fileList, maxSelectFiles, replaceFiles, toast, t, maxSize]
);
/* on send */
const handleSend = useCallback(
@@ -330,91 +180,7 @@ const ChatInput = ({
),
[isSpeaking, isTransCription, t]
);
const RenderFilePreview = useMemo(
() =>
fileList.length > 0 ? (
<Flex
maxH={'250px'}
overflowY={'auto'}
wrap={'wrap'}
px={[2, 4]}
pt={3}
userSelect={'none'}
gap={2}
mb={fileList.length > 0 ? 2 : 0}
>
{fileList.map((item, index) => (
<MyBox
key={index}
border={'sm'}
boxShadow={
'0px 2.571px 6.429px 0px rgba(19, 51, 107, 0.08), 0px 0px 0.643px 0px rgba(19, 51, 107, 0.08)'
}
rounded={'md'}
position={'relative'}
_hover={{
'.close-icon': { display: 'block' }
}}
>
<MyIcon
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.700'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
bg={'white'}
right={'-8px'}
top={'-8px'}
onClick={() => removeFiles(index)}
className="close-icon"
display={['', 'none']}
zIndex={10}
/>
{item.type === ChatFileTypeEnum.image && (
<Image
alt={'img'}
src={item.icon}
w={['2rem', '3rem']}
h={['2rem', '3rem']}
borderRadius={'md'}
objectFit={'contain'}
/>
)}
{item.type === ChatFileTypeEnum.file && (
<HStack minW={['100px', '150px']} maxW={'250px'} p={2}>
<MyIcon name={item.icon as any} w={['1.5rem', '2rem']} h={['1.5rem', '2rem']} />
<Box flex={'1 0 0'} className="textEllipsis" fontSize={'xs'}>
{item.name}
</Box>
</HStack>
)}
{/* Process */}
{!item.url && (
<Flex
position={'absolute'}
inset="0"
bg="rgba(255,255,255,0.4)"
alignItems="center"
justifyContent="center"
>
<CircularProgress
value={item.process}
color="primary.600"
bg={'white'}
size={isPc ? '30px' : '35px'}
>
{/* <CircularProgressLabel>{item.process ?? 0}%</CircularProgressLabel> */}
</CircularProgress>
</Flex>
)}
</MyBox>
))}
</Flex>
) : null,
[fileList, isPc, removeFiles]
);
const RenderTextarea = useMemo(
() => (
<Flex alignItems={'flex-end'} mt={fileList.length > 0 ? 1 : 0} pl={[2, 4]}>
@@ -431,10 +197,10 @@ const ChatInput = ({
onOpenSelectFile();
}}
>
<MyTooltip label={selectFileTip}>
<MyTooltip label={selectFileLabel}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip>
<File onSelect={onSelectFile} />
<File onSelect={(files) => onSelectFile({ files, fileList })} />
</Flex>
)}
@@ -510,7 +276,7 @@ const ChatInput = ({
.filter((file) => {
return file && fileTypeFilter(file);
}) as File[];
onSelectFile(files);
onSelectFile({ files, fileList });
if (files.length > 0) {
e.stopPropagation();
@@ -636,7 +402,7 @@ const ChatInput = ({
[
File,
TextareaDom,
fileList.length,
fileList,
handleSend,
hasFileUploading,
havInput,
@@ -650,7 +416,7 @@ const ChatInput = ({
onStop,
onWhisperRecord,
selectFileIcon,
selectFileTip,
selectFileLabel,
setValue,
showSelectFile,
showSelectImg,
@@ -700,7 +466,9 @@ const ChatInput = ({
{RenderTranslateLoading}
{/* file preview */}
{RenderFilePreview}
<Box px={[2, 4]}>
<FilePreview fileList={fileList} removeFiles={removeFiles} />
</Box>
{RenderTextarea}
</Box>

View File

@@ -20,6 +20,7 @@ import { createContext } from 'use-context-selector';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { getChatResData } from '@/web/core/chat/api';
import { ChatBoxInputFormType } from './type';
export type ChatProviderProps = OutLinkChatAuthProps & {
appAvatar?: string;
@@ -29,7 +30,7 @@ export type ChatProviderProps = OutLinkChatAuthProps & {
chatHistories: ChatSiteItemType[];
setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
variablesForm: UseFormReturn<FieldValues, any>;
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
// not chat test params
chatId?: string;

View File

@@ -0,0 +1,214 @@
import { useCallback, useMemo } from 'react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { uploadFile2DB } from '@/web/common/file/controller';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import { clone } from 'lodash';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { Control, useFieldArray } from 'react-hook-form';
import { ChatBoxInputFormType, UserInputFileItemType } from '../type';
import { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import { documentFileType } from '@fastgpt/global/common/file/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
interface UseFileUploadOptions {
outLinkAuthData: any;
chatId: string;
fileSelectConfig: AppFileSelectConfigType;
control: Control<ChatBoxInputFormType, any>;
}
export const useFileUpload = (props: UseFileUploadOptions) => {
const { outLinkAuthData, chatId, fileSelectConfig, control } = props;
const { toast } = useToast();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const {
update: updateFiles,
remove: removeFiles,
fields: fileList,
replace: replaceFiles
} = useFieldArray({
control: control,
name: 'files'
});
const showSelectFile = fileSelectConfig?.canSelectFile;
const showSelectImg = fileSelectConfig?.canSelectImg;
const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10;
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => {
if (showSelectFile && showSelectImg) {
return {
icon: 'core/chat/fileSelect',
label: t('chat:select_file_img')
};
} else if (showSelectFile) {
return {
icon: 'core/chat/fileSelect',
label: t('chat:select_file')
};
} else if (showSelectImg) {
return {
icon: 'core/chat/imgSelect',
label: t('chat:select_img')
};
}
return {};
}, [showSelectFile, showSelectImg, t]);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
multiple: true,
maxCount: maxSelectFiles
});
const onSelectFile = useCallback(
async ({ files, fileList }: { files: File[]; fileList: UserInputFileItemType[] }) => {
if (!files || files.length === 0) {
return [];
}
// Filter max files
if (files.length > maxSelectFiles) {
files = files.slice(0, maxSelectFiles);
toast({
status: 'warning',
title: t('chat:file_amount_over', { max: maxSelectFiles })
});
}
// Filter files by max size
const filterFilesByMaxSize = files.filter((file) => file.size <= maxSize);
if (filterFilesByMaxSize.length < files.length) {
toast({
status: 'warning',
title: t('file:some_file_size_exceeds_limit', { maxSize: formatFileSize(maxSize) })
});
}
// Convert files to UserInputFileItemType
const loadFiles = await Promise.all(
filterFilesByMaxSize.map(
(file) =>
new Promise<UserInputFileItemType>((resolve, reject) => {
if (file.type.includes('image')) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const item: UserInputFileItemType = {
id: getNanoid(6),
rawFile: file,
type: ChatFileTypeEnum.image,
name: file.name,
icon: reader.result as string,
status: 0
};
resolve(item);
};
reader.onerror = () => {
reject(reader.error);
};
} else {
resolve({
id: getNanoid(6),
rawFile: file,
type: ChatFileTypeEnum.file,
name: file.name,
icon: getFileIcon(file.name),
status: 0
});
}
})
)
);
// Document, image
const concatFileList = clone(
fileList.concat(loadFiles).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
})
);
replaceFiles(concatFileList);
return loadFiles;
},
[maxSelectFiles, replaceFiles, toast, t, maxSize]
);
const uploadFiles = async () => {
const filterFiles = fileList.filter((item) => item.status === 0);
if (filterFiles.length === 0) return;
replaceFiles(fileList.map((item) => ({ ...item, status: 1 })));
let errorFileIndex: number[] = [];
await Promise.allSettled(
filterFiles.map(async (file) => {
const copyFile = clone(file);
copyFile.status = 1;
if (!copyFile.rawFile) return;
try {
const fileIndex = fileList.findIndex((item) => item.id === file.id)!;
// Start upload and update process
const { previewUrl } = await uploadFile2DB({
file: copyFile.rawFile,
bucketName: 'chat',
outLinkAuthData,
metadata: {
chatId
},
percentListen(e) {
copyFile.process = e;
if (!copyFile.url) {
updateFiles(fileIndex, copyFile);
}
}
});
// Update file url
copyFile.url = `${location.origin}${previewUrl}`;
updateFiles(fileIndex, copyFile);
} catch (error) {
errorFileIndex.push(fileList.findIndex((item) => item.id === file.id)!);
toast({
status: 'warning',
title: t(
getErrText(error, t('common:error.upload_file_error_filename', { name: file.name }))
)
});
}
})
);
removeFiles(errorFileIndex);
};
return {
File,
onOpenSelectFile,
fileList,
onSelectFile,
uploadFiles,
selectFileIcon,
selectFileLabel,
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
};
};

View File

@@ -430,7 +430,8 @@ const ChatBox = (
file: {
type: file.type,
name: file.name,
url: file.url || ''
url: file.url || '',
icon: file.icon || ''
}
})),
...(text

View File

@@ -22,6 +22,7 @@ export type ChatBoxInputFormType = {
input: string;
files: UserInputFileItemType[];
chatStarted: boolean;
[key: string]: any;
};
export type ChatBoxInputType = {

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Box, Button, Flex } from '@chakra-ui/react';
@@ -6,11 +6,19 @@ import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { isEqual } from 'lodash';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import Markdown from '@/components/Markdown';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import FilePreview from '../../components/FilePreview';
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { ChatBoxInputFormType, UserInputFileItemType } from '../../ChatBox/type';
const RenderInput = () => {
const { t } = useTranslation();
const {
pluginInputs,
variablesForm,
@@ -19,10 +27,13 @@ const RenderInput = () => {
onNewChat,
onSubmit,
isChatting,
chatConfig
chatConfig,
chatId,
outLinkAuthData,
restartInputStore,
setRestartInputStore
} = useContextSelector(PluginRunContext, (v) => v);
const { t } = useTranslation();
const {
control,
handleSubmit,
@@ -31,46 +42,102 @@ const RenderInput = () => {
formState: { errors }
} = variablesForm;
const defaultFormValues = useMemo(() => {
return pluginInputs.reduce(
const {
File,
onOpenSelectFile,
fileList,
onSelectFile,
uploadFiles,
selectFileIcon,
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: chatConfig?.fileSelectConfig,
control
});
const isDisabledInput = histories.length > 0;
const onClickNewChat = useCallback(
(e: ChatBoxInputFormType, files: UserInputFileItemType[] = []) => {
setRestartInputStore({
...e,
files
});
onNewChat?.();
},
[onNewChat, setRestartInputStore]
);
useEffect(() => {
// Set last run value
if (!isDisabledInput && restartInputStore) {
reset(restartInputStore);
return;
}
// Set history to default value
const defaultFormValues = pluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
}, [pluginInputs]);
const historyFormValues = useMemo(() => {
if (histories.length === 0) return undefined;
const historyFormValues = (() => {
if (!isDisabledInput) return undefined;
const historyValue = histories[0].value;
try {
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
return (
inputValueString &&
JSON.parse(inputValueString).reduce(
(
acc: Record<string, any>,
{
key,
value
}: {
key: string;
value: any;
}
) => ({ ...acc, [key]: value }),
{}
)
);
} catch (error) {
console.error('Failed to parse input value:', error);
return undefined;
}
})();
try {
const inputValueString = histories[0].value[0].text?.content || '[]';
return JSON.parse(inputValueString).reduce(
(
acc: Record<string, any>,
{
key,
value
}: {
key: string;
value: any;
}
) => ({ ...acc, [key]: value }),
{}
);
} catch (error) {
console.error('Failed to parse input value:', error);
return undefined;
}
}, [histories]);
// Parse history file
const historyFileList = (() => {
if (!isDisabledInput) return [];
const historyValue = histories[0].value as UserChatItemValueItemType[];
return historyValue.filter((item) => item.type === 'file').map((item) => item.file);
})();
useEffect(() => {
if (isEqual(getValues(), defaultFormValues)) return;
reset(historyFormValues || defaultFormValues);
}, [defaultFormValues, getValues, historyFormValues, reset]);
reset({
...(historyFormValues || defaultFormValues),
files: historyFileList
});
}, [getValues, histories, isDisabledInput, pluginInputs, replaceFiles, reset, restartInputStore]);
const isDisabledInput = histories.length > 0;
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
return (
<>
@@ -88,7 +155,35 @@ const RenderInput = () => {
<Markdown source={chatConfig.instruction} />
</Box>
)}
{/* file select */}
{(showSelectFile || showSelectImg) && (
<Box mb={5}>
<Flex alignItems={'center'}>
<FormLabel fontSize={'md'} fontWeight={'medium'}>
{t('chat:file_input')}
</FormLabel>
<QuestionTip ml={1} label={t('chat:file_input_tip')} />
<Box flex={1} />
{histories.length === 0 && (
<Button
leftIcon={<MyIcon name={selectFileIcon as any} w={'16px'} />}
variant={'whiteBase'}
onClick={() => {
onOpenSelectFile();
}}
>
{t('chat:select')}
</Button>
)}
<File onSelect={(files) => onSelectFile({ files, fileList })} />
</Flex>
<FilePreview
fileList={fileList}
removeFiles={isDisabledInput ? undefined : removeFiles}
/>
</Box>
)}
{/* Filed */}
{pluginInputs.map((input) => {
return (
<Controller
@@ -118,18 +213,22 @@ const RenderInput = () => {
/>
);
})}
{/* Run Button */}
{onStartChat && onNewChat && (
<Flex justifyContent={'end'} mt={8}>
<Button
isLoading={isChatting}
isLoading={isChatting || hasFileUploading}
onClick={() => {
if (histories.length > 0) {
return onNewChat();
}
handleSubmit(onSubmit)();
handleSubmit((e) => {
if (isDisabledInput) {
onClickNewChat(e, fileList);
} else {
onSubmit(e, fileList);
}
})();
}}
>
{histories.length > 0 ? t('common:common.Restart') : t('common:common.Run')}
{isDisabledInput ? t('common:common.Restart') : t('common:common.Run')}
</Button>
</Flex>
)}

View File

@@ -1,8 +1,12 @@
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import { createContext } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import { AIChatItemValueItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { FieldValues } from 'react-hook-form';
import {
AIChatItemValueItemType,
ChatSiteItemType,
RuntimeUserPromptType
} from '@fastgpt/global/core/chat/type';
import { FieldValues, useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -10,17 +14,23 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { generatingMessageProps } from '../type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getPluginRunContent } from '@fastgpt/global/core/app/plugin/utils';
import { useTranslation } from 'next-i18next';
type PluginRunContextType = PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: FieldValues) => Promise<any>;
};
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatBoxInputFormType, UserInputFileItemType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
type PluginRunContextType = OutLinkChatAuthProps &
PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => Promise<any>;
outLinkAuthData: OutLinkChatAuthProps;
restartInputStore?: ChatBoxInputFormType;
setRestartInputStore: React.Dispatch<React.SetStateAction<ChatBoxInputFormType | undefined>>;
};
export const PluginRunContext = createContext<PluginRunContextType>({
pluginInputs: [],
//@ts-ignore
variablesForm: undefined,
histories: [],
setHistories: function (value: React.SetStateAction<ChatSiteItemType[]>): void {
throw new Error('Function not implemented.');
@@ -33,15 +43,24 @@ export const PluginRunContext = createContext<PluginRunContextType>({
isChatting: false,
onSubmit: function (e: FieldValues): Promise<any> {
throw new Error('Function not implemented.');
}
},
outLinkAuthData: {},
//@ts-ignore
variablesForm: undefined
});
const PluginRunContextProvider = ({
shareId,
outLinkUid,
teamId,
teamToken,
children,
...props
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const [restartInputStore, setRestartInputStore] = useState<ChatBoxInputFormType>();
const { toast } = useToast();
const chatController = useRef(new AbortController());
const { t } = useTranslation();
@@ -50,6 +69,22 @@ const PluginRunContextProvider = ({
chatController.current?.abort('stop');
}, []);
const outLinkAuthData = useMemo(
() => ({
shareId,
outLinkUid,
teamId,
teamToken
}),
[shareId, outLinkUid, teamId, teamToken]
);
const variablesForm = useForm<ChatBoxInputFormType>({
defaultValues: {
files: []
}
});
const generatingMessage = useCallback(
({ event, text = '', status, name, tool }: generatingMessageProps) => {
setHistories((state) =>
@@ -144,90 +179,99 @@ const PluginRunContextProvider = ({
[histories]
);
const { runAsync: onSubmit } = useRequest2(async (e: FieldValues) => {
if (!onStartChat) return;
if (isChatting) {
toast({
title: t('chat:is_chatting'),
status: 'warning'
});
return;
}
setTab(PluginRunBoxTabEnum.output);
const { runAsync: onSubmit } = useRequest2(
async (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => {
if (!onStartChat) return;
if (isChatting) {
toast({
title: t('chat:is_chatting'),
status: 'warning'
});
return;
}
// reset controller
abortRequest();
const abortSignal = new AbortController();
chatController.current = abortSignal;
// reset controller
abortRequest();
const abortSignal = new AbortController();
chatController.current = abortSignal;
setHistories([
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
status: 'finish',
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: getPluginRunContent({
pluginInputs,
variables: e
})
setHistories([
{
...getPluginRunUserQuery({
pluginInputs,
variables: e,
files: files as RuntimeUserPromptType['files']
}),
status: 'finish'
},
{
dataId: getNanoid(24),
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: ''
}
}
}
]
},
{
dataId: getNanoid(24),
obj: ChatRoleEnum.AI,
value: [
],
status: 'loading'
}
]);
setTab(PluginRunBoxTabEnum.output);
const messages = chats2GPTMessages({
messages: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: ''
}
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
value: []
}
],
status: 'loading'
}
]);
try {
const { responseData } = await onStartChat({
messages: [],
controller: chatController.current,
generatingMessage,
variables: e
reserveId: true
});
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
responseData
};
})
);
} catch (err: any) {
toast({ title: err.message, status: 'error' });
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
try {
const { responseData } = await onStartChat({
messages: messages,
controller: chatController.current,
generatingMessage,
variables: e
});
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
responseData
};
})
);
} catch (err: any) {
toast({ title: err.message, status: 'error' });
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
}
}
});
);
const contextValue: PluginRunContextType = {
...props,
isChatting,
onSubmit
onSubmit,
outLinkAuthData,
variablesForm,
restartInputStore,
setRestartInputStore
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};

View File

@@ -5,10 +5,11 @@ import { PluginRunBoxTabEnum } from './constants';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import React from 'react';
import { onStartChatType } from '../type';
import { ChatBoxInputFormType } from '../ChatBox/type';
export type PluginRunBoxProps = OutLinkChatAuthProps & {
pluginInputs: FlowNodeInputItemType[];
variablesForm: UseFormReturn<FieldValues, any>;
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
histories: ChatSiteItemType[]; // chatHistories[1] is the response
setHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;

View File

@@ -0,0 +1,121 @@
import React, { useMemo } from 'react';
import { FieldArrayWithId } from 'react-hook-form';
import { ChatBoxInputFormType } from '../ChatBox/type';
import { Box, CircularProgress, Flex, HStack, Image } from '@chakra-ui/react';
import MyBox from '@fastgpt/web/components/common/MyBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const RenderFilePreview = ({
fileList,
removeFiles
}: {
fileList: FieldArrayWithId<ChatBoxInputFormType, 'files', 'id'>[];
removeFiles?: (index?: number | number[]) => void;
}) => {
const { isPc } = useSystem();
return fileList.length > 0 ? (
<Flex
maxH={'250px'}
overflowY={'auto'}
wrap={'wrap'}
pt={3}
userSelect={'none'}
mb={fileList.length > 0 ? 2 : 0}
pr={0.5}
>
{fileList.map((item, index) => {
const isFile = item.type === ChatFileTypeEnum.file;
const isImage = item.type === ChatFileTypeEnum.image;
return (
<MyBox
key={index}
maxW={isFile ? 56 : 14}
w={isFile ? '50%' : '12.5%'}
aspectRatio={isFile ? 4 : 1}
pr={1.5}
pb={1.5}
mb={0.5}
>
<Box
border={'sm'}
boxShadow={
'0px 2.571px 6.429px 0px rgba(19, 51, 107, 0.08), 0px 0px 0.643px 0px rgba(19, 51, 107, 0.08)'
}
rounded={'md'}
position={'relative'}
_hover={{
'.close-icon': { display: 'block' }
}}
w={'full'}
h={'full'}
alignItems={'center'}
pl={isFile ? 1 : 0}
>
{removeFiles && (
<MyIcon
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.700'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
rounded={'full'}
bg={'white'}
right={'-8px'}
top={'-8px'}
onClick={() => removeFiles(index)}
className="close-icon"
display={['', 'none']}
zIndex={10}
/>
)}
{isImage && (
<Image
alt={'img'}
src={item.icon}
w={'full'}
h={'full'}
borderRadius={'md'}
objectFit={'contain'}
/>
)}
{isFile && (
<HStack alignItems={'center'} h={'full'}>
<MyIcon name={item.icon as any} w={['1.5rem', '2rem']} h={['1.5rem', '2rem']} />
<Box flex={'1 0 0'} pr={2} className="textEllipsis" fontSize={'xs'}>
{item.name}
</Box>
</HStack>
)}
{/* Process */}
{!item.url && (
<Flex
position={'absolute'}
inset="0"
bg="rgba(255,255,255,0.4)"
alignItems="center"
justifyContent="center"
>
<CircularProgress
value={item.process}
color="primary.600"
bg={'white'}
size={isPc ? '30px' : '35px'}
>
{/* <CircularProgressLabel>{item.process ?? 0}%</CircularProgressLabel> */}
</CircularProgress>
</Flex>
)}
</Box>
</MyBox>
);
})}
</Flex>
) : null;
};
export default React.memo(RenderFilePreview);

View File

@@ -2,14 +2,18 @@ import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { useCallback, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './PluginRunBox/constants';
import { ComponentRef as ChatComponentRef, SendPromptFnType } from './ChatBox/type';
import {
ChatBoxInputFormType,
ComponentRef as ChatComponentRef,
SendPromptFnType
} from './ChatBox/type';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
export const useChat = () => {
const ChatBoxRef = useRef<ChatComponentRef>(null);
const [chatRecords, setChatRecords] = useState<ChatSiteItemType[]>([]);
const variablesForm = useForm();
const variablesForm = useForm<ChatBoxInputFormType>();
// plugin
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);

View File

@@ -4,7 +4,7 @@ import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/cons
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import type { UserChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
@@ -13,7 +13,10 @@ import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { updatePluginInputByVariables } from '@fastgpt/global/core/workflow/utils';
import {
getPluginRunUserQuery,
updatePluginInputByVariables
} from '@fastgpt/global/core/workflow/utils';
import { NextAPI } from '@/service/middleware/entry';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
@@ -28,6 +31,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
export type Props = {
messages: ChatCompletionMessageParam[];
@@ -65,8 +69,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
throw new Error('Edges is not array');
}
const chatMessages = GPTMessages2Chats(messages);
const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined;
// console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length);
/* user auth */
@@ -82,6 +84,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const isPlugin = app.type === AppTypeEnum.plugin;
const userQuestion: UserChatItemType = (() => {
if (isPlugin) {
return getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(app.modules),
variables,
files: variables.files
});
}
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;
if (!latestHumanChat) {
throw new Error('User question is empty');
}
return latestHumanChat;
})();
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, chatMessages));
// Plugin need to replace inputs
@@ -89,7 +107,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
variables = {};
} else {
if (!userInput) {
if (!userQuestion.value) {
throw new Error('Params Error');
}
}
@@ -117,7 +135,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges, chatMessages),
variables,
query: removeEmptyUserInput(userInput),
query: removeEmptyUserInput(userQuestion.value),
chatConfig,
histories: chatMessages,
stream: true,

View File

@@ -49,13 +49,16 @@ import { NextAPI } from '@/service/middleware/entry';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { updatePluginInputByVariables } from '@fastgpt/global/core/workflow/utils';
import {
getPluginRunUserQuery,
updatePluginInputByVariables
} from '@fastgpt/global/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
import { rewriteNodeOutputByHistories } from '@fastgpt/global/core/workflow/runtime/utils';
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { getPluginRunUserQuery } from '@fastgpt/service/core/workflow/utils';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
type FastGptWebChatProps = {
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
@@ -185,8 +188,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get obj=Human history
const userQuestion: UserChatItemType = (() => {
if (isPlugin) {
// TODOget plugin files from variables
return getPluginRunUserQuery({ nodes: app.modules, variables });
return getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(app.modules),
variables,
files: variables.files
});
}
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;

View File

@@ -145,6 +145,7 @@ const DetailLogsModal = ({
{isPlugin ? (
<Box px={5} pt={2} h={'100%'}>
<PluginRunBox
chatConfig={chat?.app?.chatConfig}
pluginInputs={chat?.app.pluginInputs}
variablesForm={variablesForm}
histories={chatRecords}

View File

@@ -408,10 +408,11 @@ export const useWorkflow = () => {
/* node */
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, node: Node) => {
if (node.data.forbidDelete) {
return toast({
toast({
status: 'warning',
title: t('common:core.workflow.Can not delete node')
});
return false;
}
// If the node has child nodes, remove the child nodes
@@ -438,6 +439,8 @@ export const useWorkflow = () => {
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
return true;
});
const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => {
// If the node is not selected and the Ctrl key is pressed, select the node
@@ -506,8 +509,9 @@ export const useWorkflow = () => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node) {
handleRemoveNode(change, node);
// 如果删除失败,则不继续执行
if (node && !handleRemoveNode(change, node)) {
return;
}
} else if (change.type === 'select') {
handleSelectNode(change);

View File

@@ -13,6 +13,11 @@ import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useCreation } from 'ahooks';
import ChatFunctionTip from '@/components/core/app/Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { WorkflowContext } from '../../../context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import FileSelect from '@/components/core/app/FileSelect';
import { userFilesInput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import MyIcon from '@fastgpt/web/components/common/Icon';
type ComponentProps = {
chatConfig: AppChatConfigType;
@@ -60,6 +65,9 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
>
<Container w={'360px'}>
<Instruction {...componentsProps} />
<Box pt={4}>
<FileSelectConfig {...componentsProps} />
</Box>
</Container>
</NodeCard>
);
@@ -72,6 +80,7 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
return (
<>
<Flex>
<MyIcon name={'core/app/simpleMode/chat'} mr={2} w={'20px'} />
<FormLabel color={'myGray.600'} fontWeight={'medium'} fontSize={'14px'}>
{t('workflow:plugin.Instructions')}
</FormLabel>
@@ -100,3 +109,48 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
</>
);
}
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!;
return (
<FileSelect
value={fileSelectConfig}
color={'myGray.600'}
fontWeight={'medium'}
fontSize={'14px'}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.data.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'addOutput',
value: userFilesInput
});
} else {
repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'delOutput',
key: userFilesInput.key
});
}
}}
/>
);
}

View File

@@ -22,6 +22,7 @@ import { WorkflowContext } from '../../../context';
import IOTitle from '../../components/IOTitle';
import dynamic from 'next/dynamic';
import { defaultInput } from './InputEditModal';
import RenderOutput from '../render/RenderOutput';
const FieldEditModal = dynamic(() => import('./InputEditModal'));
@@ -140,9 +141,15 @@ const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}}
/>
</Container>
{!!outputs.length && (
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
)}
</NodeCard>
);
}, [data, inputs, nodeId, onChangeNode, selected, t]);
}, [data, inputs, nodeId, onChangeNode, outputs, selected, t]);
return (
<>

View File

@@ -72,7 +72,7 @@ export const useChatTest = ({
const CustomChatContainer = useMemoizedFn(() =>
appDetail.type === AppTypeEnum.plugin ? (
<Box p={3}>
<Box p={5}>
<PluginRunBox
pluginInputs={pluginInputs}
variablesForm={variablesForm}

View File

@@ -17,7 +17,6 @@ import { useTranslation } from 'next-i18next';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ComplianceTip from '@/components/common/ComplianceTip/index';

View File

@@ -360,7 +360,7 @@ export const emptyTemplates: Record<
nodes: [
{
nodeId: 'pluginInput',
name: i18nT('common:core.module.template.self_input'),
name: i18nT('workflow:template.plugin_start'),
avatar: 'core/workflow/template/workflowStart',
flowNodeType: FlowNodeTypeEnum.pluginInput,
showStatus: false,
@@ -385,6 +385,20 @@ export const emptyTemplates: Record<
version: '481',
inputs: [],
outputs: []
},
{
nodeId: 'pluginConfig',
name: i18nT('common:core.module.template.system_config'),
intro: '',
avatar: 'core/workflow/template/systemConfig',
flowNodeType: FlowNodeTypeEnum.pluginConfig,
position: {
x: 184.66337662472682,
y: -216.05298493910115
},
version: '4811',
inputs: [],
outputs: []
}
],
edges: []