4.8 preview (#1288)

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* perf: workflow ux

* system config

* Newflow (#89)

* docs: Add doc for Xinference (#1266)

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* perf: workflow ux

* system config

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* rename code

* move code

* update flow

* input type selector

* perf: workflow runtime

* feat: node adapt newflow

* feat: adapt plugin

* feat: 360 connection

* check workflow

* perf: flow 性能

* change plugin input type (#81)

* change plugin input type

* plugin label mode

* perf: nodecard

* debug

* perf: debug ui

* connection ui

* change workflow ui (#82)

* feat: workflow debug

* adapt openAPI for new workflow (#83)

* adapt openAPI for new workflow

* i18n

* perf: plugin debug

* plugin input ui

* delete

* perf: global variable select

* fix rebase

* perf: workflow performance

* feat: input render type icon

* input icon

* adapt flow (#84)

* adapt newflow

* temp

* temp

* fix

* feat: app schedule trigger

* feat: app schedule trigger

* perf: schedule ui

* feat: ioslatevm run js code

* perf: workflow varialbe table ui

* feat: adapt simple mode

* feat: adapt input params

* output

* feat: adapt tamplate

* fix: ts

* add if-else module (#86)

* perf: worker

* if else node

* perf: tiktoken worker

* fix: ts

* perf: tiktoken

* fix if-else node (#87)

* fix if-else node

* type

* fix

* perf: audio render

* perf: Parallel worker

* log

* perf: if else node

* adapt plugin

* prompt

* perf: reference ui

* reference ui

* handle ux

* template ui and plugin tool

* adapt v1 workflow

* adapt v1 workflow completions

* perf: time variables

* feat: workflow keyboard shortcuts

* adapt v1 workflow

* update workflow example doc (#88)

* fix: simple mode select tool

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>

* doc

* perf: extract node

* extra node field

* update plugin version

* doc

* variable

* change doc & fix prompt editor (#90)

* fold workflow code

* value type label

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-04-25 17:51:20 +08:00
committed by GitHub
parent b08d81f887
commit 439c819ff1
505 changed files with 23570 additions and 18215 deletions

View File

@@ -2,7 +2,7 @@
"author": "FastGPT Team",
"templateType": "other",
"name": "自定义反馈",
"avatar": "/imgs/module/customFeedback.svg",
"avatar": "/imgs/workflow/customFeedback.svg",
"intro": "该模块被触发时,会给当前的对话记录增加一条反馈。可用于自动记录对话效果等。",
"showStatus": false,
"isTool": false,
@@ -177,7 +177,7 @@
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"label": "core.workflow.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,

View File

@@ -2,194 +2,130 @@
"author": "FastGPT Team",
"templateType": "tools",
"name": "获取当前时间",
"avatar": "/imgs/module/getCurrentTime.svg",
"avatar": "/imgs/workflow/getCurrentTime.svg",
"intro": "获取用户当前时区的时间。",
"showStatus": false,
"isTool": true,
"weight": 10,
"modules": [
"nodes": [
{
"moduleId": "m8dupj",
"nodeId": "lmpb9v2lo2lk",
"name": "定义插件输入",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/module/input.png",
"flowType": "pluginInput",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 187.94161749205568,
"y": 179.78772129776746
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "pluginStart",
"type": "hidden",
"valueType": "boolean",
"label": "插件开始运行",
"description": "插件开始运行时,会输出一个 True 的标识。有时候,插件不会有额外的的输入,为了顺利的进入下一个阶段,你可以将该值连接到下一个节点的触发器中。",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
}
],
"outputs": [
{
"key": "pluginStart",
"label": "插件开始运行",
"type": "source",
"valueType": "boolean",
"targets": [
{
"moduleId": "cv13yt",
"key": "switch"
}
]
}
]
"inputs": [],
"outputs": []
},
{
"moduleId": "bjsa7r",
"nodeId": "i7uow4wj2wdp",
"name": "定义插件输出",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/module/output.png",
"flowType": "pluginOutput",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1176.9471084832217,
"y": 138.94098316727695
"x": 1607.7142331269126,
"y": -151.8669210746189
},
"inputs": [
{
"key": "time",
"valueType": "string",
"label": "time",
"type": "target",
"required": true,
"renderTypeList": ["reference"],
"required": false,
"description": "",
"edit": true,
"canEdit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": false,
"dataType": true,
"inputType": false
"valueType": true
},
"connected": true
"value": ["WNUvWwYBUfEr", "HdIl1GWi0tnc"]
}
],
"outputs": [
{
"key": "time",
"valueType": "string",
"label": "time",
"type": "source",
"edit": true,
"targets": []
}
]
"outputs": []
},
{
"moduleId": "cv13yt",
"nodeId": "WNUvWwYBUfEr",
"name": "文本加工",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"avatar": "/imgs/module/textEditor.svg",
"flowType": "pluginModule",
"flowNodeType": "pluginModule",
"showStatus": false,
"position": {
"x": 600.7190079155914,
"y": 1.4754510232677944
"x": 1030.0794269310472,
"y": -350.61013802286885
},
"inputs": [
{
"key": "pluginId",
"type": "hidden",
"label": "",
"value": "community-textEditor",
"valueType": "string",
"connected": false,
"showTargetInApp": false,
"showTargetInPlugin": false
},
{
"key": "switch",
"type": "triggerAndFinish",
"label": "",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
},
{
"key": "textarea",
"valueType": "string",
"label": "文本内容",
"type": "textarea",
"required": true,
"description": "可以通过 {{key}} 的方式引用传入的变量。变量仅支持字符串或数字。",
"edit": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": false,
"placeholder": "可以通过 {{key}} 的方式引用传入的变量。变量仅支持字符串或数字。",
"value": "{{cTime}}"
},
{
"key": "DYNAMIC_INPUT_KEY",
"valueType": "any",
"label": "需要加工的输入",
"type": "addInputParam",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "可动态的添加字符串类型变量,在文本编辑中通过 {{key}} 使用变量。非字符串类型,会自动转成字符串类型。",
"edit": false,
"description": "",
"canEdit": false,
"value": "",
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": false
"key": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
},
{
"key": "文本",
"valueType": "string",
"label": "文本",
"renderTypeList": ["textarea"],
"required": true,
"description": "",
"canEdit": false,
"value": "{{cTime}}",
"editField": {
"key": true
},
"connected": false
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
}
],
"outputs": [
{
"id": "HdIl1GWi0tnc",
"key": "text",
"valueType": "string",
"label": "core.module.output.label.text",
"type": "source",
"edit": false,
"targets": [
{
"moduleId": "bjsa7r",
"key": "time"
}
]
},
{
"key": "finish",
"label": "",
"description": "",
"valueType": "boolean",
"type": "hidden",
"targets": []
"label": "text",
"type": "static",
"canEdit": false
}
]
],
"pluginId": "community-textEditor"
}
],
"edges": [
{
"source": "lmpb9v2lo2lk",
"target": "WNUvWwYBUfEr",
"sourceHandle": "lmpb9v2lo2lk-source-right",
"targetHandle": "WNUvWwYBUfEr-target-left"
},
{
"source": "WNUvWwYBUfEr",
"target": "i7uow4wj2wdp",
"sourceHandle": "WNUvWwYBUfEr-source-right",
"targetHandle": "i7uow4wj2wdp-target-left"
}
]
}

View File

@@ -2,16 +2,18 @@
"author": "FastGPT Team",
"templateType": "tools",
"name": "文本加工",
"avatar": "/imgs/module/textEditor.svg",
"avatar": "/imgs/workflow/textEditor.svg",
"intro": "可对固定或传入的文本进行加工后输出,非字符串类型数据最终会转成字符串类型。",
"showStatus": false,
"isTool": false,
"weight": 100,
"modules": [
"nodes": [
{
"moduleId": "w90mfp",
"nodeId": "lmpb9v2lo2lk",
"name": "定义插件输入",
"flowType": "pluginInput",
"intro": "自定义配置外部输入,使用插件时,仅暴露自定义配置的输入",
"avatar": "/imgs/workflow/input.png",
"flowNodeType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
@@ -19,313 +21,223 @@
},
"inputs": [
{
"key": "textarea",
"valueType": "string",
"label": "文本内容",
"type": "textarea",
"required": true,
"description": "可以通过 {{key}} 的方式引用传入的变量。变量仅支持字符串或数字。",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
},
{
"key": "DYNAMIC_INPUT_KEY",
"valueType": "any",
"label": "需要加工的输入",
"type": "addInputParam",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"renderTypeList": ["addInputParam"],
"required": false,
"description": "可动态的添加字符串类型变量,在文本编辑中通过 {{key}} 使用变量。非字符串类型,会自动转成字符串类型。",
"edit": true,
"description": "",
"canEdit": true,
"value": "",
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": false
"key": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
},
{
"key": "文本",
"valueType": "string",
"label": "文本",
"renderTypeList": ["textarea"],
"required": true,
"description": "",
"canEdit": true,
"value": "",
"editField": {
"key": true
},
"connected": true
"maxLength": "",
"dynamicParamDefaultValue": {
"inputType": "reference",
"valueType": "string",
"required": true
}
}
],
"outputs": [
{
"key": "textarea",
"id": "ILc8GS7iU53M",
"key": "文本",
"valueType": "string",
"label": "文本内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "text"
}
]
"label": "文本",
"type": "static"
},
{
"key": "DYNAMIC_INPUT_KEY",
"valueType": "any",
"label": "需要加工的输入",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "DYNAMIC_INPUT_KEY"
}
]
"id": "2LCxDnOSculb",
"key": "system_addInputParam",
"valueType": "dynamic",
"label": "动态外部数据",
"type": "static"
}
]
},
{
"moduleId": "tze1ju",
"nodeId": "i7uow4wj2wdp",
"name": "定义插件输出",
"flowType": "pluginOutput",
"intro": "自定义配置外部输出,使用插件时,仅暴露自定义配置的输出",
"avatar": "/imgs/workflow/output.png",
"flowNodeType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1607.7142331269126,
"y": -145.93201540017395
"y": -151.8669210746189
},
"inputs": [
{
"key": "text",
"valueType": "string",
"label": "core.module.output.label.text",
"type": "target",
"required": true,
"label": "text",
"renderTypeList": ["reference"],
"required": false,
"description": "",
"edit": true,
"canEdit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": false,
"dataType": true,
"inputType": false
"valueType": true
},
"connected": true
"value": ["CRT7oIEU8v2P", "vooswj3VxKW8"]
}
],
"outputs": [
{
"id": "HdIl1GWi0tnc",
"key": "text",
"valueType": "string",
"label": "core.module.output.label.text",
"type": "source",
"edit": true,
"targets": []
"label": "text",
"type": "static"
}
]
},
{
"moduleId": "49de3g",
"name": "HTTP模块",
"flowType": "httpRequest468",
"nodeId": "CRT7oIEU8v2P",
"name": "HTTP 请求",
"intro": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"avatar": "/imgs/workflow/http.png",
"flowNodeType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1086.8929621216014,
"y": -451.7550009773506
"x": 1070.8458389994719,
"y": -415.09022555407836
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "custom",
"valueType": "string",
"label": "",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "hidden",
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/textEditor",
"connected": false
},
{
"key": "system_httpHeader",
"type": "custom",
"valueType": "any",
"value": "",
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpParams",
"type": "hidden",
"valueType": "any",
"value": [],
"key": "system_addInputParam",
"renderTypeList": ["addInputParam"],
"valueType": "dynamic",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpJsonBody",
"type": "hidden",
"valueType": "any",
"value": "{\r\n \"text\": \"{{text}}\"\r\n}",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": true
"description": "core.module.input.description.HTTP Dynamic Input",
"editField": {
"key": true,
"valueType": true
},
"value": ["lmpb9v2lo2lk", "2LCxDnOSculb"]
},
{
"key": "text",
"valueType": "string",
"label": "text",
"type": "target",
"required": true,
"renderTypeList": ["reference"],
"description": "",
"edit": true,
"canEdit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
"valueType": true
},
"connected": true
"value": ["lmpb9v2lo2lk", "ILc8GS7iU53M"]
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"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,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
"value": "/api/plugins/textEditor"
},
{
"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 \"text\":\"{{text}}\",\r\n \"system_addInputParam\": {{system_addInputParam}}\r\n}",
"label": "",
"required": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
},
{
"id": "system_addOutputParam",
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"type": "dynamic",
"valueType": "dynamic",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
"valueType": true
}
},
{
"type": "source",
"valueType": "string",
"id": "httpRawResponse",
"key": "httpRawResponse",
"label": "原始响应",
"description": "HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。",
"valueType": "any",
"type": "static"
},
{
"id": "vooswj3VxKW8",
"type": "dynamic",
"key": "text",
"label": "core.module.output.label.text",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "tze1ju",
"key": "text"
}
]
"valueType": "string",
"label": "text"
}
]
}
],
"edges": [
{
"source": "lmpb9v2lo2lk",
"target": "CRT7oIEU8v2P",
"sourceHandle": "lmpb9v2lo2lk-source-right",
"targetHandle": "CRT7oIEU8v2P-target-left"
},
{
"source": "CRT7oIEU8v2P",
"target": "i7uow4wj2wdp",
"sourceHandle": "CRT7oIEU8v2P-source-right",
"targetHandle": "i7uow4wj2wdp-target-left"
}
]
}

View File

@@ -1,368 +0,0 @@
{
"author": "FastGPT Team",
"templateType": "tools",
"name": "判断器",
"avatar": "/imgs/module/tfSwitch.svg",
"intro": "根据传入的内容进行 True False 输出。默认情况下,当传入的内容为 false, undefined, null, 0, none 时,会输出 false。你也可以增加一些自定义的字符串来补充输出 false 的内容。非字符、非数字、非布尔类型,直接输出 True。",
"showStatus": false,
"isTool": false,
"weight": 10,
"modules": [
{
"moduleId": "w90mfp",
"name": "定义插件输入",
"flowType": "pluginInput",
"showStatus": false,
"position": {
"x": 616.4226348688949,
"y": -165.05298493910115
},
"inputs": [
{
"key": "input",
"valueType": "any",
"type": "target",
"label": "core.module.input.label.TFSwitch input tip",
"required": true,
"edit": true,
"connected": true
},
{
"key": "rule",
"valueType": "string",
"label": "core.module.input.label.TFSwitch textarea",
"type": "textarea",
"required": false,
"description": "core.module.input.description.TFSwitch textarea",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
}
],
"outputs": [
{
"key": "input",
"valueType": "any",
"label": "core.module.input.label.TFSwitch input tip",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "8kld99",
"key": "input"
}
]
},
{
"key": "rule",
"valueType": "string",
"label": "core.module.input.label.TFSwitch textarea",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "8kld99",
"key": "rule"
}
]
}
]
},
{
"moduleId": "tze1ju",
"name": "定义插件输出",
"flowType": "pluginOutput",
"showStatus": false,
"position": {
"x": 1985.3791673445353,
"y": -144.90535546692078
},
"inputs": [
{
"key": "true",
"type": "target",
"valueType": "boolean",
"label": "True",
"required": true,
"edit": true,
"connected": true,
"description": ""
},
{
"key": "false",
"valueType": "boolean",
"label": "False",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": false,
"dataType": true,
"inputType": false
},
"connected": true
}
],
"outputs": [
{
"key": "true",
"valueType": "boolean",
"label": "True",
"type": "source",
"edit": true,
"targets": []
},
{
"key": "false",
"valueType": "boolean",
"label": "False",
"type": "source",
"edit": true,
"targets": []
}
]
},
{
"moduleId": "8kld99",
"name": "HTTP模块",
"flowType": "httpRequest468",
"showStatus": true,
"position": {
"x": 1210.560012858087,
"y": -387.62433050951756
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"description": "core.module.input.description.Trigger",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "custom",
"valueType": "string",
"label": "",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "hidden",
"valueType": "string",
"label": "",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/TFSwitch",
"connected": false
},
{
"key": "system_httpHeader",
"type": "custom",
"valueType": "any",
"label": "",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpParams",
"type": "hidden",
"valueType": "any",
"value": [],
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpJsonBody",
"type": "hidden",
"valueType": "any",
"value": "{\r\n \"input\": \"{{input}}\",\r\n \"rule\": \"{{rule}}\"\r\n}",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": false
},
{
"key": "input",
"valueType": "any",
"label": "input",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "rule",
"valueType": "string",
"label": "rule",
"type": "target",
"required": false,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "hidden",
"targets": []
},
{
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
}
},
{
"type": "source",
"valueType": "boolean",
"key": "true",
"label": "true",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "tze1ju",
"key": "true"
}
]
},
{
"type": "source",
"valueType": "boolean",
"key": "false",
"label": "false",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "tze1ju",
"key": "false"
}
]
}
]
}
]
}

View File

@@ -8,15 +8,6 @@ const nextConfig = {
reactStrictMode: process.env.NODE_ENV === 'development' ? false : true,
compress: true,
webpack(config, { isServer }) {
if (!isServer) {
config.resolve = {
...config.resolve,
fallback: {
...config.resolve.fallback,
fs: false
}
};
}
Object.assign(config.resolve.alias, {
'@mongodb-js/zstd': false,
'@aws-sdk/credential-providers': false,
@@ -41,6 +32,45 @@ const nextConfig = {
unknownContextCritical: false
};
if (isServer) {
config.externals.push('isolated-vm');
config.externals.push('worker_threads');
if (config.name === 'server') {
// config.output.globalObject = 'self';
const oldEntry = config.entry;
config = {
...config,
async entry(...args) {
const entries = await oldEntry(...args);
return {
...entries,
'worker/htmlStr2Md': path.resolve(
process.cwd(),
'../../packages/service/worker/htmlStr2Md.ts'
),
'worker/countGptMessagesTokens': path.resolve(
process.cwd(),
'../../packages/service/worker/tiktoken/countGptMessagesTokens.ts'
)
};
}
};
}
} else {
config.resolve = {
...config.resolve,
fallback: {
...config.resolve.fallback,
fs: false
}
};
if (!config.externals) {
config.externals = [];
}
}
return config;
},
transpilePackages: ['@fastgpt/*'],

View File

@@ -23,6 +23,7 @@
"@fastgpt/service": "workspace:*",
"@fastgpt/web": "workspace:*",
"@fortaine/fetch-event-source": "^3.0.6",
"@node-rs/jieba": "1.10.0",
"@tanstack/react-query": "^4.24.10",
"@types/nprogress": "^0.2.0",
"axios": "^1.5.1",
@@ -39,7 +40,6 @@
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"mermaid": "^10.2.3",
"@node-rs/jieba": "1.10.0",
"nanoid": "^4.0.1",
"next": "13.5.2",
"next-i18next": "15.2.0",

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 893 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 503 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2927 3.37501C14.4606 2.89006 14.0406 2.40519 13.5366 2.50225L10.702 3.04821C10.198 3.14526 9.98814 3.75145 10.3242 4.13935L10.6298 4.49219L7.33594 7.29243H2.67613C2.16053 7.29243 1.74255 7.7104 1.74255 8.226C1.74255 8.7416 2.16053 9.15957 2.67613 9.15957H7.46753C7.54295 9.15957 7.61629 9.15063 7.68653 9.13374C7.90786 9.1392 8.13194 9.0664 8.31389 8.91172L11.8524 5.90351L12.2143 6.32125C12.5503 6.70915 13.1803 6.58782 13.3482 6.10287L14.2927 3.37501ZM9.04488 9.24647C8.81019 9.44721 8.78266 9.80019 8.98339 10.0349L10.8753 12.2468L10.4929 12.5816C10.1067 12.9196 10.2312 13.5489 10.717 13.7143L13.4496 14.645C13.9354 14.8104 14.4181 14.3879 14.3185 13.8845L13.7582 11.0527C13.6586 10.5492 13.0513 10.3424 12.6652 10.6804L12.2804 11.0171L10.4023 8.82125C10.2016 8.58656 9.84861 8.55903 9.61392 8.75977L9.04488 9.24647Z" fill="#00A9A6"/>
</svg>

After

Width:  |  Height:  |  Size: 995 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 776 B

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

View File

@@ -108,6 +108,9 @@
"Load Failed": "Load Failed",
"Loading": "Loading",
"More settings": "More settings",
"MultipleRowSelect": {
"No data": "No data"
},
"Name": "Name",
"Name Can": "Name Can't Be Empty",
"Name is empty": "Name is empty",
@@ -276,8 +279,12 @@
"App intro": "App intro",
"App params config": "App Config",
"Chat Variable": "",
"Config schedule plan": "Config schedule config",
"Config whisper": "Config whisper",
"External using": "External use",
"Interval timer config": "Interval timer config",
"Interval timer run": "Timed execution",
"Interval timer tip": "Applications can be executed periodically",
"Make a brief introduction of your app": "Make a brief introduction of your app",
"Max histories": "Dialog round",
"Max tokens": "Max tokens",
@@ -293,6 +300,7 @@
"Select app from template": "Select from the template",
"Select quote template": "Select quote template",
"Set a name for your app": "App name",
"Setting ai property": "Click Configure AI model properties",
"Share link": "Share",
"Share link desc": "Share links with other users and use them directly without logging in",
"Share link desc detail": "You can share the model directly with other users to have a conversation, and the other user can have a conversation directly without logging in. Note that this function will consume the balance of your account, please keep the link!",
@@ -306,6 +314,7 @@
"Tool call tip": "The AI model automatically selects one or more tools to use. If this feature is enabled, the knowledge base call is also called as a tool. Please try to choose the AI model that supports \"function call\", the effect is better.",
"ToolCall": {
"No plugin": "No plugins",
"Parameter setting": "",
"Setting tool": "Setting tool",
"System": "System",
"Team": "Team",
@@ -358,6 +367,15 @@
"Show History": "Show History",
"Web Link": "Web Link"
},
"schedule": {
"Default prompt": "Default prompt",
"Default prompt placeholder": "Default problem when executing the application",
"Every day": "Every {{hour}} hour",
"Every month": "{{day}} {{hour}}:00 Every month",
"Every week": "Every week {{day}} {{hour}}:00",
"Interval": "Per {{interval}} hour",
"Open schedule": "Enable schedule"
},
"setting": "App Setting",
"share": {
"Amount limit tip": "A maximum of 10 groups can be created",
@@ -415,6 +433,7 @@
},
"chat": {
"Admin Mark Content": "Corrected response",
"Audio Not Support": "The device does not support voice play",
"Audio Speech Error": "Audio Speech Error",
"Cancel Speak": "Cancel speak",
"Canceled Speak": "Voice input has been cancelled",
@@ -505,6 +524,7 @@
"module http body": "Body",
"module http result": "Response",
"module http url": "Request Url",
"module if else Result": "if-else Result",
"module limit": "Count Limit",
"module maxToken": "MaxTokens",
"module model": "Model",
@@ -831,6 +851,7 @@
"Input description": "",
"label": "Dataset quote"
},
"Default Value": "Default value",
"Default value": "Default ",
"Default value placeholder": "Null characters are returned by default",
"Edit intro": "Edit",
@@ -842,13 +863,22 @@
"Http request settings": "Request settings",
"Input Type": "Input Type",
"Laf sync params": "Sync params",
"Max Length": "Max Length",
"Max Length placeholder": "The max length of input text",
"Max Value": "Max Value",
"Min Value": "",
"Model List": "Model list",
"No Config Tips": "no configuration",
"Output Type": "Output Type",
"Plugin output must connect": "Custom outputs must all be connected",
"Plugin tool Description": "",
"QueryExtension": {
"placeholder": "Questions about python introduction and usage, etc. The current conversation is related to the game GTA5.",
"tip": "Describes the scope of the current conversation, making it easier for the AI to complete first or vague questions, thereby enhancing the knowledge base's ability to continue conversations.If \n is empty, the question completion function is not used in the first conversation. "
},
"Quote prompt setting": "Quote prompt setting",
"Qupte prompt setting": "",
"Select Data List": "Data list",
"Select app": "Select app",
"Setting quote prompt": "Setting quote prompt",
"Unlink tip": "[{{name}}] An unfilled or unconnected parameter exists",
@@ -857,7 +887,8 @@
"Variable import": "Variable import",
"edit": {
"Field Already Exist": "Key already exist",
"Field Edit": "Field Edit"
"Field Edit": "Field Edit",
"Field Name Cannot Be Empty": "Field Name Cannot Be Empty"
},
"extract": {
"Add field": "Add",
@@ -890,8 +921,10 @@
"input": {
"Add Input": "Add Input",
"Input Number": "Input: {{length}}",
"add": "Add condition",
"description": {
"Background": "",
"HTTP Dynamic Input": "Receive output values from preceding nodes as variables, which can be used as parameters in HTTP requests.",
"Http Request Header": "User-defined request header, please strictly fill in the JSON string.\n1. Make sure the last attribute has no commas\n2. Make sure key contains double quotes\nFor example: {\"Authorization\":\"Bearer xxx\"}",
"Http Request Url": "New HTTP request address. If two 'request addresses' appear, the module can be deleted and rejoined, and the latest module configuration will be pulled.",
"Quote": "Object array format, structure:\n[{q:' question ',a:' answer '}]",
@@ -924,17 +957,6 @@
"Classify background": "For example:\n1.AIGC (Artificial Intelligence Generates content) refers to the automatic or semi-automatic generation of digital content, such as text, images, music, videos, and so on, using artificial intelligence technologies. AIGC technologies include, but are not limited to, natural language processing, computer vision, machine learning, and deep learning. These technologies can create new content or modify existing content to meet specific creative, educational, entertainment or information needs."
}
},
"inputType": {
"chat history": "History",
"dynamicTargetInput": "dynamic Target Input",
"input": "Input",
"selectApp": "App Selector",
"selectDataset": "Dataset Selector",
"selectLLMModel": "Select Chat Model",
"switch": "Switch",
"target": "Target Data",
"textarea": "Textarea"
},
"laf": {
"Select laf function": ""
},
@@ -1051,6 +1073,63 @@
"shareChat": {
"Init Error": "Init Chat Error",
"Init History Error": "Init History Error"
},
"workflow": {
"Add variable": "Add",
"Can not delete node": "Can not delete the node",
"Change input type tip": "Changing the input type will empty the entered values, please confirm!",
"Check Failed": "Workflow verification fails. Check whether the node or connection is normal",
"Confirm stop debug": "Do you want to terminate debugging? Debugging information is not retained.",
"Copy node": "Copy node",
"Custom inputs": "Inputs",
"Custom outputs": "Outputs",
"Custom variable": "Custom variable",
"Dataset quote": "Dataset quote",
"Debug": "Debug",
"Debug Node": "Workflow Debug",
"Failed": "Running failed",
"Not intro": "This node is not introduced",
"Run from here": "Run from here",
"Run result": "Run result",
"Running": "Running",
"Skipped": "Skipped",
"Stop debug": "Stop",
"Success": "Running success",
"Value type": "Type",
"chat": {
"Quote prompt": "Quote prompt"
},
"debug": {
"Done": "Done",
"Hide result": "Hide result",
"Not result": "Not result",
"Run result": "",
"Show result": "Show result"
},
"inputType": {
"JSON Editor": "JSON Editor",
"Manual input": "",
"Manual select": "Select",
"Reference": "Reference",
"Required": "Required",
"Select edit field": "Editable field",
"Select input default value": "Default value",
"Select input type": "Configurable input types",
"Select input type placeholder": "Please select a configurable input type",
"chat history": "History",
"dynamicTargetInput": "dynamic Target Input",
"input": "Input",
"number input": "number input",
"selectApp": "App Selector",
"selectDataset": "Dataset Selector",
"selectLLMModel": "Select Chat Model",
"switch": "Switch",
"target": "Target Data",
"textarea": "Textarea"
},
"tool": {
"Select Tool": "Select Tool"
}
}
},
"dataset": {

View File

@@ -25,7 +25,7 @@
"Connection type is different": "连接的类型不一致",
"Copy Module Config": "复制配置",
"Dataset Quote Template": "知识库问答模式",
"Export Config Successful": "已复制配置,请注意检查是否有重要数据",
"Export Config Successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据",
"Export Configs": "导出配置",
"Feedback Count": "用户反馈",
"Import Configs": "导入配置",
@@ -108,6 +108,9 @@
"Load Failed": "加载失败",
"Loading": "加载中...",
"More settings": "更多设置",
"MultipleRowSelect": {
"No data": "没有可选值"
},
"Name": "名称",
"Name Can": "名称不能为空",
"Name is empty": "名称不能为空",
@@ -276,8 +279,12 @@
"App intro": "应用介绍",
"App params config": "应用配置",
"Chat Variable": "对话框变量",
"Config schedule plan": "配置定时执行",
"Config whisper": "配置语音输入",
"External using": "外部使用途径",
"Interval timer config": "定时执行配置",
"Interval timer run": "定时执行",
"Interval timer tip": "可定时执行应用",
"Make a brief introduction of your app": "给你的 AI 应用一个介绍",
"Max histories": "聊天记录数量",
"Max tokens": "回复上限",
@@ -293,6 +300,7 @@
"Select app from template": "从模板中选择",
"Select quote template": "选择引用提示模板",
"Set a name for your app": "给应用设置一个名称",
"Setting ai property": "点击配置AI模型相关属性",
"Share link": "免登录窗口",
"Share link desc": "分享链接给其他用户,无需登录即可直接进行使用",
"Share link desc detail": "可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的余额,请保管好链接!",
@@ -306,6 +314,7 @@
"Tool call tip": "通过AI模型自动选择一个或多个工具进行使用。如果启动该功能知识库调用也会被当做一个工具调用。请尽量选择支持“函数调用”的AI模型效果更好。",
"ToolCall": {
"No plugin": "没有可用的插件",
"Parameter setting": "输入参数",
"Setting tool": "配置工具",
"System": "系统",
"Team": "团队",
@@ -358,6 +367,15 @@
"Show History": "展示历史对话",
"Web Link": "网络链接"
},
"schedule": {
"Default prompt": "默认问题",
"Default prompt placeholder": "执行应用时的默认问题",
"Every day": "每天{{hour}}:00",
"Every month": "每月{{day}}号{{hour}}:00",
"Every week": "每周{{day}} {{hour}}:00",
"Interval": "每{{interval}}小时",
"Open schedule": "定时执行"
},
"setting": "应用信息设置",
"share": {
"Amount limit tip": "最多创建10组",
@@ -415,6 +433,7 @@
},
"chat": {
"Admin Mark Content": "纠正后的回复",
"Audio Not Support": "设备不支持语音播放",
"Audio Speech Error": "语音播报异常",
"Cancel Speak": "取消语音输入",
"Canceled Speak": "语音输入已取消",
@@ -505,6 +524,7 @@
"module http body": "请求体",
"module http result": "响应体",
"module http url": "请求地址",
"module if else Result": "判断器结果",
"module limit": "单次搜索上限",
"module maxToken": "最大响应 Tokens",
"module model": "模型",
@@ -611,8 +631,7 @@
"success": "开始同步"
}
},
"training": {
}
"training": {}
},
"data": {
"Auxiliary Data": "辅助数据",
@@ -833,6 +852,7 @@
"Input description": "可接收知识库搜索的结果。",
"label": "知识库引用"
},
"Default Value": "默认值",
"Default value": "默认值",
"Default value placeholder": "不填则默认返回空字符",
"Edit intro": "编辑描述",
@@ -844,13 +864,22 @@
"Http request settings": "请求配置",
"Input Type": "输入类型",
"Laf sync params": "同步参数",
"Max Length": "最大长度",
"Max Length placeholder": "输入文本的最大长度",
"Max Value": "最大值",
"Min Value": "最小值",
"Model List": "模型列表",
"No Config Tips": "无可配置项",
"Output Type": "输出类型",
"Plugin output must connect": "自定义输出必须全部连接",
"Plugin tool Description": "工具参数需要描述",
"QueryExtension": {
"placeholder": "例如:\n关于 python 的介绍和使用等问题。\n当前对话与游戏《GTA5》有关。",
"tip": "描述当前对话的范围便于AI补全首次问题或模糊的问题从而增强知识库连续对话的能力。建议开启该功能后都简单的描述在对话的背景否则容易造成补全对象不准确。"
},
"Quote prompt setting": "引用提示词配置",
"Qupte prompt setting": "",
"Select Data List": "数据列表",
"Select app": "选择应用",
"Setting quote prompt": "配置引用提示词",
"Unlink tip": "【{{name}}】存在未填或未连接参数",
@@ -859,7 +888,8 @@
"Variable import": "外部参数输入",
"edit": {
"Field Already Exist": "key 重复",
"Field Edit": "字段编辑"
"Field Edit": "字段编辑",
"Field Name Cannot Be Empty": "字段名不能为空"
},
"extract": {
"Add field": "新增字段",
@@ -892,8 +922,10 @@
"input": {
"Add Input": "添加入参",
"Input Number": "入参: {{length}}",
"add": "添加条件",
"description": {
"Background": "你可以添加一些特定内容的介绍,从而更好的识别用户的问题类型。这个内容通常是给模型介绍一个它不知道的内容。",
"HTTP Dynamic Input": "接收前方节点的输出值作为变量这些变量可以被HTTP请求参数使用。",
"Http Request Header": "自定义请求头请严格填入JSON字符串。\n1. 确保最后一个属性没有逗号\n2. 确保 key 包含双引号\n例如: {\"Authorization\":\"Bearer xxx\"}",
"Http Request Url": "新的HTTP请求地址。如果出现两个“请求地址”可以删除该模块重新加入会拉取最新的模块配置。",
"Quote": "对象数组格式,结构:\n [{q:'问题',a:'回答'}]",
@@ -926,17 +958,6 @@
"Classify background": "例如: \n1. AIGC人工智能生成内容是指使用人工智能技术自动或半自动地生成数字内容如文本、图像、音乐、视频等。\n2. AIGC技术包括但不限于自然语言处理、计算机视觉、机器学习和深度学习。这些技术可以创建新内容或修改现有内容以满足特定的创意、教育、娱乐或信息需求。"
}
},
"inputType": {
"chat history": "历史记录",
"dynamicTargetInput": "动态外部数据",
"input": "输入框",
"selectApp": "应用选择",
"selectDataset": "知识库选择",
"selectLLMModel": "对话模型选择",
"switch": "开关",
"target": "外部数据",
"textarea": "段落输入"
},
"laf": {
"Select laf function": "选择laf函数"
},
@@ -1053,6 +1074,63 @@
"shareChat": {
"Init Error": "初始化对话框失败",
"Init History Error": "初始化聊天记录失败"
},
"workflow": {
"Add variable": "新增变量",
"Can not delete node": "该节点不允许删除",
"Change input type tip": "修改输入类型会清空已填写的值,请确认!",
"Check Failed": "工作流校验失败,请检查节点是否正确填值,以及连线是否正常",
"Confirm stop debug": "确认终止调试?调试信息将会不保留。",
"Copy node": "已复制节点",
"Custom inputs": "自定义输入",
"Custom outputs": "自定义输出",
"Custom variable": "自定义变量",
"Dataset quote": "知识库引用",
"Debug": "调试",
"Debug Node": "Debug模式",
"Failed": "运行失败",
"Not intro": "这个节点没有介绍~\\",
"Run from here": "从这里开始运行",
"Run result": "",
"Running": "运行中",
"Skipped": "跳过运行",
"Stop debug": "停止调试",
"Success": "运行成功",
"Value type": "数据类型",
"chat": {
"Quote prompt": "引用提示词"
},
"debug": {
"Done": "完成调试",
"Hide result": "隐藏结果",
"Not result": "无运行结果",
"Run result": "运行结果",
"Show result": "展示结果"
},
"inputType": {
"JSON Editor": "JSON 输入框",
"Manual input": "手动输入",
"Manual select": "手动选择",
"Reference": "变量引用",
"Required": "必填",
"Select edit field": "可编辑内容",
"Select input default value": "默认值",
"Select input type": "可配置的输入类型",
"Select input type placeholder": "请选择可配置的输入类型",
"chat history": "历史记录",
"dynamicTargetInput": "动态外部数据",
"input": "单行输入框",
"number input": "数字输入框",
"selectApp": "应用选择",
"selectDataset": "知识库选择",
"selectLLMModel": "对话模型选择",
"switch": "开关",
"target": "外部数据",
"textarea": "多行输入框"
},
"tool": {
"Select Tool": "选择工具"
}
}
},
"dataset": {

View File

@@ -14,7 +14,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
w={w}
h={w}
p={'1px'}
src={src}
src={src || LOGO_ICON}
{...props}
/>
);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { DispatchNodeResponseType } from '@fastgpt/global/core/module/runtime/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
const ContextModal = ({
context = [],

View File

@@ -1,8 +1,8 @@
import React, { useContext, createContext, useState, useMemo, useEffect, useCallback } from 'react';
import { useAudioPlay } from '@/web/common/utils/voice';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { splitGuideModule } from '@fastgpt/global/core/module/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import {
AppTTSConfigType,
AppWhisperConfigType,
@@ -91,7 +91,7 @@ const StateContext = createContext<useChatStoreType>({
});
export type ChatProviderProps = OutLinkChatAuthProps & {
userGuideModule?: ModuleItemType;
userGuideModule?: StoreNodeItemType;
// not chat test params
chatId?: string;

View File

@@ -1,15 +1,14 @@
import React, { useMemo, useState } from 'react';
import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/module/runtime/type.d';
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
import { Flex, BoxProps, useDisclosure, useTheme, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic';
import Tag from '../Tag';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyTooltip from '../MyTooltip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools';
@@ -99,11 +98,6 @@ const ResponseTags = ({
};
}, [showDetail, flowResponses]);
const TagStyles: BoxProps = {
mr: 2,
bg: 'transparent'
};
return flowResponses.length === 0 ? null : (
<>
{sourceList.length > 0 && (
@@ -150,52 +144,52 @@ const ResponseTags = ({
</>
)}
{showDetail && (
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'}>
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'} gap={2}>
{quoteList.length > 0 && (
<MyTooltip label="查看引用">
<Tag
<MyTag
colorSchema="blue"
type="solid"
cursor={'pointer'}
{...TagStyles}
onClick={() => setQuoteModalData({ rawSearch: quoteList })}
>
{quoteList.length}
</Tag>
</MyTag>
</MyTooltip>
)}
{llmModuleAccount === 1 && (
<>
{historyPreview.length > 0 && (
<MyTooltip label={'点击查看上下文预览'}>
<Tag
<MyTag
colorSchema="green"
cursor={'pointer'}
{...TagStyles}
type="solid"
onClick={() => setContextModalData(historyPreview)}
>
{historyPreview.length}
</Tag>
</MyTag>
</MyTooltip>
)}
</>
)}
{llmModuleAccount > 1 && (
<Tag colorSchema="blue" {...TagStyles}>
<MyTag type="solid" colorSchema="blue">
AI
</Tag>
</MyTag>
)}
{isPc && runningTime > 0 && (
<MyTooltip label={'模块运行时间和'}>
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
<MyTag colorSchema="purple" type="solid" cursor={'default'}>
{runningTime}s
</Tag>
</MyTag>
</MyTooltip>
)}
<MyTooltip label={t('core.chat.response.Read complete response tips')}>
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
<MyTag colorSchema="gray" type="solid" cursor={'pointer'} onClick={onOpenWholeModal}>
{t('core.chat.response.Read complete response')}
</Tag>
</MyTag>
</MyTooltip>
</Flex>
)}

View File

@@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@fastgpt/global/core/module/template/constants';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import Tabs from '../Tabs';
import MyModal from '@fastgpt/web/components/common/MyModal';
@@ -34,12 +34,13 @@ function Row({
{t(label)}:
</Box>
<Box
borderRadius={'md'}
borderRadius={'sm'}
fontSize={'sm'}
bg={'myGray.50'}
{...(isCodeBlock
? { transform: 'translateY(-3px)' }
: value
? { px: 3, py: 1, border: theme.borders.base }
? { px: 3, py: 2, border: theme.borders.base }
: {})}
>
{value && <Markdown source={strValue} />}
@@ -86,12 +87,14 @@ const WholeResponseModal = ({
export default WholeResponseModal;
const ResponseBox = React.memo(function ResponseBox({
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail
showDetail,
hideTabs = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
}) {
const theme = useTheme();
const { t } = useTranslation();
@@ -105,7 +108,7 @@ const ResponseBox = React.memo(function ResponseBox({
mr={2}
src={
item.moduleLogo ||
moduleTemplatesFlat.find((template) => item.moduleType === template.flowType)
moduleTemplatesFlat.find((template) => item.moduleType === template.flowNodeType)
?.avatar
}
alt={''}
@@ -125,9 +128,11 @@ const ResponseBox = React.memo(function ResponseBox({
return (
<>
<Box>
<Tabs list={list} activeId={currentTab} onChange={setCurrentTab} />
</Box>
{!hideTabs && (
<Box>
<Tabs list={list} activeId={currentTab} onChange={setCurrentTab} />
</Box>
)}
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
@@ -222,6 +227,7 @@ const ResponseBox = React.memo(function ResponseBox({
{/* classify question */}
<>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
<Row
label={t('core.chat.response.module cq')}
value={(() => {
@@ -229,7 +235,14 @@ const ResponseBox = React.memo(function ResponseBox({
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
</>
{/* if-else */}
<>
<Row
label={t('core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}

View File

@@ -1,5 +1,4 @@
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useAudioPlay } from '@/web/common/utils/voice';
import { Flex, FlexProps, Image, css, useTheme } from '@chakra-ui/react';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';

View File

@@ -26,6 +26,7 @@ import {
} from '@fastgpt/global/core/chat/constants';
import FilesBlock from './FilesBox';
import { useChatProviderStore } from '../Provider';
import Avatar from '@/components/Avatar';
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -157,7 +158,7 @@ ${JSON.stringify(questionGuides)}`;
color: 'primary.600'
}}
>
<Image src={tool.toolAvatar} alt={''} w={'14px'} mr={2} />
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'14px'} mr={2} />
<Box mr={1}>{tool.toolName}</Box>
{isChatting && !tool.response && (
<MyIcon name={'common/loading'} w={'14px'} />

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatBoxInputFormType } from '../type.d';

View File

@@ -21,9 +21,9 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { Box, Flex, Checkbox } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/module/runtime/constants';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -52,7 +52,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { formatChatValue2InputType } from './utils';
import { textareaMinH } from './constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/module/runtime/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import ChatProvider, { useChatProviderStore } from './Provider';
import ChatItem from './components/ChatItem';
@@ -79,7 +79,7 @@ type Props = OutLinkChatAuthProps & {
showEmptyIntro?: boolean;
appAvatar?: string;
userAvatar?: string;
userGuideModule?: ModuleItemType;
userGuideModule?: StoreNodeItemType;
showFileSelector?: boolean;
active?: boolean; // can use
appId: string;
@@ -588,7 +588,7 @@ const ChatBox = (
setLoading(false);
};
},
[chatHistories, onDelMessage, sendPrompt, setLoading, toast]
[chatHistories, onDelMessage, sendPrompt, setChatHistories, setLoading, toast]
);
// delete one message(One human and the ai response)
const delOneMessage = useCallback(

View File

@@ -5,7 +5,7 @@ import {
ChatSiteItemType,
ToolModuleResponseItemType
} from '@fastgpt/global/core/chat/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/module/runtime/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export type generatingMessageProps = {
event: `${SseResponseEventEnum}`;

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Flex, Box, FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
type Props = FlexProps & {
text?: string | React.ReactNode;
};
const EmptyTip = ({ text, ...props }: Props) => {
const { t } = useTranslation();
return (
<Flex mt={5} flexDirection={'column'} alignItems={'center'} pt={'10vh'} {...props}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{text || t('common.empty.Common Tip')}
</Box>
</Flex>
);
};
export default EmptyTip;

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { Box, useColorMode, Flex } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -10,8 +10,8 @@ import { getUnreadCount } from '@/web/support/user/inform/api';
import dynamic from 'next/dynamic';
import Auth from './auth';
import Navbar from './navbar';
import NavbarPhone from './navbarPhone';
const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone'));
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
@@ -42,7 +42,6 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const { colorMode, setColorMode } = useColorMode();
const { Loading } = useLoading();
const { loading, setScreenWidth, isPc, feConfigs, isNotSufficientModal } = useSystemStore();
const { userInfo } = useUserStore();
@@ -52,12 +51,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
[router.pathname, router.query]
);
useEffect(() => {
if (colorMode === 'dark' && router.pathname !== '/chat') {
setColorMode('light');
}
}, [colorMode, router.pathname, setColorMode]);
// listen screen width
useEffect(() => {
const resize = throttle(() => {
setScreenWidth(document.documentElement.clientWidth);

View File

@@ -1,135 +0,0 @@
import React, { useRef, useState } from 'react';
import {
Menu,
MenuList,
MenuItem,
Box,
useOutsideClick,
MenuButton,
MenuItemProps
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
type MenuItemType = 'primary' | 'danger';
interface Props {
width?: number | string;
offset?: [number, number];
Button: React.ReactNode;
trigger?: 'hover' | 'click';
menuList: {
isActive?: boolean;
label: string | React.ReactNode;
icon?: string;
type?: MenuItemType;
onClick: () => any;
}[];
}
const MyMenu = ({
width = 'auto',
trigger = 'hover',
offset = [0, 5],
Button,
menuList
}: Props) => {
const typeMapStyle: Record<MenuItemType, MenuItemProps> = {
primary: {
_hover: {
backgroundColor: 'primary.50',
color: 'primary.600'
}
},
danger: {
_hover: {
color: 'red.600',
background: 'red.1'
}
}
};
const menuItemStyles: MenuItemProps = {
borderRadius: 'sm',
py: 3,
display: 'flex',
alignItems: 'center'
};
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
const [isOpen, setIsOpen] = useState(false);
useOutsideClick({
ref: ref,
handler: () => {
setIsOpen(false);
}
});
return (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'} isLazy>
<Box
ref={ref}
onMouseEnter={() => {
if (trigger === 'hover') {
setIsOpen(true);
}
clearTimeout(closeTimer.current);
}}
onMouseLeave={() => {
if (trigger === 'hover') {
closeTimer.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}
}}
>
<Box
position={'relative'}
onClickCapture={() => {
if (trigger === 'click') {
setIsOpen(!isOpen);
}
}}
>
<MenuButton
w={'100%'}
h={'100%'}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
/>
<Box position={'relative'}>{Button}</Box>
</Box>
<MenuList
minW={isOpen ? `${width}px !important` : 0}
p={'6px'}
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
}
>
{menuList.map((item, i) => (
<MenuItem
key={i}
{...menuItemStyles}
{...typeMapStyle[item.type || 'primary']}
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
item.onClick && item.onClick();
}}
color={item.isActive ? 'primary.700' : 'myGray.600'}
whiteSpace={'pre-wrap'}
>
{!!item.icon && <MyIcon name={item.icon as any} w={'16px'} mr={2} />}
{item.label}
</MenuItem>
))}
</MenuList>
</Box>
</Menu>
);
};
export default MyMenu;

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -9,7 +9,7 @@ import { HUGGING_FACE_ICON, LOGO_ICON } from '@fastgpt/global/common/system/cons
import { Box, Flex } from '@chakra-ui/react';
import Avatar from '../Avatar';
const AIModelSelector = ({ list, ...props }: SelectProps) => {
const AIModelSelector = ({ list, onchange, ...props }: SelectProps) => {
const { t } = useTranslation();
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
const router = useRouter();
@@ -50,19 +50,20 @@ const AIModelSelector = ({ list, ...props }: SelectProps) => {
: avatarList;
}, [feConfigs.show_pay, avatarList, t]);
const onSelect = useCallback(
(e: string) => {
if (e === 'price') {
router.push(AI_POINT_USAGE_CARD_ROUTE);
return;
}
onchange?.(e);
},
[onchange, router]
);
return (
<>
<MySelect
list={expandList}
{...props}
onchange={(e) => {
if (e === 'price') {
router.push(AI_POINT_USAGE_CARD_ROUTE);
return;
}
props.onchange?.(e);
}}
/>
<MySelect list={expandList} {...props} onchange={onSelect} />
</>
);
};

View File

@@ -1,53 +0,0 @@
import React, { useMemo } from 'react';
import { Flex, type FlexProps } from '@chakra-ui/react';
interface Props extends FlexProps {
children: React.ReactNode | React.ReactNode[];
colorSchema?: 'blue' | 'green' | 'gray' | 'purple';
}
const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
const theme = useMemo(() => {
const map = {
blue: {
borderColor: 'primary.500',
bg: '#F2FBFF',
color: 'primary.600'
},
green: {
borderColor: '#67c13b',
bg: '#f8fff8',
color: '#67c13b'
},
purple: {
borderColor: '#A558C9',
bg: '#F6EEFA',
color: '#A558C9'
},
gray: {
borderColor: 'borderColor.base',
bg: 'myGray.50',
color: 'myGray.700'
}
};
return map[colorSchema];
}, [colorSchema]);
return (
<Flex
{...theme}
borderWidth={'1px'}
px={2}
lineHeight={1}
py={1}
borderRadius={'sm'}
fontSize={'xs'}
alignItems={'center'}
{...props}
>
{children}
</Flex>
);
};
export default Tag;

View File

@@ -14,8 +14,8 @@ import {
} from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { SettingAIDataType } from '@fastgpt/global/core/module/node/type.d';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import { getDocPath } from '@/web/common/system/doc';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
@@ -42,7 +42,7 @@ const AIChatSettingsModal = ({
defaultValues: defaultData
});
const model = watch('model');
const showResponseAnswerText = watch(ModuleInputKeyEnum.aiChatIsResponseText) !== undefined;
const showResponseAnswerText = watch(NodeInputKeyEnum.aiChatIsResponseText) !== undefined;
const showMaxHistoriesSlider = watch('maxHistories') !== undefined;
const selectedModel = llmModelList.find((item) => item.model === model) || llmModelList[0];
@@ -72,7 +72,7 @@ const AIChatSettingsModal = ({
return (
<MyModal
isOpen
iconSrc="/imgs/module/AI.png"
iconSrc="/imgs/workflow/AI.png"
onClose={onClose}
title={
<>
@@ -136,7 +136,7 @@ const AIChatSettingsModal = ({
<QuestionTip ml={1} label={t('core.module.template.AI support tool tip')} />
</Box>
<Box flex={1} ml={'10px'}>
{selectedModel?.usedInToolCall ? '支持' : '不支持'}
{selectedModel?.toolChoice || selectedModel?.functionCall ? '支持' : '不支持'}
</Box>
</Flex>
<Flex mt={8}>
@@ -152,9 +152,9 @@ const AIChatSettingsModal = ({
width={'95%'}
min={0}
max={10}
value={getValues(ModuleInputKeyEnum.aiChatTemperature)}
value={getValues(NodeInputKeyEnum.aiChatTemperature)}
onChange={(e) => {
setValue(ModuleInputKeyEnum.aiChatTemperature, e);
setValue(NodeInputKeyEnum.aiChatTemperature, e);
setRefresh(!refresh);
}}
/>
@@ -174,9 +174,9 @@ const AIChatSettingsModal = ({
min={100}
max={tokenLimit}
step={50}
value={getValues(ModuleInputKeyEnum.aiChatMaxToken)}
value={getValues(NodeInputKeyEnum.aiChatMaxToken)}
onChange={(val) => {
setValue(ModuleInputKeyEnum.aiChatMaxToken, val);
setValue(NodeInputKeyEnum.aiChatMaxToken, val);
setRefresh(!refresh);
}}
/>
@@ -215,11 +215,11 @@ const AIChatSettingsModal = ({
</Box>
<Box flex={1} ml={'10px'}>
<Switch
isChecked={getValues(ModuleInputKeyEnum.aiChatIsResponseText)}
isChecked={getValues(NodeInputKeyEnum.aiChatIsResponseText)}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
setValue(ModuleInputKeyEnum.aiChatIsResponseText, value);
setValue(NodeInputKeyEnum.aiChatIsResponseText, value);
setRefresh((state) => !state);
}}
/>

View File

@@ -1,11 +1,13 @@
import React, { useEffect } from 'react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { LLMModelTypeEnum, llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SettingAIDataType } from '@fastgpt/global/core/module/node/type';
import { Box, Button, Flex, css, useDisclosure } from '@chakra-ui/react';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import AISettingModal from '@/components/core/ai/AISettingModal';
import Avatar from '@/components/Avatar';
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
type Props = {
llmModelType?: `${LLMModelTypeEnum}`;
@@ -14,6 +16,7 @@ type Props = {
};
const SettingLLMModel = ({ llmModelType = LLMModelTypeEnum.all, defaultData, onChange }: Props) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const model = defaultData.model;
@@ -41,30 +44,39 @@ const SettingLLMModel = ({ llmModelType = LLMModelTypeEnum.all, defaultData, onC
model: modelList[0].model
});
}
}, [defaultData, model, modelList, onChange]);
}, []);
return (
<Box position={'relative'}>
<Button
w={'100%'}
justifyContent={'flex-start'}
variant={'whitePrimary'}
_active={{
transform: 'none'
}}
leftIcon={
<Avatar
borderRadius={'0'}
src={selectedModel?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'18px'}
/>
<Box
css={css({
span: {
display: 'block'
}
pl={4}
onClick={onOpenAIChatSetting}
>
{selectedModel?.name}
</Button>
})}
position={'relative'}
>
<MyTooltip label={t('core.app.Setting ai property')}>
<Button
w={'100%'}
justifyContent={'flex-start'}
variant={'whiteFlow'}
_active={{
transform: 'none'
}}
leftIcon={
<Avatar
borderRadius={'0'}
src={selectedModel?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'18px'}
/>
}
pl={4}
onClick={onOpenAIChatSetting}
>
{selectedModel?.name}
</Button>
</MyTooltip>
{isOpenAIChatSetting && (
<AISettingModal
onClose={onCloseAIChatSetting}

View File

@@ -20,7 +20,7 @@ import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import MyRadio from '@/components/common/MyRadio';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -133,7 +133,7 @@ const DatasetParamsModal = ({
{
label: t('core.module.template.Query extension'),
id: SearchSettingTabEnum.queryExtension,
icon: '/imgs/module/cfr.svg'
icon: '/imgs/workflow/cfr.svg'
}
]}
activeId={currentTabType}
@@ -223,9 +223,9 @@ const DatasetParamsModal = ({
min={100}
max={maxTokens}
step={50}
value={getValues(ModuleInputKeyEnum.datasetMaxTokens) ?? 1000}
value={getValues(NodeInputKeyEnum.datasetMaxTokens) ?? 1000}
onChange={(val) => {
setValue(ModuleInputKeyEnum.datasetMaxTokens, val);
setValue(NodeInputKeyEnum.datasetMaxTokens, val);
setRefresh(!refresh);
}}
/>
@@ -249,9 +249,9 @@ const DatasetParamsModal = ({
min={0}
max={1}
step={0.01}
value={getValues(ModuleInputKeyEnum.datasetSimilarity) ?? 0.5}
value={getValues(NodeInputKeyEnum.datasetSimilarity) ?? 0.5}
onChange={(val) => {
setValue(ModuleInputKeyEnum.datasetSimilarity, val);
setValue(NodeInputKeyEnum.datasetSimilarity, val);
setRefresh(!refresh);
}}
/>

View File

@@ -11,7 +11,7 @@ import {
Divider
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import type { SelectedDatasetType } from '@fastgpt/global/core/module/api.d';
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/api.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTooltip from '@/components/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -20,7 +20,7 @@ import { useTranslation } from 'next-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import DatasetSelectContainer, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import EmptyTip from '@/components/EmptyTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
export const DatasetSelectModal = ({
isOpen,

View File

@@ -11,7 +11,7 @@ const QGSwitch = (props: SwitchProps) => {
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
<Box>{t('core.app.Question Guide')}</Box>
<Box fontWeight={'medium'}>{t('core.app.Question Guide')}</Box>
<MyTooltip label={t('core.app.Question Guide Tip')} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>

View File

@@ -0,0 +1,340 @@
import { Box, Button, Flex, ModalBody, useDisclosure, Switch, Textarea } from '@chakra-ui/react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { AppScheduledTriggerConfigType } from '@fastgpt/global/core/app/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import dynamic from 'next/dynamic';
import type { MultipleSelectProps } from '@fastgpt/web/components/common/MySelect/type.d';
import { useForm } from 'react-hook-form';
import { cronParser2Fields } from '@fastgpt/global/common/string/time';
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
);
// options type:
enum CronJobTypeEnum {
month = 'month',
week = 'week',
day = 'day',
interval = 'interval'
}
type CronType = 'month' | 'week' | 'day' | 'interval';
const get24HoursOptions = () => {
return Array.from({ length: 24 }, (_, i) => ({
label: `${i < 10 ? '0' : ''}${i}:00`,
value: i
}));
};
const getWeekOptions = () => {
return Array.from({ length: 7 }, (_, i) => {
if (i === 0) {
return {
label: '星期日',
value: i,
children: get24HoursOptions()
};
}
return {
label: `星期${i}`,
value: i,
children: get24HoursOptions()
};
});
};
const getMonthOptions = () => {
return Array.from({ length: 28 }, (_, i) => ({
label: `${i + 1}`,
value: i,
children: get24HoursOptions()
}));
};
const getInterValOptions = () => {
// 每n小时
return [
{
label: `每小时`,
value: 1
},
{
label: `每2小时`,
value: 2
},
{
label: `每3小时`,
value: 3
},
{
label: `每4小时`,
value: 4
},
{
label: `每6小时`,
value: 6
},
{
label: `每12小时`,
value: 12
}
];
};
const defaultValue = ['day', 0, 0];
const defaultCronString = '0 0 * * *';
type CronFieldType = [CronType, number, number];
const ScheduledTriggerConfig = ({
value,
onChange
}: {
value: AppScheduledTriggerConfigType | null;
onChange: (e: AppScheduledTriggerConfigType | null) => void;
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const { register, setValue, watch } = useForm<AppScheduledTriggerConfigType>({
defaultValues: {
cronString: value?.cronString || '',
timezone: value?.timezone,
defaultPrompt: value?.defaultPrompt || ''
}
});
const timezone = watch('timezone');
const cronString = watch('cronString');
const cronSelectList = useRef<MultipleSelectProps['list']>([
{
label: '每天执行',
value: CronJobTypeEnum.day,
children: get24HoursOptions()
},
{
label: '每周执行',
value: CronJobTypeEnum.week,
children: getWeekOptions()
},
{
label: '每月执行',
value: CronJobTypeEnum.month,
children: getMonthOptions()
},
{
label: '间隔执行',
value: CronJobTypeEnum.interval,
children: getInterValOptions()
}
]);
/* cron string to config field */
const cronConfig = useMemo(() => {
if (!cronString) {
return null;
}
const cronField = cronParser2Fields(cronString);
if (!cronField) {
return null;
}
if (cronField.dayOfMonth.length !== 31) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.month, cronField.dayOfMonth[0], cronField.hour[0]]
};
}
if (cronField.dayOfWeek.length !== 8) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.week, cronField.dayOfWeek[0], cronField.hour[0]]
};
}
if (cronField.hour.length === 1) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.day, cronField.hour[0], 0]
};
}
return {
isOpen: true,
cronField: [CronJobTypeEnum.interval, 24 / cronField.hour.length, 0]
};
}, [cronString]);
const isOpenSchedule = cronConfig?.isOpen;
const cronField = (cronConfig?.cronField || defaultValue) as CronFieldType;
const cronConfig2cronString = useCallback(
(e: CronFieldType) => {
if (e[0] === CronJobTypeEnum.month) {
setValue('cronString', `0 ${e[2]} ${e[1]} * *`);
} else if (e[0] === CronJobTypeEnum.week) {
setValue('cronString', `0 ${e[2]} * * ${e[1]}`);
} else if (e[0] === CronJobTypeEnum.day) {
setValue('cronString', `0 ${e[1]} * * *`);
} else if (e[0] === CronJobTypeEnum.interval) {
setValue('cronString', `0 */${e[1]} * * *`);
} else {
setValue('cronString', '');
}
},
[setValue]
);
// cron config to show label
const formatLabel = useMemo(() => {
if (!isOpenSchedule) {
return t('common.Not open');
}
if (cronField[0] === 'month') {
return t('core.app.schedule.Every month', {
day: cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'week') {
return t('core.app.schedule.Every week', {
day: cronField[1] === 0 ? '日' : cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'day') {
return t('core.app.schedule.Every day', {
hour: cronField[1]
});
}
if (cronField[0] === 'interval') {
return t('core.app.schedule.Interval', {
interval: cronField[1]
});
}
return t('common.Not open');
}, [cronField, isOpenSchedule, t]);
// update value
watch((data) => {
if (!data.cronString) {
onChange(null);
return;
}
onChange({
cronString: data.cronString,
timezone: data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
defaultPrompt: data.defaultPrompt || ''
});
});
useEffect(() => {
if (!value?.timezone) {
setValue('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}, []);
const Render = useMemo(() => {
return (
<>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/schedulePlan'} w={'20px'} />
<Flex alignItems={'center'} ml={2} flex={1}>
{t('core.app.Interval timer run')}
<QuestionTip ml={1} label={t('core.app.Interval timer tip')} />
</Flex>
<MyTooltip label={t('core.app.Config schedule plan')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formatLabel}
</Button>
</MyTooltip>
</Flex>
<MyModal
isOpen={isOpen}
onClose={onClose}
iconSrc={'core/app/schedulePlan'}
title={t('core.app.Interval timer config')}
overflow={'unset'}
>
<ModalBody>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box flex={'0 0 80px'}> {t('core.app.schedule.Open schedule')}</Box>
<Switch
size={'lg'}
isChecked={isOpenSchedule}
onChange={(e) => {
if (e.target.checked) {
setValue('cronString', defaultCronString);
} else {
setValue('cronString', '');
}
}}
/>
</Flex>
{isOpenSchedule && (
<>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'}></Box>
<Box flex={'1 0 0'}>
<MultipleRowSelect
label={formatLabel}
value={cronField}
list={cronSelectList.current}
onSelect={(e) => {
cronConfig2cronString(e as CronFieldType);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'}></Box>
<Box flex={'1 0 0'}>
<TimezoneSelect
value={timezone}
onChange={(e) => {
setValue('timezone', e);
}}
/>
</Box>
</Flex>
<Box mt={5}>
<Box>{t('core.app.schedule.Default prompt')}</Box>
<Textarea
{...register('defaultPrompt')}
rows={8}
bg={'myGray.50'}
placeholder={t('core.app.schedule.Default prompt placeholder')}
/>
</Box>
</>
)}
</ModalBody>
</MyModal>
</>
);
}, [
cronConfig2cronString,
cronField,
formatLabel,
isOpen,
isOpenSchedule,
onClose,
onOpen,
register,
setValue,
t,
timezone
]);
return Render;
};
export default React.memo(ScheduledTriggerConfig);

View File

@@ -80,7 +80,7 @@ const TTSSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/tts'} mr={2} w={'20px'} />
<Box>{t('core.app.TTS')}</Box>
<Box fontWeight={'medium'}>{t('core.app.TTS')}</Box>
<MyTooltip label={t('core.app.TTS Tip')} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
@@ -90,7 +90,6 @@ const TTSSelect = ({
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
fontSize={'md'}
mr={'-5px'}
onClick={onOpen}
>

View File

@@ -25,7 +25,7 @@ import {
useDisclosure
} from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import { VariableInputEnum, variableMap } from '@fastgpt/global/core/module/constants';
import { VariableInputEnum, variableMap } from '@fastgpt/global/core/workflow/constants';
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useForm } from 'react-hook-form';
@@ -34,11 +34,11 @@ import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTooltip from '@/components/MyTooltip';
import { variableTip } from '@fastgpt/global/core/module/template/tip';
import { variableTip } from '@fastgpt/global/core/workflow/template/tip';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyRadio from '@/components/common/MyRadio';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/module/utils';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
const VariableEdit = ({
variables,
@@ -98,7 +98,7 @@ const VariableEdit = ({
<Box>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/variable'} w={'20px'} />
<Box ml={2} flex={1}>
<Box ml={2} flex={1} fontWeight={'medium'}>
{t('core.module.Variable')}
<MyTooltip label={t(variableTip)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
@@ -110,7 +110,6 @@ const VariableEdit = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
fontSize={'md'}
onClick={() => {
resetEdit({ variable: addVariable() });
onOpenEdit();

View File

@@ -32,14 +32,13 @@ const WhisperConfig = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/whisper'} mr={2} w={'20px'} />
<Box>{t('core.app.Whisper')}</Box>
<Box fontWeight={'medium'}>{t('core.app.Whisper')}</Box>
<Box flex={1} />
<MyTooltip label={t('core.app.Config whisper')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
fontSize={'md'}
mr={'-5px'}
onClick={onOpen}
>

View File

@@ -183,7 +183,9 @@ const QuoteItem = ({
w={'100%'}
size="sm"
borderRadius={'20px'}
colorScheme={scoreTheme[i]?.colorSchema}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorSchema
})}
bg="#E8EBF0"
/>
)}
@@ -199,7 +201,14 @@ const QuoteItem = ({
</Box>
{canViewSource && (
<Flex alignItems={'center'} mt={3} gap={4} color={'myGray.500'} fontSize={'xs'}>
<Flex
alignItems={'center'}
flexWrap={'wrap'}
mt={3}
gap={4}
color={'myGray.500'}
fontSize={'xs'}
>
<MyTooltip label={t('core.dataset.Quote Length')}>
<Flex alignItems={'center'}>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />

View File

@@ -33,7 +33,7 @@ const DatasetSelectContainer = ({
return (
<MyModal
iconSrc="/imgs/module/db.png"
iconSrc="/imgs/workflow/db.png"
title={
<Box fontWeight={'normal'}>
<ParentPaths

View File

@@ -1,572 +0,0 @@
import {
type Node,
type NodeChange,
type Edge,
type EdgeChange,
useNodesState,
useEdgesState,
Connection,
addEdge
} from 'reactflow';
import type { FlowModuleItemType, FlowNodeTemplateType } from '@fastgpt/global/core/module/type.d';
import type {
FlowNodeChangeProps,
FlowNodeInputItemType
} from '@fastgpt/global/core/module/node/type';
import React, {
type SetStateAction,
type Dispatch,
useContext,
useCallback,
createContext,
useRef,
useEffect,
useMemo
} from 'react';
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import {
ModuleIOValueTypeEnum,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
type requestEventType =
| 'onChangeNode'
| 'onCopyNode'
| 'onResetNode'
| 'onDelNode'
| 'onDelConnect'
| 'setNodes';
export type useFlowProviderStoreType = {
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
mode: 'app' | 'plugin';
filterAppIds: string[];
nodes: Node<FlowModuleItemType, string | undefined>[];
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onDelNode: (nodeId: string) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onCopyNode: (nodeId: string) => void;
onResetNode: (e: { id: string; module: FlowNodeTemplateType }) => void;
onDelEdge: (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
onDelConnect: (id: string) => void;
onConnect: ({ connect }: { connect: Connection }) => any;
initData: (modules: ModuleItemType[]) => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
moduleId: string
) => {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
};
hasToolNode: boolean;
};
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
mode: 'app',
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
): void {
return;
},
onNodesChange: function (changes: NodeChange[]): void {
return;
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
return;
},
onEdgesChange: function (changes: EdgeChange[]): void {
return;
},
onFixView: function (): void {
return;
},
onDelNode: function (nodeId: string): void {
return;
},
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onCopyNode: function (nodeId: string): void {
return;
},
onDelEdge: function (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
return;
},
onDelConnect: function (id: string): void {
return;
},
onConnect: function ({ connect }: { connect: Connection }) {
return;
},
initData: function (modules: ModuleItemType[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (e): void {
throw new Error('Function not implemented.');
},
splitToolInputs: function (
inputs: FlowNodeInputItemType[],
moduleId: string
): {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
} {
throw new Error('Function not implemented.');
},
hasToolNode: false
});
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({
mode,
filterAppIds = [],
children
}: {
mode: useFlowProviderStoreType['mode'];
filterAppIds?: string[];
children: React.ReactNode;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowType === FlowNodeTypeEnum.tools);
}, [nodes]);
const onFixView = useCallback(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
}, []);
const onDelEdge = useCallback(
({
moduleId,
sourceHandle,
targetHandle
}: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => {
if (!sourceHandle && !targetHandle) return;
setEdges((state) =>
state.filter((edge) => {
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
return true;
})
);
},
[setEdges]
);
const onDelConnect = useCallback(
(id: string) => {
setEdges((state) => state.filter((item) => item.id !== id));
},
[setEdges]
);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
const type = source?.outputs.find(
(output) => output.key === connect.sourceHandle
)?.valueType;
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion && !type) {
return ModuleIOValueTypeEnum.boolean;
}
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
}
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
})();
const targetType = nodes
.find((node) => node.id === connect.target)
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
if (
connect.sourceHandle === ModuleOutputKeyEnum.selectedTools &&
connect.targetHandle === ModuleOutputKeyEnum.selectedTools
) {
} else if (!sourceType || !targetType) {
return toast({
status: 'warning',
title: t('app.Connection is invalid')
});
} else if (
sourceType !== ModuleIOValueTypeEnum.any &&
targetType !== ModuleIOValueTypeEnum.any &&
sourceType !== targetType
) {
return toast({
status: 'warning',
title: t('app.Connection type is different')
});
}
setEdges((state) =>
addEdge(
{
...connect,
type: EDGE_TYPE
},
state
)
);
},
[nodes, setEdges, t, toast]
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.id !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
},
[setEdges, setNodes]
);
/* change */
const onChangeNode = useCallback(
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
const updateObj: Record<string, any> = {};
if (type === 'attr') {
if (key) {
updateObj[key] = value;
}
} else if (type === 'updateInput') {
updateObj.inputs = node.data.inputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceInput') {
onDelEdge({ moduleId, targetHandle: key });
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === key);
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addInput',
index: oldInputIndex,
value
});
});
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.inputs = node.data.inputs;
} else {
if (index !== undefined) {
const inputs = [...node.data.inputs];
inputs.splice(index, 0, value);
updateObj.inputs = inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
}
}
} else if (type === 'delInput') {
onDelEdge({ moduleId, targetHandle: key });
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
} else if (type === 'updateOutput') {
updateObj.outputs = node.data.outputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceOutput') {
onDelEdge({ moduleId, sourceHandle: key });
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === key);
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addOutput',
index: oldOutputIndex,
value
});
});
} else if (type === 'addOutput') {
const output = node.data.outputs.find((output) => output.key === value.key);
if (output) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.outputs = node.data.outputs;
} else {
if (index !== undefined) {
const outputs = [...node.data.outputs];
outputs.splice(index, 0, value);
updateObj.outputs = outputs;
} else {
updateObj.outputs = node.data.outputs.concat(value);
}
}
} else if (type === 'delOutput') {
onDelEdge({ moduleId, sourceHandle: key });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
},
[onDelEdge, setNodes, toast]
);
const onCopyNode = useCallback(
(nodeId: string) => {
setNodes((nodes) => {
const node = nodes.find((node) => node.id === nodeId);
if (!node) return nodes;
const template = {
avatar: node.data.avatar,
name: node.data.name,
intro: node.data.intro,
flowType: node.data.flowType,
inputs: node.data.inputs,
outputs: node.data.outputs,
showStatus: node.data.showStatus
};
return nodes.concat(
appModule2FlowNode({
item: {
...template,
moduleId: nanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 }
}
})
);
});
},
[setNodes]
);
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = useCallback(
(inputs: FlowNodeInputItemType[], moduleId: string) => {
const isTool = !!edges.find(
(edge) =>
edge.targetHandle === ModuleOutputKeyEnum.selectedTools && edge.target === moduleId
);
return {
isTool,
toolInputs: inputs.filter((item) => isTool && item.toolDescription),
commonInputs: inputs.filter((item) => {
if (!isTool) return true;
return !item.toolDescription;
})
};
},
[edges]
);
// reset a node data. delete edge and replace it
const onResetNode = useCallback(
({ id, module }: { id: string; module: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ moduleId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ moduleId: id, sourceHandle: item.key });
});
return {
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
},
[onDelEdge, setNodes]
);
const initData = useCallback(
(modules: ModuleItemType[]) => {
const edges = appModule2FlowEdge({
modules
});
setEdges(edges);
setNodes(modules.map((item) => appModule2FlowNode({ item })));
onFixView();
},
[setEdges, setNodes, onFixView]
);
// use eventbus to avoid refresh ReactComponents
useEffect(() => {
eventBus.on(
EventNameEnum.requestFlowEvent,
({ type, data }: { type: requestEventType; data: any }) => {
switch (type) {
case 'onChangeNode':
onChangeNode(data);
return;
case 'onCopyNode':
onCopyNode(data);
return;
case 'onResetNode':
onResetNode(data);
return;
case 'onDelNode':
onDelNode(data);
return;
case 'onDelConnect':
onDelConnect(data);
return;
case 'setNodes':
setNodes(data);
return;
}
}
);
return () => {
eventBus.off(EventNameEnum.requestFlowEvent);
};
}, []);
useEffect(() => {
eventBus.on(EventNameEnum.requestFlowStore, () => {
eventBus.emit('receiveFlowStore', {
nodes,
edges,
mode,
filterAppIds,
reactFlowWrapper
});
});
return () => {
eventBus.off(EventNameEnum.requestFlowStore);
};
}, [edges, filterAppIds, mode, nodes]);
const value = {
reactFlowWrapper,
mode,
filterAppIds,
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
onFixView,
onDelNode,
onChangeNode,
onResetNode,
onCopyNode,
onDelEdge,
onDelConnect,
onConnect,
initData,
splitToolInputs,
hasToolNode
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowNodeChangeProps) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onChangeNode',
data: e
});
};
export const onCopyNode = (nodeId: string) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onCopyNode',
data: nodeId
});
};
export const onResetNode = (e: Parameters<useFlowProviderStoreType['onResetNode']>[0]) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onResetNode',
data: e
});
};
export const onDelConnect = (e: Parameters<useFlowProviderStoreType['onDelConnect']>[0]) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onDelConnect',
data: e
});
};
export const onSetNodes = (e: useFlowProviderStoreType['nodes']) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'setNodes',
data: e
});
};
export const getFlowStore = () =>
new Promise<{
nodes: useFlowProviderStoreType['nodes'];
edges: useFlowProviderStoreType['edges'];
mode: useFlowProviderStoreType['mode'];
filterAppIds: useFlowProviderStoreType['filterAppIds'];
reactFlowWrapper: useFlowProviderStoreType['reactFlowWrapper'];
}>((resolve) => {
eventBus.on('receiveFlowStore', (data: any) => {
resolve(data);
eventBus.off('receiveFlowStore');
});
eventBus.emit(EventNameEnum.requestFlowStore);
});

View File

@@ -1,30 +0,0 @@
import React from 'react';
import { useStoreApi, type ConnectionLineComponentProps } from 'reactflow';
const CustomConnection = ({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) => {
const store = useStoreApi();
const { connectionHandleId } = store.getState();
console.log(fromX, fromY, toX, toY, connectionHandleId);
return (
<g>
<path
fill="none"
stroke={connectionHandleId || ''}
strokeWidth={1.5}
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle
cx={toX}
cy={toY}
fill="#fff"
r={3}
stroke={connectionHandleId || ''}
strokeWidth={1.5}
/>
</g>
);
};
export default CustomConnection;

View File

@@ -1,116 +0,0 @@
import React, { useMemo } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { onDelConnect, useFlowProviderStore } from '../../FlowProvider';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
const ButtonEdge = (props: EdgeProps) => {
const { nodes } = useFlowProviderStore();
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
selected,
sourceHandleId,
animated,
style = {}
} = props;
const active = (() => {
const connectNode = nodes.find((node) => {
return (node.id === props.source || node.id === props.target) && node.selected;
});
return !!(connectNode || selected);
})();
const [, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
const isToolEdge = sourceHandleId === ModuleOutputKeyEnum.selectedTools;
const memoEdgeLabel = useMemo(() => {
return (
<EdgeLabelRenderer>
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'20px'}
h={'20px'}
bg={'white'}
borderRadius={'20px'}
color={'black'}
cursor={'pointer'}
borderWidth={'1px'}
borderColor={'borderColor.low'}
zIndex={active ? 1000 : 0}
_hover={{
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
}}
onClick={() => onDelConnect(id)}
>
<MyIcon
name="closeSolid"
w={'100%'}
color={active ? 'primary.700' : 'myGray.400'}
></MyIcon>
</Flex>
{!isToolEdge && (
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-78%, -50%) translate(${targetX}px,${targetY}px)`}
pointerEvents={'all'}
w={'16px'}
h={'16px'}
bg={'white'}
zIndex={active ? 1000 : 0}
>
<MyIcon
name={'common/rightArrowLight'}
w={'100%'}
color={active ? 'primary.700' : 'myGray.400'}
></MyIcon>
</Flex>
)}
</EdgeLabelRenderer>
);
}, [labelX, labelY, active, isToolEdge, targetX, targetY, id]);
const memoBezierEdge = useMemo(() => {
const edgeStyle: React.CSSProperties = {
...style,
...(active
? {
strokeWidth: 5,
stroke: '#3370ff'
}
: { strokeWidth: 2, zIndex: 2, stroke: 'myGray.300' })
};
return <BezierEdge {...props} style={edgeStyle} />;
}, [style, active, props]);
return (
<>
{memoBezierEdge}
{memoEdgeLabel}
</>
);
};
export default React.memo(ButtonEdge);

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import { useFlowProviderStore } from '../../FlowProvider';
import Divider from '../modules/Divider';
import RenderToolInput from '../render/RenderToolInput';
import { useTranslation } from 'next-i18next';
const NodeAnswer = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { splitToolInputs } = useFlowProviderStore();
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
{toolInputs.length > 0 && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} />
</Container>
</>
)}
<RenderInput moduleId={moduleId} flowInputList={commonInputs} />
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeAnswer);

View File

@@ -1,149 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { AddIcon } from '@chakra-ui/icons';
import {
ModuleIOValueTypeEnum,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/module/template/system/datasetConcat';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import TargetHandle from '../render/TargetHandle';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SourceHandle from '../render/SourceHandle';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodes } = useFlowProviderStore();
const { moduleId, inputs, outputs } = data;
const quotes = useMemo(
() => inputs.filter((item) => item.valueType === ModuleIOValueTypeEnum.datasetQuote),
[inputs]
);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodes.forEach((item) => {
if (item.type === FlowNodeTypeEnum.chatNode) {
const model =
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [llmModelList, nodes]);
const RenderQuoteList = useMemo(() => {
return (
<Box>
<Box>
{quotes.map((quote, i) => (
<Flex key={quote.key} position={'relative'} mb={4} alignItems={'center'}>
<TargetHandle handleKey={quote.key} valueType={quote.valueType} />
<Box>
{t('core.chat.Quote')}
{i + 1}
</Box>
<MyIcon
ml={2}
w={'14px'}
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: quote.key
});
}}
/>
</Flex>
))}
</Box>
<Button
leftIcon={<AddIcon />}
variant={'whiteBase'}
onClick={() => {
onChangeNode({
moduleId,
type: 'addInput',
value: getOneQuoteInputTemplate()
});
}}
>
{t('core.module.Dataset quote.Add quote')}
</Button>
</Box>
);
}, [moduleId, quotes, t]);
const CustomComponent = useMemo(() => {
console.log(111);
return {
[ModuleInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
<Box px={2}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: tokenLimit, value: tokenLimit }
]}
width={'100%'}
min={100}
max={tokenLimit}
step={50}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
)
};
}, [moduleId, tokenLimit]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} position={'relative'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* render dataset select */}
{RenderQuoteList}
<Flex position={'absolute'} right={4} top={'60%'}>
<Box>{t('core.module.Dataset quote.Concat result')}</Box>
<SourceHandle
handleKey={ModuleOutputKeyEnum.datasetQuoteQA}
valueType={ModuleIOValueTypeEnum.datasetQuote}
// transform={'translate(-14px, -50%)'}
/>
</Flex>
{/* <RenderOutput moduleId={moduleId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
};
export default React.memo(NodeDatasetConcat);

View File

@@ -1,10 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
const NodeEmpty = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
return <NodeCard selected={selected} {...data}></NodeCard>;
};
export default React.memo(NodeEmpty);

View File

@@ -1,649 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../../modules/Divider';
import Container from '../../modules/Container';
import RenderInput from '../../render/RenderInput';
import RenderOutput from '../../render/RenderOutput';
import {
Box,
Flex,
Input,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Button,
useDisclosure
} from '@chakra-ui/react';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode, useFlowProviderStore } from '../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import Tabs from '@/components/Tabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/utils';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import dynamic from 'next/dynamic';
import MySelect from '@fastgpt/web/components/common/MySelect';
import RenderToolInput from '../../render/RenderToolInput';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
export const HttpHeaders = [
{ key: 'A-IM', label: 'A-IM' },
{ key: 'Accept', label: 'Accept' },
{ key: 'Accept-Charset', label: 'Accept-Charset' },
{ key: 'Accept-Encoding', label: 'Accept-Encoding' },
{ key: 'Accept-Language', label: 'Accept-Language' },
{ key: 'Accept-Datetime', label: 'Accept-Datetime' },
{ key: 'Access-Control-Request-Method', label: 'Access-Control-Request-Method' },
{ key: 'Access-Control-Request-Headers', label: 'Access-Control-Request-Headers' },
{ key: 'Authorization', label: 'Authorization' },
{ key: 'Cache-Control', label: 'Cache-Control' },
{ key: 'Connection', label: 'Connection' },
{ key: 'Content-Length', label: 'Content-Length' },
{ key: 'Content-Type', label: 'Content-Type' },
{ key: 'Cookie', label: 'Cookie' },
{ key: 'Date', label: 'Date' },
{ key: 'Expect', label: 'Expect' },
{ key: 'Forwarded', label: 'Forwarded' },
{ key: 'From', label: 'From' },
{ key: 'Host', label: 'Host' },
{ key: 'If-Match', label: 'If-Match' },
{ key: 'If-Modified-Since', label: 'If-Modified-Since' },
{ key: 'If-None-Match', label: 'If-None-Match' },
{ key: 'If-Range', label: 'If-Range' },
{ key: 'If-Unmodified-Since', label: 'If-Unmodified-Since' },
{ key: 'Max-Forwards', label: 'Max-Forwards' },
{ key: 'Origin', label: 'Origin' },
{ key: 'Pragma', label: 'Pragma' },
{ key: 'Proxy-Authorization', label: 'Proxy-Authorization' },
{ key: 'Range', label: 'Range' },
{ key: 'Referer', label: 'Referer' },
{ key: 'TE', label: 'TE' },
{ key: 'User-Agent', label: 'User-Agent' },
{ key: 'Upgrade', label: 'Upgrade' },
{ key: 'Via', label: 'Via' },
{ key: 'Warning', label: 'Warning' },
{ key: 'Dnt', label: 'Dnt' },
{ key: 'X-Requested-With', label: 'X-Requested-With' },
{ key: 'X-CSRF-Token', label: 'X-CSRF-Token' }
];
enum TabEnum {
params = 'params',
headers = 'headers',
body = 'body'
}
export type PropsArrType = {
key: string;
type: string;
value: string;
};
const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
moduleId,
inputs
}: {
moduleId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const { toast } = useToast();
const [_, startSts] = useTransition();
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
const requestMethods = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod);
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
const onChangeUrl = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e.target.value
}
});
},
[moduleId, requestUrl]
);
const onBlurUrl = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// 拆分params和url
const url = val.split('?')[0];
const params = val.split('?')[1];
if (params) {
const paramsArr = params.split('&');
const paramsObj = paramsArr.reduce((acc, cur) => {
const [key, value] = cur.split('=');
return {
...acc,
[key]: value
};
}, {});
const inputParams = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
if (!inputParams || Object.keys(paramsObj).length === 0) return;
const concatParams: PropsArrType[] = inputParams?.value || [];
Object.entries(paramsObj).forEach(([key, value]) => {
if (!concatParams.find((item) => item.key === key)) {
concatParams.push({ key, value: value as string, type: 'string' });
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpParams,
value: {
...inputParams,
value: concatParams
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: url
}
});
toast({
status: 'success',
title: t('core.module.http.Url and params have been split')
});
}
},
[inputs, moduleId, requestUrl, t, toast]
);
return (
<Box>
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
<Box>{t('core.module.Http request settings')}</Box>
<Button variant={'link'} onClick={onOpenCurl}>
{t('core.module.http.curl import')}
</Button>
</Box>
<Flex alignItems={'center'} className="nodrag">
<MySelect
h={'34px'}
w={'88px'}
bg={'myGray.50'}
width={'100%'}
value={requestMethods?.value}
list={[
{
label: 'GET',
value: 'GET'
},
{
label: 'POST',
value: 'POST'
},
{
label: 'PUT',
value: 'PUT'
},
{
label: 'DELETE',
value: 'DELETE'
},
{
label: 'PATCH',
value: 'PATCH'
}
]}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpMethod,
value: {
...requestMethods,
value: e
}
});
}}
/>
<Input
flex={'1 0 0'}
ml={2}
h={'34px'}
value={requestUrl?.value}
placeholder={t('core.module.input.label.Http Request Url')}
fontSize={'xs'}
onChange={onChangeUrl}
onBlur={onBlurUrl}
/>
</Flex>
{isOpenCurl && <CurlImportModal moduleId={moduleId} inputs={inputs} onClose={onCloseCurl} />}
</Box>
);
});
export function RenderHttpProps({
moduleId,
inputs
}: {
moduleId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const { nodes } = useFlowProviderStore();
const requestMethods = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === ModuleInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === ModuleInputKeyEnum.httpJsonBody);
const paramsLength = params?.value?.length || 0;
const headersLength = headers?.value?.length || 0;
// get variable
const variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const systemVariables = [
{
key: 'appId',
label: t('core.module.http.AppId')
},
{
key: 'chatId',
label: t('core.module.http.ChatId')
},
{
key: 'responseChatItemId',
label: t('core.module.http.ResponseChatItemId')
},
{
key: 'variables',
label: t('core.module.http.Variables')
},
{
key: 'histories',
label: t('core.module.http.Histories')
},
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit || input.toolDescription)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...moduleVariables, ...globalVariables, ...systemVariables];
}, [inputs, nodes, t]);
const variableText = useMemo(() => {
return variables
.map((item) => `${item.key}${item.key !== item.label ? `(${item.label})` : ''}`)
.join('\n');
}, [variables]);
return (
<Box>
<Flex alignItems={'center'} mb={2}>
{t('core.module.Http request props')}
<MyTooltip label={t('core.module.http.Props tip', { variable: variableText })}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Tabs
list={[
{ label: <RenderPropsItem text="Params" num={paramsLength} />, id: TabEnum.params },
...(!['GET', 'DELETE'].includes(requestMethods)
? [
{
label: (
<Flex alignItems={'center'}>
Body
{jsonBody?.value && <Box ml={1}></Box>}
</Flex>
),
id: TabEnum.body
}
]
: []),
{ label: <RenderPropsItem text="Headers" num={headersLength} />, id: TabEnum.headers }
]}
activeId={selectedTab}
onChange={(e) => setSelectedTab(e as any)}
/>
{params &&
headers &&
jsonBody &&
{
[TabEnum.params]: (
<RenderForm
moduleId={moduleId}
input={params}
variables={variables}
tabType={TabEnum.params}
/>
),
[TabEnum.body]: <RenderJson moduleId={moduleId} variables={variables} input={jsonBody} />,
[TabEnum.headers]: (
<RenderForm
moduleId={moduleId}
input={headers}
variables={variables}
tabType={TabEnum.headers}
/>
)
}[selectedTab]}
</Box>
);
}
const RenderForm = ({
moduleId,
input,
variables,
tabType
}: {
moduleId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
tabType?: TabEnum;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [list, setList] = useState<PropsArrType[]>(input.value || []);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [shouldUpdateNode, setShouldUpdateNode] = useState(false);
const leftVariables = useMemo(() => {
return (tabType === TabEnum.headers ? HttpHeaders : variables).filter((variable) => {
const existVariables = list.map((item) => item.key);
return !existVariables.includes(variable.key);
});
}, [list, tabType, variables]);
useEffect(() => {
setList(input.value || []);
}, [input.value]);
useEffect(() => {
if (shouldUpdateNode) {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list
}
});
setShouldUpdateNode(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list]);
const handleKeyChange = (index: number, newKey: string) => {
setList((prevList) => {
if (!newKey) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key cannot be empty')
});
return prevList;
}
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
});
setShouldUpdateNode(true);
};
const handleAddNewProps = (key: string, value: string = '') => {
setList((prevList) => {
if (!key) {
return prevList;
}
const checkExist = prevList.find((item) => item.key === key);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return [...prevList, { key, type: 'string', value }];
});
setShouldUpdateNode(true);
};
return (
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2}>{t('core.module.http.Props name')}</Th>
<Th px={2}>{t('core.module.http.Props value')}</Th>
</Tr>
</Thead>
<Tbody>
{list.map((item, index) => (
<Tr key={`${input.key}${index}`}>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(value) => {
handleKeyChange(index, value);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Props name')}
value={item.key}
variables={leftVariables}
onBlur={(val) => {
handleKeyChange(index, val);
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('core.module.http.Props value')}
value={item.value}
variables={variables}
onBlur={(val) => {
setList((prevList) =>
prevList.map((item, i) => (i === index ? { ...item, value: val } : item))
);
setShouldUpdateNode(true);
}}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={() => {
setList((prevlist) => prevlist.filter((val) => val.key !== item.key));
setShouldUpdateNode(true);
}}
/>
</Box>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Add props')}
value={''}
variables={leftVariables}
updateTrigger={updateTrigger}
onBlur={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput />
</Box>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
);
};
const RenderJson = ({
moduleId,
input,
variables
}: {
moduleId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const { t } = useTranslation();
const [_, startSts] = useTransition();
return (
<Box mt={1}>
<JSONEditor
bg={'myGray.50'}
defaultHeight={200}
resize
value={input.value}
placeholder={t('core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: e
}
});
});
}}
variables={variables}
/>
</Box>
);
};
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
return (
<Flex alignItems={'center'}>
<Box>{text}</Box>
{num > 0 && (
<Box ml={1} borderRadius={'50%'} bg={'myGray.200'} px={2} py={'1px'}>
{num}
</Box>
)}
</Flex>
);
};
const NodeHttp = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { splitToolInputs, hasToolNode } = useFlowProviderStore();
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
const CustomComponents = useMemo(
() => ({
[ModuleInputKeyEnum.httpMethod]: () => (
<RenderHttpMethodAndUrl moduleId={moduleId} inputs={inputs} />
),
[ModuleInputKeyEnum.httpHeaders]: () => (
<>
<RenderHttpProps moduleId={moduleId} inputs={inputs} />
<Box mt={2} transform={'translateY(10px)'}>
{t('core.module.Variable import')}
</Box>
</>
)
}),
[inputs, moduleId, t]
);
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
{hasToolNode && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Divider text={t('common.Input')} />
<Container>
<RenderInput
moduleId={moduleId}
flowInputList={commonInputs}
CustomComponent={CustomComponents}
/>
</Container>
</>
<>
<Divider text={t('common.Output')} />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</>
</NodeCard>
);
};
export default React.memo(NodeHttp);

View File

@@ -1,266 +0,0 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { onChangeNode } from '../../FlowProvider';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import Container from '../modules/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from '../render/SourceHandle';
import type {
EditInputFieldMap,
EditNodeFieldType,
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/module/node/type.d';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
const FieldEditModal = dynamic(() => import('../render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
};
const createEditField: EditInputFieldMap = {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: true,
isToolInput: true
};
const NodePluginInput = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<NodeCard minW={'300px'} selected={selected} forbidMenu {...data}>
<Container mt={1} borderTop={'2px solid'} borderTopColor={'myGray.300'}>
{inputs.map((item) => (
<Flex
key={item.key}
className="nodrag"
cursor={'default'}
justifyContent={'right'}
alignItems={'center'}
position={'relative'}
mb={7}
>
{item.edit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: item.type,
valueType: item.valueType,
key: item.key,
label: item.label,
description: item.description,
required: item.required,
isToolInput: !!item.toolDescription
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: item.key
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</>
)}
{item.description && (
<MyTooltip label={t(item.description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box position={'relative'}>
{t(item.label)}
{item.required && (
<Box
position={'absolute'}
right={'-6px'}
top={'-3px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
<SourceHandle handleKey={item.key} valueType={item.valueType} />
</Flex>
))}
<Box textAlign={'right'} mt={5}>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setCreateField(defaultCreateField);
}}
>
{t('core.module.input.Add Input')}
</Button>
</Box>
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
edit: true,
editField: createEditField
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
setCreateField(undefined);
}}
/>
)}
{!!editField?.key && (
<FieldEditModal
editField={createEditField}
defaultField={editField}
keys={[editField.key]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
// check key valid
const memInput = inputs.find((item) => item.key === editField.key);
const memOutput = outputs.find((item) => item.key === editField.key);
if (!memInput || !memOutput) return setEditField(undefined);
const newInput: FlowNodeInputItemType = {
...memInput,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
...(data.inputType === FlowNodeInputTypeEnum.addInputParam
? {
editField: {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: false
},
defaultEditField: {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
}
}
: {})
};
const newOutput: FlowNodeOutputItemType = {
...memOutput,
valueType: data.valueType,
key: data.key,
label: data.label
};
console.log(data);
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginInput);

View File

@@ -1,238 +0,0 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { onChangeNode } from '../../FlowProvider';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import Container from '../modules/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import TargetHandle from '../render/TargetHandle';
import { useToast } from '@fastgpt/web/hooks/useToast';
import {
EditNodeFieldType,
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/module/node/type';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
const FieldEditModal = dynamic(() => import('../render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
};
const createEditField = {
key: true,
name: true,
description: true,
required: false,
dataType: true,
inputType: false
};
const NodePluginOutput = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<NodeCard minW={'300px'} selected={selected} forbidMenu {...data}>
<Container mt={1} borderTop={'2px solid'} borderTopColor={'myGray.300'}>
{inputs.map((item) => (
<Flex
key={item.key}
className="nodrag"
cursor={'default'}
justifyContent={'left'}
alignItems={'center'}
position={'relative'}
mb={7}
>
<TargetHandle handleKey={item.key} valueType={item.valueType} />
<Box position={'relative'}>
{t(item.label)}
<Box
position={'absolute'}
right={'-6px'}
top={'-3px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
</Box>
{item.description && (
<MyTooltip label={t(item.description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={2} />
</MyTooltip>
)}
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: item.type,
valueType: item.valueType,
key: item.key,
label: item.label,
description: item.description,
required: item.required
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: item.key,
value: ''
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</Flex>
))}
<Box textAlign={'left'} mt={5}>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setCreateField(defaultCreateField);
}}
>
{t('core.module.output.Add Output')}
</Button>
</Box>
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: createEditField
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
setCreateField(undefined);
}}
/>
)}
{!!editField?.key && (
<FieldEditModal
editField={createEditField}
defaultField={editField}
keys={[editField.key]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
// check key valid
const memInput = inputs.find((item) => item.key === editField.key);
const memOutput = outputs.find((item) => item.key === editField.key);
if (!memInput || !memOutput) return setEditField(undefined);
const newInput: FlowNodeInputItemType = {
...memInput,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description
};
const newOutput: FlowNodeOutputItemType = {
...memOutput,
valueType: data.valueType,
key: data.key,
label: data.label
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderOutput from '../render/RenderOutput';
const QuestionInputNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { moduleId, outputs } = data;
return (
<NodeCard minW={'240px'} selected={selected} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(QuestionInputNode);

View File

@@ -1,59 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import RenderToolInput from '../render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../../FlowProvider';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
const NodeSimple = ({
data,
selected,
minW = '350px',
maxW
}: NodeProps<FlowModuleItemType> & { minW?: string | number; maxW?: string | number }) => {
const { t } = useTranslation();
const { splitToolInputs } = useFlowProviderStore();
const { moduleId, inputs, outputs } = data;
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
const filterHiddenInputs = useMemo(
() => commonInputs.filter((item) => item.type !== 'hidden'),
[commonInputs]
);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{toolInputs.length > 0 && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Divider text={t('common.Input')} />
<Container>
<RenderInput moduleId={moduleId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Divider text={t('common.Output')} />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
};
export default React.memo(NodeSimple);

View File

@@ -1,33 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import { useTranslation } from 'next-i18next';
import { ToolSourceHandle } from '../render/ToolHandle';
import { Box } from '@chakra-ui/react';
const NodeTools = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Divider text={t('common.Input')} />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Box position={'relative'}>
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Divider showBorderBottom={false} text={t('core.module.template.Tool module')} />
</Box>
<ToolSourceHandle moduleId={moduleId} />
</Box>
</NodeCard>
);
};
export default React.memo(NodeTools);

View File

@@ -1,194 +0,0 @@
import React, { useCallback, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Flex, Textarea, useTheme } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { welcomeTextTip } from '@fastgpt/global/core/module/template/tip';
import { onChangeNode } from '../../FlowProvider';
import VariableEdit from '../../../../app/VariableEdit';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import Container from '../modules/Container';
import NodeCard from '../render/NodeCard';
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import { splitGuideModule } from '@fastgpt/global/core/module/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/constants/app';
const NodeUserGuide = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const theme = useTheme();
return (
<>
<NodeCard minW={'300px'} selected={selected} forbidMenu {...data}>
<Container className="nodrag" borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<WelcomeText data={data} />
<Box pt={4} pb={2}>
<ChatStartVariable data={data} />
</Box>
<Box pt={3} borderTop={theme.borders.base}>
<TTSGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} />
</Box>
</Container>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
function WelcomeText({ data }: { data: FlowModuleItemType }) {
const { t } = useTranslation();
const { inputs, moduleId } = data;
const [, startTst] = useTransition();
const welcomeText = inputs.find((item) => item.key === ModuleInputKeyEnum.welcomeText);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'core/modules/welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
<Box>{t('core.app.Welcome Text')}</Box>
<MyTooltip label={t(welcomeTextTip)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={t(welcomeTextTip)}
onChange={(e) => {
startTst(() => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.welcomeText,
type: 'updateInput',
value: {
...welcomeText,
value: e.target.value
}
});
});
}}
/>
)}
</>
);
}
function ChatStartVariable({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === ModuleInputKeyEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.variables,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.variables),
value
}
});
},
[inputs, moduleId]
);
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function QuestionGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === ModuleInputKeyEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
return (
<QGSwitch
isChecked={questionGuide}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.questionGuide,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.questionGuide),
value
}
});
}}
/>
);
}
function TTSGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const { ttsConfig } = splitGuideModule({ inputs } as ModuleItemType);
return (
<TTSSelect
value={ttsConfig}
onChange={(e) => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.tts,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.tts),
value: e
}
});
}}
/>
);
}
function WhisperGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const { ttsConfig, whisperConfig } = splitGuideModule({ inputs } as ModuleItemType);
return (
<WhisperConfig
isOpenAudio={ttsConfig.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.whisper,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.whisper),
value: e
}
});
}}
/>
);
}

View File

@@ -1,306 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { DYNAMIC_INPUT_KEY, ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { EditInputFieldMap, EditNodeFieldType } from '@fastgpt/global/core/module/node/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MySelect from '@fastgpt/web/components/common/MySelect';
const FieldEditModal = ({
editField = {
key: true,
name: true,
description: true,
dataType: true
},
defaultField,
keys = [],
onClose,
onSubmit
}: {
editField?: EditInputFieldMap;
defaultField: EditNodeFieldType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const isCreate = useMemo(() => !defaultField.key, [defaultField.key]);
const showDynamicInputSelect =
!keys.includes(DYNAMIC_INPUT_KEY) || defaultField.key === DYNAMIC_INPUT_KEY;
const inputTypeList = [
{
label: t('core.module.inputType.target'),
value: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.input'),
value: FlowNodeInputTypeEnum.input,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
valueType: ModuleIOValueTypeEnum.boolean
},
{
label: t('core.module.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
valueType: ModuleIOValueTypeEnum.selectDataset
},
...(showDynamicInputSelect
? [
{
label: t('core.module.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
valueType: ModuleIOValueTypeEnum.any
}
]
: [])
];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -2)
.map((item) => ({
label: t(item.label),
value: item.value
}));
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: defaultField
});
const inputType = watch('inputType');
const outputType = watch('outputType');
const required = watch('required');
const [refresh, setRefresh] = useState(false);
const showDataTypeSelect = useMemo(() => {
if (!editField.dataType) return false;
if (inputType === undefined) return true;
if (inputType === FlowNodeInputTypeEnum.target) return true;
if (outputType === FlowNodeOutputTypeEnum.source) return true;
return false;
}, [editField.dataType, inputType, outputType]);
const showRequired = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.required || editField.defaultValue;
}, [editField.defaultValue, editField.required, inputType]);
const showNameInput = useMemo(() => {
return editField.name;
}, [editField.name]);
const showKeyInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, inputType]);
const showDescriptionInput = useMemo(() => {
return editField.description;
}, [editField.description]);
const onSubmitSuccess = useCallback(
(data: EditNodeFieldType) => {
if (!data.key) return;
if (isCreate && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
onSubmit({
data,
changeKey: !keys.includes(data.key)
});
},
[isCreate, keys, onSubmit, t, toast]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/module/extract.png"
title={t('core.module.edit.Field Edit')}
onClose={onClose}
>
<ModalBody overflow={'visible'}>
{/* input type select: target, input, textarea.... */}
{editField.inputType && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('core.module.Input Type')}</Box>
<MySelect
w={'288px'}
list={inputTypeList}
value={getValues('inputType')}
onchange={(e: string) => {
const type = e as `${FlowNodeInputTypeEnum}`;
const selectedItem = inputTypeList.find((item) => item.value === type);
setValue('inputType', type);
setValue('valueType', selectedItem?.valueType);
if (type === FlowNodeInputTypeEnum.selectDataset) {
setValue('label', selectedItem?.label);
} else if (type === FlowNodeInputTypeEnum.addInputParam) {
setValue('label', t('core.module.valueType.dynamicTargetInput'));
setValue('key', DYNAMIC_INPUT_KEY);
setValue('required', false);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{showRequired && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('common.Require Input')}</Box>
<Switch
{...register('required', {
onChange(e) {
if (!e.target.checked) {
setValue('defaultValue', '');
}
}
})}
/>
</Flex>
)}
{showRequired && required && editField.defaultValue && (
<Flex alignItems={'center'} mb={5}>
<Box flex={['0 0 70px']}>{t('core.module.Default value')}</Box>
<Input
bg={'myGray.50'}
placeholder={t('core.module.Default value placeholder')}
{...register('defaultValue')}
/>
</Flex>
)}
{editField.isToolInput && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('isToolInput')} />
</Flex>
)}
{showDataTypeSelect && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<MySelect
w={'288px'}
list={dataTypeSelectList}
value={getValues('valueType')}
onchange={(e: string) => {
const type = e as `${ModuleIOValueTypeEnum}`;
setValue('valueType', type);
if (
type === ModuleIOValueTypeEnum.chatHistory ||
type === ModuleIOValueTypeEnum.datasetQuote
) {
const label = dataTypeSelectList.find((item) => item.value === type)?.label;
setValue('label', label);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{showNameInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Name')}</Box>
<Input
bg={'myGray.50'}
placeholder="预约字段/sql语句……"
{...register('label', { required: true })}
/>
</Flex>
)}
{showKeyInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true,
onChange: (e) => {
const value = e.target.value;
// auto fill label
if (!showNameInput) {
setValue('label', value);
}
}
})}
/>
</Flex>
)}
{showDescriptionInput && (
<Box mb={5} alignItems={'flex-start'}>
<Box flex={'0 0 70px'} mb={'1px'}>
{t('core.module.Field Description')}
</Box>
<Textarea
bg={'myGray.50'}
placeholder={t('common.choosable')}
rows={5}
{...register('description')}
/>
</Box>
)}
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -1,270 +0,0 @@
import React, { useMemo } from 'react';
import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import type { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { useTranslation } from 'next-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { onChangeNode, onCopyNode, onResetNode, useFlowProviderStore } from '../../FlowProvider';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { ToolTargetHandle } from './ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import TriggerAndFinish from './RenderInput/templates/TriggerAndFinish';
type Props = FlowModuleItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
maxW?: string | number;
forbidMenu?: boolean;
selected?: boolean;
};
const NodeCard = (props: Props) => {
const { t } = useTranslation();
const {
children,
avatar = LOGO_ICON,
name = t('core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
moduleId,
flowType,
inputs,
selected,
forbidMenu,
isTool = false
} = props;
const { toast } = useToast();
const { setLoading } = useSystemStore();
const { nodes, splitToolInputs, onDelNode } = useFlowProviderStore();
// edit intro
const { onOpenModal: onOpenIntroModal, EditModal: EditIntroModal } = useEditTextarea({
title: t('core.module.Edit intro'),
tip: '调整该模块会对工具调用时机有影响。\n你可以通过精确的描述该模块功能引导模型进行工具调用。',
canEmpty: false
});
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
content: t('module.Confirm Sync Plugin')
});
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const showToolHandle = useMemo(
() => isTool && !!nodes.find((item) => item.data?.flowType === FlowNodeTypeEnum.tools),
[isTool, nodes]
);
const moduleIsTool = useMemo(() => {
const { isTool } = splitToolInputs([], moduleId);
return isTool;
}, [moduleId, splitToolInputs]);
const Header = useMemo(() => {
const menuList = [
...(flowType === FlowNodeTypeEnum.pluginModule
? [
{
icon: 'common/refreshLight',
label: t('plugin.Synchronous version'),
variant: 'whiteBase',
onClick: () => {
const pluginId = inputs.find(
(item) => item.key === ModuleInputKeyEnum.pluginId
)?.value;
if (!pluginId) return;
onOpenConfirmSync(async () => {
try {
setLoading(true);
const pluginModule = await getPreviewPluginModule(pluginId);
onResetNode({
id: moduleId,
module: pluginModule
});
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
}
setLoading(false);
})();
}
}
]
: [
{
icon: 'edit',
label: t('common.Rename'),
variant: 'whiteBase',
onClick: () =>
onOpenModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
moduleId,
type: 'attr',
key: 'name',
value: e
});
}
})
}
]),
{
icon: 'copy',
label: t('common.Copy'),
variant: 'whiteBase',
onClick: () => onCopyNode(moduleId)
},
{
icon: 'delete',
label: t('common.Delete'),
variant: 'whiteDanger',
onClick: onOpenConfirmDeleteNode(() => onDelNode(moduleId))
}
];
return (
<Box className="custom-drag-handle" px={4} py={3} position={'relative'}>
{showToolHandle && <ToolTargetHandle moduleId={moduleId} />}
<Flex alignItems={'center'}>
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'}>
{t(name)}
</Box>
</Flex>
{!forbidMenu && (
<Box
className="nodrag controller-menu"
display={'none'}
flexDirection={'column'}
gap={3}
position={'absolute'}
top={'-20px'}
right={0}
transform={'translateX(90%)'}
pl={'20px'}
pr={'10px'}
pb={'20px'}
pt={'20px'}
>
{menuList.map((item) => (
<Box key={item.icon}>
<Button
size={'xs'}
variant={item.variant}
leftIcon={<MyIcon name={item.icon as any} w={'12px'} />}
onClick={item.onClick}
>
{item.label}
</Button>
</Box>
))}
</Box>
)}
<Flex alignItems={'flex-end'} py={1}>
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
{t(intro)}
</Box>
{moduleIsTool && (
<Button
size={'xs'}
variant={'whiteBase'}
onClick={() => {
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
moduleId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
{t('core.module.Edit intro')}
</Button>
)}
</Flex>
{/* switch */}
<TriggerAndFinish moduleId={moduleId} isTool={moduleIsTool} />
</Box>
);
}, [
flowType,
t,
onOpenConfirmDeleteNode,
showToolHandle,
moduleId,
avatar,
name,
forbidMenu,
intro,
moduleIsTool,
inputs,
onOpenConfirmSync,
setLoading,
toast,
onOpenModal,
onDelNode,
onOpenIntroModal
]);
const RenderModal = useMemo(() => {
return (
<>
<EditTitleModal maxLength={20} />
{moduleIsTool && <EditIntroModal maxLength={500} />}
<ConfirmSyncModal />
<ConfirmDeleteModal />
</>
);
}, [ConfirmDeleteModal, ConfirmSyncModal, EditIntroModal, EditTitleModal, moduleIsTool]);
return (
<Box
minW={minW}
maxW={maxW}
bg={'white'}
borderWidth={'1px'}
borderColor={selected ? 'primary.600' : 'borderColor.base'}
borderRadius={'md'}
boxShadow={'1'}
_hover={{
boxShadow: '4',
'& .controller-menu': {
display: 'flex'
}
}}
>
{Header}
<Box className="nowheel">{children}</Box>
{RenderModal}
</Box>
);
};
export default React.memo(NodeCard);

View File

@@ -1,147 +0,0 @@
import { EditNodeFieldType, FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { onChangeNode, useFlowProviderStoreType } from '../../../FlowProvider';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import TargetHandle from '../TargetHandle';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
type Props = FlowNodeInputItemType & {
moduleId: string;
inputKey: string;
mode: useFlowProviderStoreType['mode'];
};
const InputLabel = ({ moduleId, inputKey, mode, ...item }: Props) => {
const { t } = useTranslation();
const {
required = false,
description,
edit,
label,
type,
valueType,
showTargetInApp,
showTargetInPlugin
} = item;
const [editField, setEditField] = useState<EditNodeFieldType>();
const targetHandle = useMemo(() => {
if (type === FlowNodeInputTypeEnum.target) return true;
if (mode === 'app' && showTargetInApp) return true;
if (mode === 'plugin' && showTargetInPlugin) return true;
return false;
}, [mode, showTargetInApp, showTargetInPlugin, type]);
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{t(label)}
{description && (
<MyTooltip label={t(description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
{required && (
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
{targetHandle && <TargetHandle handleKey={inputKey} valueType={valueType} />}
{edit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: type,
valueType: valueType,
key: inputKey,
required,
label,
description
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField?.key && (
<FieldEditModal
editField={item.editField}
keys={[editField.key]}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
const newInput: FlowNodeInputItemType = {
...item,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
export default React.memo(InputLabel);

View File

@@ -1,57 +0,0 @@
import React, { useState } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { EditNodeFieldType } from '@fastgpt/global/core/module/node/type';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(item.defaultEditField || {});
}}
>
{t('core.module.input.Add Input')}
</Button>
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={inputs.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
key: data.key,
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: item.editField
}
});
setEditField(undefined);
}}
/>
)}
</>
);
};
export default React.memo(AddInputParam);

View File

@@ -1,39 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper
} from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const NumberInputRender = ({ item, moduleId }: RenderInputProps) => {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
};
export default React.memo(NumberInputRender);

View File

@@ -1,27 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import MySelect from '@fastgpt/web/components/common/MySelect';
const SelectRender = ({ item, moduleId }: RenderInputProps) => {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
};
export default React.memo(SelectRender);

View File

@@ -1,175 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { getFlowStore, onChangeNode, useFlowProviderStoreType } from '../../../../FlowProvider';
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@fastgpt/global/core/module/api';
import Avatar from '@/components/Avatar';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
const DatasetSelectModal = dynamic(() => import('@/components/core/module/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/module/DatasetParamsModal'));
const SelectDatasetRender = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const theme = useTheme();
const { llmModelList } = useSystemStore();
const [nodes, setNodes] = useState<useFlowProviderStoreType['nodes']>([]);
const [data, setData] = useState({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5,
usingReRank: false
});
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenDatasetSelect,
onClose: onCloseDatasetSelect
} = useDisclosure();
const selectedDatasets = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value?.find((item) => item.datasetId === dataset._id));
}, [allDatasets, item.value]);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodes.forEach((item) => {
if (item.type === FlowNodeTypeEnum.chatNode) {
const model =
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [nodes]);
const {
isOpen: isOpenDatasetPrams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
useQuery(['loadAllDatasets'], loadAllDatasets);
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
useEffect(() => {
async () => {
const { nodes } = await getFlowStore();
setNodes(nodes);
};
}, []);
return (
<>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} minW={'350px'} w={'100%'}>
<Button
h={'36px'}
leftIcon={<MyIcon name={'common/selectLight'} w={'14px'} />}
onClick={onOpenDatasetSelect}
>
{t('common.Choose')}
</Button>
{/* <Button
h={'36px'}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'14px'} />}
onClick={onOpenDatasetParams}
>
{t('core.dataset.search.Params Setting')}
</Button> */}
{selectedDatasets.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseDatasetSelect}
/>
)}
{/* {isOpenDatasetPrams && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)} */}
</>
);
};
export default React.memo(SelectDatasetRender);

View File

@@ -1,54 +0,0 @@
import React, { useCallback, useEffect } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
const SelectAiModelRender = ({ item, moduleId }: RenderInputProps) => {
const { llmModelList } = useSystemStore();
const modelList = llmModelList.filter((model) => {
if (!item.llmModelType) return true;
const filterField = llmModelTypeFilterMap[item.llmModelType];
if (!filterField) return true;
//@ts-ignore
return !!model[filterField];
});
const onChangeModel = useCallback(
(e: string) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, moduleId]
);
useEffect(() => {
if (!item.value && modelList.length > 0) {
onChangeModel(modelList[0].model);
}
}, [item.value, modelList, onChangeModel]);
return (
<AIModelSelector
minW={'350px'}
width={'100%'}
value={item.value}
list={modelList.map((item) => ({
value: item.model,
label: item.name
}))}
onchange={onChangeModel}
/>
);
};
export default React.memo(SelectAiModelRender);

View File

@@ -1,49 +0,0 @@
import React, { useCallback } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { SettingAIDataType } from '@fastgpt/global/core/module/node/type';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
const SelectAiModelRender = ({ item, inputs = [], moduleId }: RenderInputProps) => {
const onChangeModel = useCallback(
(e: SettingAIDataType) => {
for (const key in e) {
const input = inputs.find((input) => input.key === key);
input &&
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...input,
// @ts-ignore
value: e[key]
}
});
}
},
[inputs, moduleId]
);
const llmModelData: SettingAIDataType = {
model: inputs.find((input) => input.key === ModuleInputKeyEnum.aiModel)?.value ?? '',
maxToken:
inputs.find((input) => input.key === ModuleInputKeyEnum.aiChatMaxToken)?.value ?? 2048,
temperature:
inputs.find((input) => input.key === ModuleInputKeyEnum.aiChatTemperature)?.value ?? 1,
isResponseAnswerText: inputs.find(
(input) => input.key === ModuleInputKeyEnum.aiChatIsResponseText
)?.value
};
return (
<SettingLLMModel
llmModelType={item.llmModelType}
defaultData={llmModelData}
onChange={onChangeModel}
/>
);
};
export default React.memo(SelectAiModelRender);

View File

@@ -1,243 +0,0 @@
import React, { useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, BoxProps, Button, Flex, ModalFooter, useDisclosure } from '@chakra-ui/react';
import { onChangeNode, useFlowProviderStore } from '../../../../FlowProvider';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useForm } from 'react-hook-form';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type';
import { useTranslation } from 'next-i18next';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/utils';
import { ModalBody } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import {
Prompt_QuotePromptList,
Prompt_QuoteTemplateList
} from '@fastgpt/global/core/ai/prompt/AIChat';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import PromptTemplate from '@/components/PromptTemplate';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
const SettingQuotePrompt = ({ inputs = [], moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const { nodes } = useFlowProviderStore();
const { watch, setValue, handleSubmit } = useForm({
defaultValues: {
quoteTemplate: inputs.find((input) => input.key === 'quoteTemplate')?.value || '',
quotePrompt: inputs.find((input) => input.key === 'quotePrompt')?.value || ''
}
});
const aiChatQuoteTemplate = watch('quoteTemplate');
const aiChatQuotePrompt = watch('quotePrompt');
const variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
const systemVariables = [
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
return [...globalVariables, ...moduleVariables, ...systemVariables];
}, [inputs, t]);
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
templates: PromptTemplateItem[];
}>();
const quoteTemplateVariables = (() => [
{
key: 'q',
label: 'q',
icon: 'core/app/simpleMode/variable'
},
{
key: 'a',
label: 'a',
icon: 'core/app/simpleMode/variable'
},
{
key: 'source',
label: t('core.dataset.search.Source name'),
icon: 'core/app/simpleMode/variable'
},
{
key: 'sourceId',
label: t('core.dataset.search.Source id'),
icon: 'core/app/simpleMode/variable'
},
{
key: 'index',
label: t('core.dataset.search.Quote index'),
icon: 'core/app/simpleMode/variable'
},
...variables
])();
const quotePromptVariables = (() => [
{
key: 'quote',
label: t('core.app.Quote templates'),
icon: 'core/app/simpleMode/variable'
},
{
key: 'question',
label: t('core.module.input.label.user question'),
icon: 'core/app/simpleMode/variable'
},
...variables
])();
const LabelStyles: BoxProps = {
fontSize: ['sm', 'md']
};
const selectTemplateBtn: BoxProps = {
color: 'primary.500',
cursor: 'pointer'
};
const onSubmit = (data: { quoteTemplate: string; quotePrompt: string }) => {
const quoteTemplateInput = inputs.find(
(input) => input.key === ModuleInputKeyEnum.aiChatQuoteTemplate
);
const quotePromptInput = inputs.find(
(input) => input.key === ModuleInputKeyEnum.aiChatQuotePrompt
);
if (quoteTemplateInput) {
onChangeNode({
moduleId,
type: 'updateInput',
key: quoteTemplateInput.key,
value: {
...quoteTemplateInput,
value: data.quoteTemplate
}
});
}
if (quotePromptInput) {
onChangeNode({
moduleId,
type: 'updateInput',
key: quotePromptInput.key,
value: {
...quotePromptInput,
value: data.quotePrompt
}
});
}
onClose();
};
return (
<>
<Button variant={'whitePrimary'} size={'sm'} onClick={onOpen}>
{t('core.module.Setting quote prompt')}
</Button>
<MyModal
isOpen={isOpen}
iconSrc={'modal/edit'}
title={t('core.module.Quote prompt setting')}
w={'600px'}
>
<ModalBody>
<Box>
<Flex {...LabelStyles} mb={1}>
{t('core.app.Quote templates')}
<MyTooltip
label={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: t('core.app.Select quote template'),
templates: Prompt_QuoteTemplateList
})
}
>
{t('common.Select template')}
</Box>
</Flex>
<PromptEditor
variables={quoteTemplateVariables}
h={160}
title={t('core.app.Quote templates')}
placeholder={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
value={aiChatQuoteTemplate}
onChange={(e) => {
setValue('quoteTemplate', e);
}}
/>
</Box>
<Box mt={4}>
<Flex {...LabelStyles} mb={1}>
{t('core.app.Quote prompt')}
<MyTooltip
label={t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value })}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
<PromptEditor
variables={quotePromptVariables}
title={t('core.app.Quote prompt')}
h={280}
placeholder={t('template.Quote Prompt Tip', {
default: Prompt_QuotePromptList[0].value
})}
value={aiChatQuotePrompt}
onChange={(e) => {
setValue('quotePrompt', e);
}}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
{!!selectTemplateData && (
<PromptTemplate
title={selectTemplateData.title}
templates={selectTemplateData.templates}
onClose={() => setSelectTemplateData(undefined)}
onSuccess={(e) => {
const quoteVal = e.value;
const promptVal = Prompt_QuotePromptList.find((item) => item.title === e.title)?.value;
setValue('quoteTemplate', quoteVal);
setValue('quotePrompt', promptVal);
}}
/>
)}
</>
);
};
export default React.memo(SettingQuotePrompt);

View File

@@ -1,35 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import MySlider from '@/components/Slider';
const SliderRender = ({ item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
return (
<Box px={2}>
<MySlider
markList={item.markList}
width={'100%'}
min={item.min || 0}
max={item.max}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
};
export default React.memo(SliderRender);

View File

@@ -1,26 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Switch } from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const SwitchRender = ({ item, moduleId }: RenderInputProps) => {
return (
<Switch
size={'lg'}
isChecked={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.checked
}
});
}}
/>
);
};
export default React.memo(SwitchRender);

View File

@@ -1,26 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Input } from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const TextInput = ({ item, moduleId }: RenderInputProps) => {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
};
export default React.memo(TextInput);

View File

@@ -1,66 +0,0 @@
import React, { useCallback, useMemo, useTransition } from 'react';
import type { RenderInputProps } from '../type';
import { useFlowProviderStore, onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/utils';
const TextareaRender = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const { nodes } = useFlowProviderStore();
// get variable
const variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
const systemVariables = [
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
return [...globalVariables, ...moduleVariables, ...systemVariables];
}, [inputs, nodes, t]);
const onChange = useCallback(
(e: string) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, moduleId]
);
return (
<PromptEditor
variables={variables}
title={t(item.label)}
h={150}
placeholder={t(item.placeholder || '')}
value={item.value}
onChange={onChange}
/>
);
};
export default React.memo(TextareaRender);

View File

@@ -1,61 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import TargetHandle from '../../TargetHandle';
import SourceHandle from '../../SourceHandle';
import { ModuleInputKeyEnum, ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useFlowProviderStore } from '../../../../FlowProvider';
const TriggerAndFinish = ({ moduleId, isTool }: { moduleId: string; isTool: boolean }) => {
const { t } = useTranslation();
const { nodes } = useFlowProviderStore();
const inputs = useMemo(
() => nodes.find((node) => node.data.moduleId === moduleId)?.data?.inputs || [],
[moduleId, nodes]
);
const hasSwitch = useMemo(
() => inputs.some((input) => input.key === ModuleInputKeyEnum.switch),
[inputs]
);
const outputs = useMemo(
() => nodes.find((node) => node.data.moduleId === moduleId)?.data?.outputs || [],
[moduleId, nodes]
);
const hasFinishOutput = useMemo(
() => outputs.some((output) => output.key === ModuleOutputKeyEnum.finish),
[outputs]
);
const Render = useMemo(() => {
return (
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
justifyContent={'space-between'}
position={'relative'}
>
<Box position={'relative'}>
{!isTool && (
<Box mt={2}>
<TargetHandle handleKey={ModuleInputKeyEnum.switch} valueType={'any'} />
{t('core.module.input.label.switch')}
</Box>
)}
</Box>
{hasFinishOutput && (
<Box position={'relative'} mt={2}>
{t('core.module.output.label.running done')}
<SourceHandle handleKey={ModuleOutputKeyEnum.finish} valueType={'boolean'} />
</Box>
)}
</Flex>
);
}, [hasFinishOutput, isTool, t]);
return hasSwitch ? Render : null;
};
export default React.memo(TriggerAndFinish);

View File

@@ -1,41 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import TargetHandle from '../../TargetHandle';
import SourceHandle from '../../SourceHandle';
import { ModuleInputKeyEnum, ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
const UserChatInput = ({ item }: RenderInputProps) => {
const { t } = useTranslation();
return (
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
justifyContent={'space-between'}
position={'relative'}
>
<Box position={'relative'}>
<TargetHandle handleKey={ModuleInputKeyEnum.userChatInput} valueType={item.valueType} />
{t('core.module.input.label.user question')}
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
</Box>
<Box position={'relative'}>
{t('core.module.input.label.user question')}
<SourceHandle handleKey={ModuleOutputKeyEnum.userChatInput} valueType={item.valueType} />
</Box>
</Flex>
);
};
export default React.memo(UserChatInput);

View File

@@ -1,142 +0,0 @@
import { EditNodeFieldType, FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { onChangeNode } from '../../../FlowProvider';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import SourceHandle from '../SourceHandle';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
const OutputLabel = ({
moduleId,
outputKey,
outputs,
...item
}: FlowNodeOutputItemType & {
outputKey: string;
moduleId: string;
outputs: FlowNodeOutputItemType[];
}) => {
const { t } = useTranslation();
const { label = '', description, edit } = item;
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<Flex
className="nodrag"
cursor={'default'}
justifyContent={'right'}
alignItems={'center'}
position={'relative'}
>
{edit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
key: outputKey,
label: item.label,
description: item.description,
valueType: item.valueType,
outputType: item.type,
required: item.required,
defaultValue: item.defaultValue
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delOutput',
key: outputKey
});
}}
/>
</>
)}
{description && (
<MyTooltip label={t(description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box position={'relative'}>
{item.required && (
<Box
position={'absolute'}
top={'-2px'}
left={'-5px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
{t(label)}
</Box>
{item.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle handleKey={outputKey} valueType={item.valueType} />
)}
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={[outputKey]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.outputType || !data.key) return;
const newOutput: FlowNodeOutputItemType = {
...item,
type: data.outputType,
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description,
required: data.required,
defaultValue: data.defaultValue
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
export default React.memo(OutputLabel);

View File

@@ -1,80 +0,0 @@
import React, { useMemo } from 'react';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { Box } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import OutputLabel from './Label';
import { RenderOutputProps } from './type';
import dynamic from 'next/dynamic';
const RenderList: {
types: `${FlowNodeOutputTypeEnum}`[];
Component: React.ComponentType<RenderOutputProps>;
}[] = [
{
types: [FlowNodeOutputTypeEnum.addOutputParam],
Component: dynamic(() => import('./templates/AddOutputParam'))
}
];
const RenderToolOutput = ({
moduleId,
flowOutputList
}: {
moduleId: string;
flowOutputList: FlowNodeOutputItemType[];
}) => {
const sortOutputs = useMemo(
() =>
[...flowOutputList].sort((a, b) => {
if (a.type === FlowNodeOutputTypeEnum.addOutputParam) {
return 1;
}
if (b.type === FlowNodeOutputTypeEnum.addOutputParam) {
return -1;
}
if (a.key === ModuleOutputKeyEnum.finish) return -1;
if (b.key === ModuleOutputKeyEnum.finish) return 1;
return 0;
}),
[flowOutputList]
);
return (
<>
{sortOutputs.map((output) => {
const RenderComponent = (() => {
const Component = RenderList.find(
(item) => output.type && item.types.includes(output.type)
)?.Component;
if (!Component) return null;
return <Component outputs={sortOutputs} item={output} moduleId={moduleId} />;
})();
return (
output.type !== FlowNodeOutputTypeEnum.hidden && (
<Box key={output.key} _notLast={{ mb: 7 }} position={'relative'}>
{output.label && (
<OutputLabel
moduleId={moduleId}
outputKey={output.key}
outputs={sortOutputs}
{...output}
/>
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
)
);
})}
</>
);
};
export default React.memo(RenderToolOutput);

View File

@@ -1,60 +0,0 @@
import React, { useState } from 'react';
import type { RenderOutputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Box, Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/module/node/type';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddOutputParam = ({ outputs = [], item, moduleId }: RenderOutputProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<Box textAlign={'right'}>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(item.defaultEditField || {});
}}
>
{t('core.module.output.Add Output')}
</Button>
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={outputs.map((output) => output.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addOutput',
key: data.key,
value: {
type: data.outputType,
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description,
required: data.required,
defaultValue: data.defaultValue,
edit: true,
editField: item.editField,
targets: []
}
});
setEditField(undefined);
}}
/>
)}
</Box>
);
};
export default React.memo(AddOutputParam);

View File

@@ -1,25 +0,0 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
export const defaultEditFormData: FlowNodeInputItemType = {
valueType: 'string',
type: FlowNodeInputTypeEnum.target,
key: '',
label: '',
toolDescription: '',
required: true,
edit: true,
editField: {
key: true,
description: true,
dataType: true
},
defaultEditField: {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string
}
};

View File

@@ -1,58 +0,0 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, Position } from 'reactflow';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${ModuleIOValueTypeEnum}`;
}
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? ModuleIOValueTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType && FlowValueTypeMap[valueType]
? FlowValueTypeMap[valueType]?.handlerStyle
: FlowValueTypeMap[ModuleIOValueTypeEnum.any]?.handlerStyle,
[valueType]
);
return (
<Box
position={'absolute'}
top={'50%'}
right={'-18px'}
transform={'translate(0,-50%)'}
{...props}
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeMap[valType]?.label),
description: FlowValueTypeMap[valType]?.description
})}
>
<Handle
style={{
width: '14px',
height: '14px',
borderWidth: '3.5px',
backgroundColor: 'white',
...valueStyle
}}
type="source"
id={handleKey}
position={Position.Right}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(SourceHandle);

View File

@@ -1,107 +0,0 @@
import MyTooltip from '@/components/MyTooltip';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { Box, BoxProps } from '@chakra-ui/react';
import {
ModuleIOValueTypeEnum,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { Connection, Handle, Position } from 'reactflow';
import { useFlowProviderStore } from '../../FlowProvider';
import { useCallback } from 'react';
type ToolHandleProps = BoxProps & {
moduleId: string;
};
export const ToolTargetHandle = ({ moduleId }: ToolHandleProps) => {
const { t } = useTranslation();
const valueTypeMap = FlowValueTypeMap[ModuleIOValueTypeEnum.tools];
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
shouldWrapChildren={false}
>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent'
}}
type="target"
id={ModuleOutputKeyEnum.selectedTools}
position={Position.Top}
>
<Box
w={'14px'}
h={'14px'}
border={'4px solid #5E8FFF'}
transform={'translate(-40%,-30%) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
</MyTooltip>
);
};
export const ToolSourceHandle = ({ moduleId }: ToolHandleProps) => {
const { t } = useTranslation();
const { setEdges, nodes } = useFlowProviderStore();
const valueTypeMap = FlowValueTypeMap[ModuleIOValueTypeEnum.tools];
/* onConnect edge, delete tool input and switch */
const onConnect = useCallback(
(e: Connection) => {
const node = nodes.find((node) => node.id === e.target);
if (!node) return;
const inputs = node.data.inputs;
setEdges((edges) =>
edges.filter((edge) => {
const input = inputs.find((input) => input.key === edge.targetHandle);
if (
edge.target === node.id &&
(!!input?.toolDescription || input?.key === ModuleInputKeyEnum.switch)
) {
return false;
}
return true;
})
);
},
[nodes, setEdges]
);
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
shouldWrapChildren={false}
>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent'
}}
type="source"
id={ModuleOutputKeyEnum.selectedTools}
position={Position.Bottom}
onConnect={onConnect}
>
<Box
w={'14px'}
h={'14px'}
border={'4px solid #5E8FFF'}
transform={'translate(-40%,-30%) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
</MyTooltip>
);
};

View File

@@ -1,203 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import ReactFlow, {
Background,
Connection,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow
} from 'reactflow';
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import ButtonEdge from './components/modules/ButtonEdge';
import ModuleTemplateList from './ModuleTemplateList';
import { useFlowProviderStore } from './FlowProvider';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
const NodeSimple = dynamic(() => import('./components/nodes/NodeSimple'));
const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
[FlowNodeTypeEnum.userGuide]: dynamic(() => import('./components/nodes/NodeUserGuide')),
[FlowNodeTypeEnum.questionInput]: dynamic(() => import('./components/nodes/NodeQuestionInput')),
[FlowNodeTypeEnum.historyNode]: NodeSimple,
[FlowNodeTypeEnum.chatNode]: NodeSimple,
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(
() => import('./components/nodes/NodeDatasetConcat')
),
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./components/nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./components/nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./components/nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./components/nodes/NodeHttp')),
[FlowNodeTypeEnum.httpRequest]: NodeSimple,
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./components/nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./components/nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./components/nodes/NodeTools')),
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowModuleItemType>) => (
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./components/nodes/NodeLaf'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Container = React.memo(function Container() {
const { toast } = useToast();
const { t } = useTranslation();
const { reactFlowWrapper, nodes, onNodesChange, edges, onEdgesChange, onConnect } =
useFlowProviderStore();
const customOnConnect = useCallback(
(connect: Connection) => {
if (!connect.sourceHandle || !connect.targetHandle) {
return;
}
if (connect.source === connect.target) {
return toast({
status: 'warning',
title: t('core.module.Can not connect self')
});
}
onConnect({
connect
});
},
[onConnect, t, toast]
);
return (
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={{
animated: true,
zIndex: 0
}}
elevateEdgesOnSelect
connectionLineStyle={{ strokeWidth: 2, stroke: '#5A646Es' }}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={customOnConnect}
>
<FlowController />
</ReactFlow>
);
});
const Flow = ({ Header, ...data }: { Header: React.ReactNode }) => {
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
const memoRenderContainer = useMemo(() => {
return (
<Box
minH={'400px'}
flex={'1 0 0'}
w={'100%'}
h={0}
position={'relative'}
onContextMenu={(e) => {
e.preventDefault();
return false;
}}
>
{/* open module template */}
<IconButton
position={'absolute'}
top={5}
left={5}
size={'mdSquare'}
borderRadius={'50%'}
icon={<SmallCloseIcon fontSize={'26px'} />}
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<Container {...data} />
<ModuleTemplateList isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</Box>
);
}, [data, isOpenTemplate, onCloseTemplate, onOpenTemplate]);
return (
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
<ReactFlowProvider>
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
{Header}
{memoRenderContainer}
</Flex>
</ReactFlowProvider>
</Box>
);
};
export default React.memo(Flow);
const FlowController = React.memo(function FlowController() {
const { fitView } = useReactFlow();
return (
<>
<MiniMap
style={{
height: 78,
width: 126,
marginBottom: 35
}}
pannable
/>
<Controls
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 5,
background: 'white',
borderRadius: '6px',
overflow: 'hidden',
boxShadow:
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
showInteractive={false}
showFitView={false}
>
<MyTooltip label={'页面居中'}>
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
<MyIcon name={'core/modules/fixview'} w={'14px'} />
</ControlButton>
</MyTooltip>
</Controls>
<Background />
</>
);
});

View File

@@ -1,74 +0,0 @@
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeOutputTargetItemType } from '@fastgpt/global/core/module/node/type';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { type Node, type Edge } from 'reactflow';
export const flowNode2Modules = ({
nodes,
edges
}: {
nodes: Node<FlowModuleItemType, string | undefined>[];
edges: Edge<any>[];
}) => {
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
intro: item.data.intro,
avatar: item.data.avatar,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((input) => ({
...input,
connected: false
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowNodeOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected = !!edges.find(
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter((edge) => {
if (
edge.source === module.moduleId &&
edge.sourceHandle === output.key &&
edge.targetHandle
) {
return true;
}
})
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
};
export const filterExportModules = (modules: ModuleItemType[]) => {
modules.forEach((module) => {
// dataset - remove select dataset value
if (module.flowType === FlowNodeTypeEnum.datasetSearchNode) {
module.inputs.forEach((item) => {
if (item.key === ModuleInputKeyEnum.datasetSelectList) {
item.value = [];
}
});
}
});
return JSON.stringify(modules, null, 2);
};

View File

@@ -1,4 +1,4 @@
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import React, {
useMemo,
@@ -16,10 +16,16 @@ import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
getDefaultEntryNodeIds,
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
export type ChatTestComponentRef = {
resetChatTest: () => void;
@@ -28,11 +34,13 @@ export type ChatTestComponentRef = {
const ChatTest = (
{
app,
modules = [],
nodes = [],
edges = [],
onClose
}: {
app: AppSchema;
modules?: ModuleItemType[];
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
@@ -40,16 +48,17 @@ const ChatTest = (
const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const isOpen = useMemo(() => modules && modules.length > 0, [modules]);
const isOpen = useMemo(() => nodes && nodes.length > 0, [nodes]);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
/* get histories */
let historyMaxLen = 6;
modules.forEach((module) => {
module.inputs.forEach((input) => {
nodes.forEach((node) => {
node.inputs.forEach((input) => {
if (
(input.key === ModuleInputKeyEnum.history ||
input.key === ModuleInputKeyEnum.historyMaxAmount) &&
(input.key === NodeInputKeyEnum.history ||
input.key === NodeInputKeyEnum.historyMaxAmount) &&
typeof input.value === 'number'
) {
historyMaxLen = Math.max(historyMaxLen, input.value);
@@ -64,10 +73,12 @@ const ChatTest = (
data: {
history,
prompt: chatList[chatList.length - 2].value,
modules,
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges),
variables,
appId: app._id,
appName: `调试-${app.name}`
appName: `调试-${app.name}`,
mode: 'test'
},
onMessage: generatingMessage,
abortCtrl: controller
@@ -75,7 +86,7 @@ const ChatTest = (
return { responseText, responseData };
},
[app._id, app.name, modules]
[app._id, app.name, edges, nodes]
);
useImperativeHandle(ref, () => ({
@@ -138,14 +149,14 @@ const ChatTest = (
appAvatar={app.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(modules)}
showFileSelector={checkChatSupportSelectFileByModules(modules)}
userGuideModule={getGuideModule(nodes)}
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
</Flex>
{/* <Box
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
@@ -154,7 +165,7 @@ const ChatTest = (
bottom={0}
right={0}
onClick={onClose}
/> */}
/>
</>
);
};

View File

@@ -0,0 +1,682 @@
import {
type Node,
type NodeChange,
type Edge,
type EdgeChange,
useNodesState,
useEdgesState,
OnConnectStartParams
} from 'reactflow';
import type {
FlowNodeItemType,
FlowNodeTemplateType
} from '@fastgpt/global/core/workflow/type/index.d';
import type { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe.d';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, {
type SetStateAction,
type Dispatch,
useContext,
useCallback,
createContext,
useRef,
useMemo,
useState,
useEffect
} from 'react';
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { defaultRunningStatus } from '../constants';
import { postWorkflowDebug } from '@/web/core/workflow/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type useFlowProviderStoreType = {
// connect
connectingEdge: OnConnectStartParams | undefined;
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
// nodes
basicNodeTemplates: FlowNodeTemplateType[];
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
mode: 'app' | 'plugin';
filterAppIds: string[];
nodes: Node<FlowNodeItemType, string | undefined>[];
nodeList: FlowNodeItemType[];
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
// debug
workflowDebugData:
| {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[];
}
| undefined;
onNextNodeDebug: () => Promise<void>;
onStartNodeDebug: ({
entryNodeId,
runtimeNodes,
runtimeEdges
}: {
entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}) => Promise<void>;
onStopNodeDebug: () => void;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onResetNode: (e: { id: string; module: FlowNodeTemplateType }) => void;
onDelEdge: (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
initData: (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
nodeId: string
) => {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
};
hasToolNode: boolean;
hoverNodeId: string | undefined;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onUpdateNodeError: (node: string, isError: Boolean) => void;
};
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
mode: 'app',
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
): void {
return;
},
onNodesChange: function (changes: NodeChange[]): void {
return;
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
return;
},
onEdgesChange: function (changes: EdgeChange[]): void {
return;
},
onFixView: function (): void {
return;
},
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onDelEdge: function (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
return;
},
onResetNode: function (e): void {
throw new Error('Function not implemented.');
},
splitToolInputs: function (
inputs: FlowNodeInputItemType[],
nodeId: string
): {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
} {
throw new Error('Function not implemented.');
},
hasToolNode: false,
connectingEdge: undefined,
basicNodeTemplates: [],
initData: function (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }): void {
throw new Error('Function not implemented.');
},
hoverNodeId: undefined,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
onUpdateNodeError: function (nodeId: string, isError: Boolean): void {
throw new Error('Function not implemented.');
},
nodeList: [],
workflowDebugData: undefined,
onNextNodeDebug: function (): Promise<void> {
throw new Error('Function not implemented.');
},
onStartNodeDebug: function ({
entryNodeId,
runtimeNodes,
runtimeEdges
}: {
entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}): Promise<void> {
throw new Error('Function not implemented.');
},
onStopNodeDebug: function (): void {
throw new Error('Function not implemented.');
},
setConnectingEdge: function (
value: React.SetStateAction<OnConnectStartParams | undefined>
): void {
throw new Error('Function not implemented.');
}
});
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({
mode,
basicNodeTemplates = [],
filterAppIds = [],
children,
appId,
pluginId
}: {
mode: useFlowProviderStoreType['mode'];
basicNodeTemplates: FlowNodeTemplateType[];
filterAppIds?: string[];
children: React.ReactNode;
appId?: string;
pluginId?: string;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { toast } = useToast();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [hoverNodeId, setHoverNodeId] = useState<string>();
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
const stringifyNodes = useMemo(() => JSON.stringify(nodes.map((node) => node.data)), [nodes]);
const nodeList = useMemo(
() => JSON.parse(stringifyNodes) as FlowNodeItemType[],
[stringifyNodes]
);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodes]);
const onFixView = useCallback(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
}, []);
/* edge */
const onDelEdge = useCallback(
({
nodeId,
sourceHandle,
targetHandle
}: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => {
if (!sourceHandle && !targetHandle) return;
setEdges((state) =>
state.filter((edge) => {
if (edge.source === nodeId && edge.sourceHandle === sourceHandle) return false;
if (edge.target === nodeId && edge.targetHandle === targetHandle) return false;
return true;
})
);
},
[setEdges]
);
/* node */
// reset a node data. delete edge and replace it
const onResetNode = useCallback(
({ id, module }: { id: string; module: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ nodeId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ nodeId: id, sourceHandle: item.key });
});
return {
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
},
[onDelEdge, setNodes]
);
const onChangeNode = useCallback(
(props: FlowNodeChangeProps) => {
const { nodeId, type } = props;
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== nodeId) return node;
const updateObj: Record<string, any> = {};
if (type === 'attr') {
if (props.key) {
updateObj[props.key] = props.value;
}
} else if (type === 'updateInput') {
updateObj.inputs = node.data.inputs.map((item) =>
item.key === props.key ? props.value : item
);
} else if (type === 'replaceInput') {
onDelEdge({ nodeId, targetHandle: props.key });
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === props.key);
updateObj.inputs = node.data.inputs.filter((item) => item.key !== props.key);
setTimeout(() => {
onChangeNode({
nodeId,
type: 'addInput',
index: oldInputIndex,
value: props.value
});
});
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === props.value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.inputs = node.data.inputs;
} else {
if (props.index !== undefined) {
const inputs = [...node.data.inputs];
inputs.splice(props.index, 0, props.value);
updateObj.inputs = inputs;
} else {
updateObj.inputs = node.data.inputs.concat(props.value);
}
}
} else if (type === 'delInput') {
onDelEdge({ nodeId, targetHandle: props.key });
updateObj.inputs = node.data.inputs.filter((item) => item.key !== props.key);
} else if (type === 'updateOutput') {
updateObj.outputs = node.data.outputs.map((item) =>
item.key === props.key ? props.value : item
);
} else if (type === 'replaceOutput') {
onDelEdge({ nodeId, sourceHandle: props.key });
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === props.key);
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
console.log(props.value);
setTimeout(() => {
onChangeNode({
nodeId,
type: 'addOutput',
index: oldOutputIndex,
value: props.value
});
});
} else if (type === 'addOutput') {
const output = node.data.outputs.find((output) => output.key === props.value.key);
if (output) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.outputs = node.data.outputs;
} else {
if (props.index !== undefined) {
const outputs = [...node.data.outputs];
outputs.splice(props.index, 0, props.value);
updateObj.outputs = outputs;
} else {
updateObj.outputs = node.data.outputs.concat(props.value);
}
}
} else if (type === 'delOutput') {
onDelEdge({ nodeId, sourceHandle: props.key });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
},
[onDelEdge, setNodes, toast]
);
const onUpdateNodeError = useCallback(
(nodeId: string, isError: Boolean) => {
setNodes((nodes) => {
return nodes.map((item) => {
if (item.data?.nodeId === nodeId) {
item.selected = true;
//@ts-ignore
item.data.isError = isError;
}
return item;
});
});
},
[setNodes]
);
/* Run workflow debug and get next runtime data */
const [workflowDebugData, setWorkflowDebugData] = useState<{
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[];
}>();
const onNextNodeDebug = useCallback(
async (debugData = workflowDebugData) => {
if (!debugData) return;
// 1. Cancel node selected status and debugResult.showStatus
setNodes((state) =>
state.map((node) => ({
...node,
selected: false,
data: {
...node.data,
debugResult: node.data.debugResult
? {
...node.data.debugResult,
showResult: false,
isExpired: true
}
: undefined
}
}))
);
// 2. Set isEntry field and get entryNodes
const runtimeNodes = debugData.runtimeNodes.map((item) => ({
...item,
isEntry: debugData.nextRunNodes.some((node) => node.nodeId === item.nodeId)
}));
const entryNodes = runtimeNodes.filter((item) => item.isEntry);
const runtimeNodeStatus: Record<string, string> = entryNodes
.map((node) => {
const status = checkNodeRunStatus({
node,
runtimeEdges: debugData?.runtimeEdges || []
});
return {
nodeId: node.nodeId,
status
};
})
.reduce(
(acc, cur) => ({
...acc,
[cur.nodeId]: cur.status
}),
{}
);
// 3. Set entry node status to running
entryNodes.forEach((node) => {
if (runtimeNodeStatus[node.nodeId] !== 'wait') {
console.log(node.name);
onChangeNode({
nodeId: node.nodeId,
type: 'attr',
key: 'debugResult',
value: defaultRunningStatus
});
}
});
try {
// 4. Run one step
const { finishedEdges, finishedNodes, nextStepRunNodes, flowResponses } =
await postWorkflowDebug({
nodes: runtimeNodes,
edges: debugData.runtimeEdges,
variables: {},
appId,
pluginId
});
// console.log({ finishedEdges, finishedNodes, nextStepRunNodes, flowResponses });
// 5. Store debug result
const newStoreDebugData = {
runtimeNodes: finishedNodes,
// edges need to save status
runtimeEdges: finishedEdges.map((edge) => {
const oldEdge = debugData.runtimeEdges.find(
(item) => item.source === edge.source && item.target === edge.target
);
const status =
oldEdge?.status && oldEdge.status !== RuntimeEdgeStatusEnum.waiting
? oldEdge.status
: edge.status;
return {
...edge,
status
};
}),
nextRunNodes: nextStepRunNodes
};
setWorkflowDebugData(newStoreDebugData);
// 6. selected entry node and Update entry node debug result
setNodes((state) =>
state.map((node) => {
const isEntryNode = entryNodes.some((item) => item.nodeId === node.data.nodeId);
if (!isEntryNode || runtimeNodeStatus[node.data.nodeId] === 'wait') return node;
const result = flowResponses.find((item) => item.nodeId === node.data.nodeId);
if (runtimeNodeStatus[node.data.nodeId] === 'skip') {
return {
...node,
selected: isEntryNode,
data: {
...node.data,
debugResult: {
status: 'skipped',
showResult: true,
isExpired: false
}
}
};
}
return {
...node,
selected: isEntryNode,
data: {
...node.data,
debugResult: {
status: 'success',
response: result,
showResult: true,
isExpired: false
}
}
};
})
);
// Check for an empty response
if (flowResponses.length === 0 && nextStepRunNodes.length > 0) {
onNextNodeDebug(newStoreDebugData);
}
} catch (error) {
entryNodes.forEach((node) => {
onChangeNode({
nodeId: node.nodeId,
type: 'attr',
key: 'debugResult',
value: {
status: 'failed',
message: getErrText(error, 'Debug failed'),
showResult: true
}
});
});
console.log(error);
}
},
[appId, onChangeNode, pluginId, setNodes, workflowDebugData]
);
const onStopNodeDebug = useCallback(() => {
setWorkflowDebugData(undefined);
setNodes((state) =>
state.map((node) => ({
...node,
selected: false,
data: {
...node.data,
debugResult: undefined
}
}))
);
}, [setNodes]);
const onStartNodeDebug = useCallback(
async ({
entryNodeId,
runtimeNodes,
runtimeEdges
}: {
entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}) => {
const data = {
runtimeNodes,
runtimeEdges,
nextRunNodes: runtimeNodes.filter((node) => node.nodeId === entryNodeId)
};
onStopNodeDebug();
setWorkflowDebugData(data);
onNextNodeDebug(data);
},
[onNextNodeDebug, onStopNodeDebug]
);
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = useCallback(
(inputs: FlowNodeInputItemType[], nodeId: string) => {
const isTool = !!edges.find(
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
);
return {
isTool,
toolInputs: inputs.filter((item) => isTool && item.toolDescription),
commonInputs: inputs.filter((item) => {
if (!isTool) return true;
return !item.toolDescription;
})
};
},
[edges]
);
const initData = useCallback(
(e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })));
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })));
setTimeout(() => {
onFixView();
}, 100);
},
[setEdges, setNodes, onFixView]
);
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [nodes]);
const value = {
reactFlowWrapper,
mode,
filterAppIds,
edges,
setEdges,
onEdgesChange,
// nodes
nodes,
nodeList,
setNodes,
onNodesChange,
hoverNodeId,
setHoverNodeId,
onUpdateNodeError,
workflowDebugData,
onNextNodeDebug,
onStartNodeDebug,
onStopNodeDebug,
basicNodeTemplates,
// connect
connectingEdge,
setConnectingEdge,
onFixView,
onChangeNode,
onResetNode,
onDelEdge,
initData,
splitToolInputs,
hasToolNode
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
export default React.memo(FlowProvider);
type GetWorkflowStoreResponse = {
nodes: Node<FlowNodeItemType>[];
};
export const getWorkflowStore = () =>
new Promise<GetWorkflowStoreResponse>((resolve) => {
eventBus.on(EventNameEnum.receiveWorkflowStore, (data: GetWorkflowStoreResponse) => {
resolve(data);
eventBus.off(EventNameEnum.receiveWorkflowStore);
});
eventBus.emit(EventNameEnum.requestWorkflowStore);
});

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