Feat: Workflow loop node;feat: support openai o1;perf: query extension prompt;fix: intro was not delivered when the datase was created (#2719)

* feat: loop node (#2675)

* loop node frontend

* loop-node

* fix-code

* fix version

* fix

* fix

* fix

* perf: loop array code

* perf: get histories error tip

* feat: support openai o1

* perf: query extension prompt

* feat: 4811 doc

* remove log

* fix: loop node zindex & variable picker type (#2710)

* perf: performance

* perf: workflow performance

* remove uninvalid code

* perf:code

* fix: invoice table refresh

* perf: loop node data type

* fix: loop node store assistants

* perf: target connection

* feat: loop node support help line

* perf: add default icon

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer 2024-09-15 22:41:05 +08:00 committed by GitHub
parent 1ebc95a282
commit 2bdda4638d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 2001 additions and 718 deletions

View File

@ -11,17 +11,84 @@ weight: 813
### 1. 做好数据备份
### 2. 修改配置文件
如需增加 openai o1 模型,可添加如下配置:
```json
{
"model": "o1-mini",
"name": "o1-mini",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 120000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": false,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {
"temperature": 1,
"max_tokens": null,
"stream": false
}
},
{
"model": "o1-preview",
"name": "o1-preview",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 120000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": false,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {
"temperature": 1,
"max_tokens": null,
"stream": false
}
}
```
-------
### 3. 修改镜像 tag 并重启
## V4.8.11 更新说明
1.
2. 新增 - 聊天记录滚动加载,不再只加载 30 条。
3. 新增 - 工作流增加触摸板优先模式。
4. 新增 - 沙盒增加字符串转 base64 全局方法。
5. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。
5. 优化 - 工作流 handler 性能优化。
6. 优化 - 工作流快捷键,避免调试测试时也会触发。
7. 优化 - 流输出,切换 tab 时仍可以继续输出。
8. 修复 - 知识库选择权限问题。
9. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。
5. 新增 - 支持 openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature`、`max_tokens` 和 `stream`配置o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看。
6. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。
7. 优化 - 工作流 handler 性能优化。
8. 优化 - 工作流快捷键,避免调试测试时也会触发。
9. 优化 - 流输出,切换 tab 时仍可以继续输出。
10. 修复 - 知识库选择权限问题。
11. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。
12. 修复 - createDataset 接口intro 为赋值。

View File

@ -344,7 +344,7 @@ export const runtimePrompt2ChatsValue = (
return value;
};
export const getSystemPrompt = (prompt?: string): ChatItemType[] => {
export const getSystemPrompt_ChatItemType = (prompt?: string): ChatItemType[] => {
if (!prompt) return [];
return [
{

View File

@ -24,6 +24,7 @@ export enum WorkflowIOValueTypeEnum {
arrayNumber = 'arrayNumber',
arrayBoolean = 'arrayBoolean',
arrayObject = 'arrayObject',
arrayAny = 'arrayAny',
any = 'any',
chatHistory = 'chatHistory',
@ -135,7 +136,17 @@ export enum NodeInputKeyEnum {
fileUrlList = 'fileUrlList',
// user select
userSelectOptions = 'userSelectOptions'
userSelectOptions = 'userSelectOptions',
// loop
loopInputArray = 'loopInputArray',
childrenNodeIdList = 'childrenNodeIdList',
nodeWidth = 'nodeWidth',
nodeHeight = 'nodeHeight',
// loop start
loopStartInput = 'loopStartInput',
// loop end
loopEndInput = 'loopEndInput'
}
export enum NodeOutputKeyEnum {
@ -178,7 +189,13 @@ export enum NodeOutputKeyEnum {
ifElseResult = 'ifElseResult',
//user select
selectResult = 'selectResult'
selectResult = 'selectResult',
// loop
loopArray = 'loopArray',
// loop start
loopStartInput = 'loopStartInput'
}
export enum VariableInputEnum {

View File

@ -125,7 +125,10 @@ export enum FlowNodeTypeEnum {
textEditor = 'textEditor',
customFeedback = 'customFeedback',
readFiles = 'readFiles',
userSelect = 'userSelect'
userSelect = 'userSelect',
loop = 'loop',
loopStart = 'loopStart',
loopEnd = 'loopEnd'
}
// node IO value type
@ -162,6 +165,10 @@ export const FlowValueTypeMap = {
label: 'array<object>',
value: WorkflowIOValueTypeEnum.arrayObject
},
[WorkflowIOValueTypeEnum.arrayAny]: {
label: 'array',
value: WorkflowIOValueTypeEnum.arrayAny
},
[WorkflowIOValueTypeEnum.any]: {
label: 'any',
value: WorkflowIOValueTypeEnum.any

View File

@ -172,6 +172,15 @@ export type DispatchNodeResponseType = {
// update var
updateVarResult?: any[];
// loop
loopResult?: any[];
loopInput?: any[];
loopDetail?: ChatHistoryItemResType[];
// loop start
loopInputValue?: any;
// loop end
loopOutputValue?: any;
};
export type DispatchNodeResultType<T = {}> = {

View File

@ -29,6 +29,9 @@ import { TextEditorNode } from './system/textEditor';
import { CustomFeedbackNode } from './system/customFeedback';
import { ReadFilesNodes } from './system/readFiles';
import { UserSelectNode } from './system/userSelect/index';
import { LoopNode } from './system/loop/loop';
import { LoopStartNode } from './system/loop/loopStart';
import { LoopEndNode } from './system/loop/loopEnd';
const systemNodes: FlowNodeTemplateType[] = [
AiChatModule,
@ -46,7 +49,8 @@ const systemNodes: FlowNodeTemplateType[] = [
LafModule,
IfElseNode,
VariableUpdateNode,
CodeNode
CodeNode,
LoopNode
];
/* app flow module templates */
export const appSystemModuleTemplates: FlowNodeTemplateType[] = [
@ -74,5 +78,7 @@ export const moduleTemplatesFlat: FlowNodeTemplateType[] = [
EmptyNode,
RunPluginModule,
RunAppNode,
RunAppModule
RunAppModule,
LoopStartNode,
LoopEndNode
];

View File

@ -83,3 +83,25 @@ export const Input_Template_File_Link: FlowNodeInputItemType = {
description: i18nT('app:workflow.user_file_input_desc'),
valueType: WorkflowIOValueTypeEnum.arrayString
};
export const Input_Template_Children_Node_List: FlowNodeInputItemType = {
key: NodeInputKeyEnum.childrenNodeIdList,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.arrayString,
label: '',
value: []
};
export const Input_Template_Node_Width: FlowNodeInputItemType = {
key: NodeInputKeyEnum.nodeWidth,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.number,
label: '',
value: 900
};
export const Input_Template_Node_Height: FlowNodeInputItemType = {
key: NodeInputKeyEnum.nodeHeight,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.number,
label: '',
value: 900
};

View File

@ -0,0 +1,54 @@
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '../../../node/constant';
import { FlowNodeTemplateType } from '../../../type/node';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../../constants';
import { getHandleConfig } from '../../utils';
import { i18nT } from '../../../../../../web/i18n/utils';
import {
Input_Template_Children_Node_List,
Input_Template_Node_Height,
Input_Template_Node_Width
} from '../../input';
export const LoopNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.loop,
templateType: FlowNodeTemplateTypeEnum.tools,
flowNodeType: FlowNodeTypeEnum.loop,
sourceHandle: getHandleConfig(true, true, true, true),
targetHandle: getHandleConfig(true, true, true, true),
avatar: 'core/workflow/template/loop',
name: i18nT('workflow:loop'),
intro: i18nT('workflow:intro_loop'),
showStatus: true,
version: '4811',
inputs: [
{
key: NodeInputKeyEnum.loopInputArray,
renderTypeList: [FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.arrayAny,
required: true,
label: i18nT('workflow:loop_input_array'),
value: []
},
Input_Template_Children_Node_List,
Input_Template_Node_Width,
Input_Template_Node_Height
],
outputs: [
{
id: NodeOutputKeyEnum.loopArray,
key: NodeOutputKeyEnum.loopArray,
label: i18nT('workflow:loop_result'),
type: FlowNodeOutputTypeEnum.static,
valueType: WorkflowIOValueTypeEnum.arrayAny
}
]
};

View File

@ -0,0 +1,34 @@
import { i18nT } from '../../../../../../web/i18n/utils';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../../constants';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant';
import { FlowNodeTemplateType } from '../../../type/node';
import { getHandleConfig } from '../../utils';
export const LoopEndNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.loopEnd,
templateType: FlowNodeTemplateTypeEnum.systemInput,
flowNodeType: FlowNodeTypeEnum.loopEnd,
sourceHandle: getHandleConfig(false, false, false, false),
targetHandle: getHandleConfig(false, false, false, true),
unique: true,
forbidDelete: true,
avatar: 'core/workflow/template/loopEnd',
name: i18nT('workflow:loop_end'),
showStatus: false,
version: '4811',
inputs: [
{
key: NodeInputKeyEnum.loopEndInput,
renderTypeList: [FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.any,
label: '',
required: true,
value: []
}
],
outputs: []
};

View File

@ -0,0 +1,34 @@
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant';
import { FlowNodeTemplateType } from '../../../type/node.d';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../../constants';
import { getHandleConfig } from '../../utils';
import { i18nT } from '../../../../../../web/i18n/utils';
export const LoopStartNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.loopStart,
templateType: FlowNodeTemplateTypeEnum.systemInput,
flowNodeType: FlowNodeTypeEnum.loopStart,
sourceHandle: getHandleConfig(false, true, false, false),
targetHandle: getHandleConfig(false, false, false, false),
avatar: 'core/workflow/template/loopStart',
name: i18nT('workflow:loop_start'),
unique: true,
forbidDelete: true,
showStatus: false,
version: '4811',
inputs: [
{
key: NodeInputKeyEnum.loopStartInput,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
label: '',
required: true,
value: ''
}
],
outputs: []
};

View File

@ -95,6 +95,7 @@ export type NodeTemplateListType = {
// react flow node type
export type FlowNodeItemType = FlowNodeTemplateType & {
nodeId: string;
parentNodeId?: string;
isError?: boolean;
debugResult?: {
status: 'running' | 'success' | 'skipped' | 'failed';

View File

@ -11,7 +11,7 @@
"jschardet": "3.1.1",
"nanoid": "^4.0.1",
"next": "14.2.5",
"openai": "4.57.0",
"openai": "4.61.0",
"openapi-types": "^12.1.3",
"timezones-list": "^3.0.2"
},

View File

@ -4,13 +4,17 @@ import { ChatItemType } from '@fastgpt/global/core/chat/type';
import { countGptMessagesTokens } from '../../../common/string/tiktoken/index';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { getLLMModel } from '../model';
/*
query extension -
*/
const defaultPrompt = `作为一个向量检索助手,你的任务是结合历史记录,从不同角度,为“原问题”生成个不同版本的“检索词”,从而提高向量检索的语义丰富度,提高向量检索的精度。生成的问题要求指向对象清晰明确,并与“原问题语言相同”。
const title = global.feConfigs?.systemTitle || 'FastAI';
const defaultPrompt = `作为一个向量检索助手,你的任务是结合历史记录,从不同角度,为“原问题”生成个不同版本的“检索词”,从而提高向量检索的语义丰富度,提高向量检索的精度。
<Example></Example>
<Example>
@ -49,49 +53,50 @@ A: 护产假的天数根据员工所在的城市而定。请提供您所在的
:
"""
Q: 作者是谁
A: FastGPT labring
A: ${title} labring
"""
原问题: Tell me about him
: ["Introduce labring, the author of FastGPT." ," Background information on author labring." "," Why does labring do FastGPT?"]
: ["Introduce labring, the author of ${title}." ," Background information on author labring." "," Why does labring do ${title}?"]
----------------
:
"""
Q: 对话背景
A: 关于 FatGPT 使
A: 关于 ${title} 使
"""
原问题: 你好
: ["你好"]
----------------
:
"""
Q: FastGPT
A: FastGPT
Q: ${title}
A: ${title}
"""
原问题: 你知道 laf
: ["laf 的官网地址是多少?","laf 的使用教程。","laf 有什么特点和优势。"]
----------------
:
"""
Q: FastGPT
Q: ${title}
A: 1.
2. 便
3.
"""
原问题: 介绍下第2点
: ["介绍下 FastGPT 简便的优势", "从哪些方面,可以体现出 FastGPT 的简便"]
: ["介绍下 ${title} 简便的优势", "从哪些方面,可以体现出 ${title} 的简便"]
----------------
:
"""
Q: 什么是 FastGPT
A: FastGPT RAG
Q: 什么是 ${title}
A: ${title} RAG
Q: 什么是 Laf
A: Laf
"""
原问题: 它们有什么关系
: ["FastGPT和Laf有什么关系","介绍下FastGPT","介绍下Laf"]
: ["${title}和Laf有什么关系","介绍下${title}","介绍下Laf"]
</Example>
----------------
-----
:
@ -130,6 +135,8 @@ A: ${chatBg}
.join('\n');
const concatFewShot = `${systemFewShot}${historyFewShot}`.trim();
const modelData = getLLMModel(model);
const ai = getAIApi({
timeout: 480000
});
@ -144,11 +151,12 @@ A: ${chatBg}
}
] as ChatCompletionMessageParam[];
const result = await ai.chat.completions.create({
model: model,
model: modelData.model,
temperature: 0.01,
// @ts-ignore
messages,
stream: false
stream: false,
...modelData.defaultConfig
});
let answer = result.choices?.[0]?.message?.content || '';
@ -161,6 +169,8 @@ A: ${chatBg}
};
}
// Intercept the content of [] and retain []
answer = answer.match(/\[.*?\]/)?.[0] || '';
answer = answer.replace(/\\"/g, '"');
try {

View File

@ -93,6 +93,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
const { text } = chatValue2RuntimePrompt(assistantResponses);
return {
assistantResponses,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
moduleLogo: appData.avatar,
query: userChatInput,

View File

@ -128,7 +128,8 @@ const completions = async ({
model: cqModel.model,
temperature: 0.01,
messages: requestMessages,
stream: false
stream: false,
...cqModel.defaultConfig
});
const answer = data.choices?.[0].message?.content || '';

View File

@ -355,7 +355,8 @@ Human: ${content}`
model: extractModel.model,
temperature: 0.01,
messages: requestMessages,
stream: false
stream: false,
...extractModel.defaultConfig
});
const answer = data.choices?.[0].message?.content || '';

View File

@ -14,7 +14,7 @@ import {
GPTMessages2Chats,
chatValue2RuntimePrompt,
chats2GPTMessages,
getSystemPrompt,
getSystemPrompt_ChatItemType,
runtimePrompt2ChatsValue
} from '@fastgpt/global/core/chat/adapt';
import { formatModelChars2Points } from '../../../../../support/wallet/usage/utils';
@ -95,7 +95,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
});
const messages: ChatItemType[] = [
...getSystemPrompt(systemPrompt),
...getSystemPrompt_ChatItemType(toolModel.defaultSystemChatPrompt),
...getSystemPrompt_ChatItemType(systemPrompt),
// Add file input prompt to histories
...chatHistories.map((item) => {
if (item.obj === ChatRoleEnum.Human) {

View File

@ -114,15 +114,16 @@ export const runToolWithPromptCall = async (
})
]);
const requestBody = {
...toolModel?.defaultConfig,
model: toolModel.model,
temperature: computedTemperature({
model: toolModel,
temperature
}),
max_completion_tokens: max_tokens,
max_tokens,
stream,
messages: requestMessages
messages: requestMessages,
...toolModel?.defaultConfig
};
// console.log(JSON.stringify(requestBody, null, 2));
@ -135,9 +136,13 @@ export const runToolWithPromptCall = async (
Accept: 'application/json, text/plain, */*'
}
});
const isStreamResponse =
typeof aiResponse === 'object' &&
aiResponse !== null &&
('iterator' in aiResponse || 'controller' in aiResponse);
const answer = await (async () => {
if (res && stream) {
if (res && isStreamResponse) {
const { answer } = await streamResponse({
res,
toolNodes,
@ -164,6 +169,17 @@ export const runToolWithPromptCall = async (
})
});
}
// 不支持 stream 模式的模型的流失响应
if (stream && !isStreamResponse) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
text: replaceAnswer
})
});
}
// No tool is invoked, indicating that the process is over
const gptAssistantResponse: ChatCompletionAssistantMessageParam = {
role: ChatCompletionRequestMessageRoleEnum.Assistant,

View File

@ -128,17 +128,18 @@ export const runToolWithToolChoice = async (
})
]);
const requestBody: any = {
...toolModel?.defaultConfig,
model: toolModel.model,
temperature: computedTemperature({
model: toolModel,
temperature
}),
max_completion_tokens: max_tokens,
max_tokens,
stream,
messages: requestMessages,
tools,
tool_choice: 'auto'
tool_choice: 'auto',
...toolModel?.defaultConfig
};
// console.log(JSON.stringify(requestBody, null, 2));
@ -153,9 +154,13 @@ export const runToolWithToolChoice = async (
Accept: 'application/json, text/plain, */*'
}
});
const isStreamResponse =
typeof aiResponse === 'object' &&
aiResponse !== null &&
('iterator' in aiResponse || 'controller' in aiResponse);
const { answer, toolCalls } = await (async () => {
if (res && stream) {
if (res && isStreamResponse) {
return streamResponse({
res,
workflowStreamResponse,
@ -165,6 +170,7 @@ export const runToolWithToolChoice = async (
} else {
const result = aiResponse as ChatCompletion;
const calls = result.choices?.[0]?.message?.tool_calls || [];
const answer = result.choices?.[0]?.message?.content || '';
// 加上name和avatar
const toolCalls = calls.map((tool) => {
@ -176,8 +182,33 @@ export const runToolWithToolChoice = async (
};
});
// 不支持 stream 模式的模型的流失响应
toolCalls.forEach((tool) => {
workflowStreamResponse?.({
event: SseResponseEventEnum.toolCall,
data: {
tool: {
id: tool.id,
toolName: tool.toolName,
toolAvatar: tool.toolAvatar,
functionName: tool.function.name,
params: tool.function?.arguments ?? '',
response: ''
}
}
});
});
if (answer) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
text: answer
})
});
}
return {
answer: result.choices?.[0]?.message?.content || '',
answer,
toolCalls: toolCalls
};
}
@ -239,7 +270,7 @@ export const runToolWithToolChoice = async (
toolName: '',
toolAvatar: '',
params: '',
response: sliceStrStartEnd(stringToolResponse, 500, 500)
response: sliceStrStartEnd(stringToolResponse, 2000, 2000)
}
}
});

View File

@ -19,7 +19,7 @@ import { countMessagesTokens } from '../../../../common/string/tiktoken/index';
import {
chats2GPTMessages,
chatValue2RuntimePrompt,
getSystemPrompt,
getSystemPrompt_ChatItemType,
GPTMessages2Chats,
runtimePrompt2ChatsValue
} from '@fastgpt/global/core/chat/adapt';
@ -153,15 +153,16 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
]);
const requestBody = {
...modelConstantsData?.defaultConfig,
model: modelConstantsData.model,
temperature: computedTemperature({
model: modelConstantsData,
temperature
}),
max_completion_tokens: max_tokens,
max_tokens,
stream,
messages: requestMessages
messages: requestMessages,
...modelConstantsData?.defaultConfig
};
// console.log(JSON.stringify(requestBody, null, 2), '===');
try {
@ -175,8 +176,13 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
}
});
const isStreamResponse =
typeof response === 'object' &&
response !== null &&
('iterator' in response || 'controller' in response);
const { answerText } = await (async () => {
if (res && stream) {
if (res && isStreamResponse) {
// sse response
const { answer } = await streamResponse({
res,
@ -195,6 +201,14 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
const unStreamResponse = response as ChatCompletion;
const answer = unStreamResponse.choices?.[0]?.message?.content || '';
// Some models do not support streaming
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
text: answer
})
});
return {
answerText: answer
};
@ -310,9 +324,9 @@ async function getChatMessages({
: userChatInput;
const messages: ChatItemType[] = [
...getSystemPrompt(systemPrompt),
...getSystemPrompt_ChatItemType(systemPrompt),
...(stringQuoteText
? getSystemPrompt(
? getSystemPrompt_ChatItemType(
replaceVariable(Prompt_DocumentQuote, {
quote: stringQuoteText
})

View File

@ -66,6 +66,9 @@ import {
UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/userSelect/type';
import { dispatchRunAppNode } from './plugin/runApp';
import { dispatchLoop } from './loop/runLoop';
import { dispatchLoopEnd } from './loop/runLoopEnd';
import { dispatchLoopStart } from './loop/runLoopStart';
const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
@ -91,6 +94,9 @@ const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.customFeedback]: dispatchCustomFeedback,
[FlowNodeTypeEnum.readFiles]: dispatchReadFiles,
[FlowNodeTypeEnum.userSelect]: dispatchUserSelect,
[FlowNodeTypeEnum.loop]: dispatchLoop,
[FlowNodeTypeEnum.loopStart]: dispatchLoopStart,
[FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd,
// none
[FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig,
@ -160,7 +166,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
let chatResponses: ChatHistoryItemResType[] = []; // response request and save to database
let chatAssistantResponse: AIChatItemValueItemType[] = []; // The value will be returned to the user
let chatNodeUsages: ChatNodeUsageType[] = [];
let toolRunResponse: ToolRunResponseItemType;
let toolRunResponse: ToolRunResponseItemType; // Run with tool mode. Result will response to tool node.
let debugNextStepRunNodes: RuntimeNodeItemType[] = [];
// 记录交互节点,交互节点需要在工作流完全结束后再进行计算
let workflowInteractiveResponse:
@ -196,9 +202,11 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
if (responseData) {
chatResponses.push(responseData);
}
if (nodeDispatchUsages) {
chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages);
}
if (toolResponses !== undefined) {
if (Array.isArray(toolResponses) && toolResponses.length === 0) return;
if (typeof toolResponses === 'object' && Object.keys(toolResponses).length === 0) {
@ -206,6 +214,8 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}
toolRunResponse = toolResponses;
}
// Histories store
if (assistantResponses) {
chatAssistantResponse = chatAssistantResponse.concat(assistantResponses);
} else if (answerText) {

View File

@ -0,0 +1,93 @@
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
DispatchNodeResultType,
ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
import { dispatchWorkFlow } from '..';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { AIChatItemValueItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopInputArray]: Array<any>;
[NodeInputKeyEnum.childrenNodeIdList]: string[];
}>;
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.loopArray]: Array<any>;
}>;
export const dispatchLoop = async (props: Props): Promise<Response> => {
const {
params,
runtimeNodes,
user,
node: { name }
} = props;
const { loopInputArray = [], childrenNodeIdList } = params;
if (!Array.isArray(loopInputArray)) {
return Promise.reject('Input value is not an array');
}
if (loopInputArray.length > 50) {
return Promise.reject('Input array length cannot be greater than 50');
}
const runNodes = runtimeNodes.filter((node) => childrenNodeIdList.includes(node.nodeId));
const outputValueArr = [];
const loopDetail: ChatHistoryItemResType[] = [];
let assistantResponses: AIChatItemValueItemType[] = [];
let totalPoints = 0;
for await (const item of loopInputArray) {
const response = await dispatchWorkFlow({
...props,
runtimeNodes: runNodes.map((node) =>
node.flowNodeType === FlowNodeTypeEnum.loopStart
? {
...node,
isEntry: true,
inputs: node.inputs.map((input) =>
input.key === NodeInputKeyEnum.loopStartInput
? {
...input,
value: item
}
: input
)
}
: {
...node,
isEntry: false
}
)
});
const loopOutputValue = response.flowResponses.find(
(res) => res.moduleType === FlowNodeTypeEnum.loopEnd
)?.loopOutputValue;
outputValueArr.push(loopOutputValue);
loopDetail.push(...response.flowResponses);
assistantResponses.push(...response.assistantResponses);
totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
}
return {
[DispatchNodeResponseKeyEnum.assistantResponses]: assistantResponses,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: totalPoints,
loopInput: loopInputArray,
loopResult: outputValueArr,
loopDetail: loopDetail
},
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
{
totalPoints: user.openaiAccount?.key ? 0 : totalPoints,
moduleName: name
}
],
[NodeOutputKeyEnum.loopArray]: outputValueArr
};
};

View File

@ -0,0 +1,21 @@
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
DispatchNodeResultType,
ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopEndInput]: any;
}>;
type Response = DispatchNodeResultType<{}>;
export const dispatchLoopEnd = async (props: Props): Promise<Response> => {
const { params } = props;
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
loopOutputValue: params.loopEndInput
}
};
};

View File

@ -0,0 +1,23 @@
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
DispatchNodeResultType,
ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.loopStartInput]: any;
}>;
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.loopStartInput]: any;
}>;
export const dispatchLoopStart = async (props: Props): Promise<Response> => {
const { params } = props;
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
loopInputValue: params.loopStartInput
},
[NodeOutputKeyEnum.loopStartInput]: params.loopStartInput
};
};

View File

@ -107,6 +107,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
const { text } = chatValue2RuntimePrompt(assistantResponses);
return {
assistantResponses,
[DispatchNodeResponseKeyEnum.runTimes]: runTimes,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
moduleLogo: appData.avatar,

View File

@ -20,7 +20,7 @@ export const dispatchAnswer = (props: Record<string, any>): AnswerResponse => {
} = props as AnswerProps;
const formatText = typeof text === 'string' ? text : JSON.stringify(text, null, 2);
const responseText = `\n${formatText}`;
const responseText = `\n${formatText}`.replaceAll('\\n', '\n');
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,

View File

@ -5,13 +5,14 @@ import { useTranslation } from 'next-i18next';
type Props = FlexProps & {
text?: string | React.ReactNode;
iconSize?: string | number;
};
const EmptyTip = ({ text, ...props }: Props) => {
const EmptyTip = ({ text, iconSize = '48px', ...props }: Props) => {
const { t } = useTranslation();
return (
<Flex mt={5} flexDirection={'column'} alignItems={'center'} py={'10vh'} {...props}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<MyIcon name="empty" w={iconSize} h={iconSize} color={'transparent'} />
<Box mt={2} color={'myGray.500'} fontSize={'sm'}>
{text || t('common:common.empty.Common Tip')}
</Box>

View File

@ -177,6 +177,7 @@ export const iconPaths = {
'core/workflow/debugResult': () => import('./icons/core/workflow/debugResult.svg'),
'core/workflow/edgeArrow': () => import('./icons/core/workflow/edgeArrow.svg'),
'core/workflow/grout': () => import('./icons/core/workflow/grout.svg'),
'core/workflow/inputType/array': () => import('./icons/core/workflow/inputType/array.svg'),
'core/workflow/inputType/customVariable': () =>
import('./icons/core/workflow/inputType/customVariable.svg'),
'core/workflow/inputType/dynamic': () => import('./icons/core/workflow/inputType/dynamic.svg'),
@ -226,6 +227,9 @@ export const iconPaths = {
'core/workflow/template/ifelse': () => import('./icons/core/workflow/template/ifelse.svg'),
'core/workflow/template/lafDispatch': () =>
import('./icons/core/workflow/template/lafDispatch.svg'),
'core/workflow/template/loop': () => import('./icons/core/workflow/template/loop.svg'),
'core/workflow/template/loopEnd': () => import('./icons/core/workflow/template/loopEnd.svg'),
'core/workflow/template/loopStart': () => import('./icons/core/workflow/template/loopStart.svg'),
'core/workflow/template/mathCall': () => import('./icons/core/workflow/template/mathCall.svg'),
'core/workflow/template/pluginOutput': () =>
import('./icons/core/workflow/template/pluginOutput.svg'),

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 11" fill="none">
<path d="M0.331787 1.04757C0.331787 0.725401 0.592954 0.464233 0.91512 0.464233H2.3005C2.62267 0.464233 2.88383 0.725401 2.88383 1.04757V1.44423C2.88383 1.7664 2.62267 2.02757 2.3005 2.02757H1.89513L1.89512 9.36302H2.3005C2.62267 9.36302 2.88383 9.62419 2.88383 9.94635V10.343C2.88383 10.6652 2.62267 10.9264 2.3005 10.9264H0.91512C0.592954 10.9264 0.331787 10.6652 0.331787 10.343V9.94635L0.331791 9.94412L0.331792 1.44656L0.331787 1.44423V1.04757Z" fill="#3370FF"/>
<path d="M12.6676 1.04757C12.6676 0.725401 12.4065 0.464233 12.0843 0.464233H10.6989C10.3768 0.464233 10.1156 0.725401 10.1156 1.04757V1.44423C10.1156 1.7664 10.3768 2.02757 10.6989 2.02757H11.1043V9.36302H10.6989C10.3768 9.36302 10.1156 9.62419 10.1156 9.94635V10.343C10.1156 10.6652 10.3768 10.9264 10.6989 10.9264H12.0843C12.4065 10.9264 12.6676 10.6652 12.6676 10.343V9.94635L12.6676 9.94412V1.44656L12.6676 1.44423V1.04757Z" fill="#3370FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.34759 8.72133C4.60023 8.72133 4.82416 8.5587 4.90231 8.31846L5.27478 7.17351H7.72561L8.09725 8.31814C8.17531 8.55855 8.39931 8.72133 8.65207 8.72133H8.94793C9.3486 8.72133 9.63 8.32668 9.49942 7.94789L7.56163 2.32667C7.48052 2.09137 7.25904 1.93345 7.01015 1.93345H5.99239C5.74359 1.93345 5.52217 2.09125 5.44099 2.32643L3.50046 7.94765C3.36968 8.32648 3.65109 8.72133 4.05186 8.72133H4.34759ZM7.36188 6.05324L6.53607 3.50982C6.53119 3.49477 6.51717 3.48459 6.50136 3.48459C6.48555 3.48459 6.47154 3.49476 6.46665 3.5098L5.63922 6.05324H7.36188Z" fill="#3370FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,11 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_10814_19474)"/>
<path d="M12.9578 11.1092C12.9578 9.34187 14.3905 7.90918 16.1578 7.90918H24.9728C26.7401 7.90918 28.1728 9.34187 28.1728 11.1092V19.3092C28.1728 21.0765 26.7401 22.5092 24.9728 22.5092H24.9678V14.3092C24.9678 12.5419 23.5351 11.1092 21.7678 11.1092L12.9578 11.1092Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.82697 16.2425C7.82697 14.4752 9.25965 13.0425 11.027 13.0425H19.842C21.6093 13.0425 23.042 14.4752 23.042 16.2425V24.4425C23.042 26.2098 21.6093 27.6425 19.842 27.6425H11.027C9.25966 27.6425 7.82697 26.2098 7.82697 24.4425V16.2425ZM19.1012 19.1173C19.4917 18.7268 19.4917 18.0936 19.1012 17.7031C18.7107 17.3126 18.0775 17.3126 17.687 17.7031L14.5304 20.8597L13.1819 19.5113C12.7914 19.1208 12.1582 19.1208 11.7677 19.5113C11.3772 19.9018 11.3772 20.535 11.7677 20.9255L13.7874 22.9452C13.7987 22.9576 13.8103 22.9699 13.8224 22.9819C14.0171 23.1767 14.2722 23.2743 14.5274 23.2748C14.7846 23.2758 15.0421 23.1782 15.2384 22.9819C15.2504 22.9699 15.2621 22.9576 15.2734 22.9451L19.1012 19.1173Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_10814_19474" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#EB78FE"/>
<stop offset="1" stop-color="#C071FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,11 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_10832_16007)"/>
<path d="M13.9673 10.3122C13.1868 10.8479 13.1935 11.9491 13.863 12.6185C14.3986 13.1541 15.2613 13.1384 15.9008 12.7325C16.5477 12.3219 17.268 12.0318 18.0272 11.8808C19.2374 11.6401 20.4919 11.7636 21.6319 12.2358C22.7719 12.708 23.7463 13.5077 24.4318 14.5337C25.1174 15.5597 25.4833 16.7659 25.4833 17.9999C25.4833 19.2338 25.1174 20.4401 24.4318 21.4661C23.7463 22.4921 22.7719 23.2917 21.6319 23.7639C20.4919 24.2361 19.2374 24.3597 18.0272 24.119C17.268 23.968 16.5477 23.6779 15.9008 23.2673C15.2613 22.8614 14.3986 22.8457 13.863 23.3812C13.1935 24.0507 13.1868 25.1518 13.9673 25.6876C15.0044 26.3995 16.1799 26.8976 17.4252 27.1453C19.234 27.5051 21.1088 27.3204 22.8127 26.6147C24.5165 25.9089 25.9728 24.7138 26.9974 23.1803C28.022 21.6469 28.5689 19.8441 28.5689 17.9999C28.5689 16.1557 28.022 14.3528 26.9974 12.8194C25.9728 11.286 24.5165 10.0908 22.8127 9.38509C21.1088 8.67933 19.234 8.49468 17.4252 8.85447C16.1799 9.10217 15.0044 9.60029 13.9673 10.3122Z" fill="white"/>
<path d="M19.2443 22.5497C17.3579 22.5497 15.7397 21.4017 15.0501 19.7663H9.19726C8.22171 19.7663 7.43087 18.9754 7.43087 17.9999C7.43087 17.0243 8.22171 16.2335 9.19726 16.2335H15.0501C15.7397 14.598 17.3579 13.45 19.2443 13.45C21.7571 13.45 23.7942 15.4871 23.7942 17.9999C23.7942 20.5127 21.7571 22.5497 19.2443 22.5497Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_10832_16007" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#68C0FF"/>
<stop offset="1" stop-color="#52A2FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,11 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_10829_15860)"/>
<path d="M22.0325 10.3122C22.813 10.8479 22.8062 11.9491 22.1368 12.6185C21.6012 13.1541 20.7384 13.1384 20.099 12.7325C19.4521 12.3219 18.7317 12.0318 17.9726 11.8808C16.7623 11.6401 15.5079 11.7636 14.3679 12.2358C13.2279 12.708 12.2535 13.5077 11.5679 14.5337C10.8824 15.5597 10.5165 16.7659 10.5165 17.9999C10.5165 19.2338 10.8824 20.4401 11.5679 21.4661C12.2535 22.4921 13.2279 23.2917 14.3679 23.7639C15.5079 24.2361 16.7624 24.3597 17.9726 24.119C18.7317 23.968 19.4521 23.6779 20.099 23.2673C20.7384 22.8614 21.6012 22.8457 22.1368 23.3812C22.8062 24.0507 22.813 25.1518 22.0325 25.6876C20.9954 26.3995 19.8199 26.8976 18.5746 27.1453C16.7658 27.5051 14.8909 27.3204 13.1871 26.6147C11.4832 25.9089 10.0269 24.7138 9.00232 23.1803C7.97772 21.6469 7.43085 19.8441 7.43085 17.9999C7.43085 16.1557 7.97772 14.3528 9.00232 12.8194C10.0269 11.286 11.4832 10.0908 13.1871 9.38509C14.8909 8.67933 16.7658 8.49468 18.5746 8.85447C19.8199 9.10217 20.9954 9.60029 22.0325 10.3122Z" fill="white"/>
<path d="M16.7554 22.5497C18.6418 22.5497 20.2601 21.4017 20.9497 19.7663H26.8025C27.778 19.7663 28.5689 18.9754 28.5689 17.9999C28.5689 17.0243 27.778 16.2335 26.8025 16.2335H20.9497C20.2601 14.598 18.6418 13.45 16.7554 13.45C14.2426 13.45 12.2056 15.4871 12.2056 17.9999C12.2056 20.5127 14.2426 22.5497 16.7554 22.5497Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_10829_15860" x1="18" y1="0" x2="5.5" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#68C0FF"/>
<stop offset="1" stop-color="#52A2FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -13,6 +13,7 @@ const MultipleRowSelect = ({
emptyTip,
maxH = 300,
onSelect,
popDirection = 'bottom',
styles
}: MultipleSelectProps) => {
const { t } = useTranslation();
@ -124,7 +125,13 @@ const MultipleRowSelect = ({
{isOpen && (
<Box
position={'absolute'}
top={'45px'}
{...(popDirection === 'top'
? {
bottom: '45px'
}
: {
top: '45px'
})}
py={2}
bg={'white'}
border={'1px solid #fff'}

View File

@ -13,4 +13,5 @@ export type MultipleSelectProps<T = any> = {
maxH?: number;
onSelect: (val: any[]) => void;
styles?: ButtonProps;
popDirection?: 'top' | 'bottom';
};

View File

@ -101,7 +101,7 @@ export default function VariablePickerPlugin({
<MyIcon name={(item.icon as any) || 'core/modules/variable'} w={'14px'} />
<Box ml={2} fontSize={'sm'} whiteSpace={'nowrap'}>
{item.key}
{item.key !== item.label && `(${item.label})`}
{item.key !== item.label && `(${t(item.label as any)})`}
</Box>
</Flex>
))}

View File

@ -552,7 +552,11 @@
"search using reRank": "Result Re-Rank",
"text output": "Text Output",
"update_var_result": "Variable Update Result (Displays Multiple Variable Update Results in Order)",
"user_select_result": "User Selection Result"
"user_select_result": "User Selection Result",
"loop_input": "Loop Input Array",
"loop_output": "Loop Output Array",
"loop_input_element": "Loop Input Element",
"loop_output_element": "Loop Output Element"
},
"retry": "Regenerate",
"tts": {

View File

@ -1,10 +1,12 @@
{
"Array_element": "Array element",
"Code": "Code",
"about_xxx_question": "Question regarding xxx",
"add_new_input": "Add New Input",
"append_application_reply_to_history_as_new_context": "Append the application's reply to the history as new context",
"application_call": "Application Call",
"assigned_reply": "Assigned Reply",
"can_not_loop": "This node can't loop.",
"choose_another_application_to_call": "Select another application to call",
"classification_result": "Classification Result",
"code": {
@ -88,6 +90,8 @@
"length_not_equal_to": "Length Not Equal To",
"less_than": "Less Than",
"less_than_or_equal_to": "Less Than or Equal To",
"loop": "Batch execution",
"loop_start_tip": "Not input array",
"max_dialog_rounds": "Maximum Number of Dialog Rounds",
"max_tokens": "Maximum Tokens",
"mouse_priority": "Mouse first",

View File

@ -552,7 +552,11 @@
"search using reRank": "结果重排",
"text output": "文本输出",
"update_var_result": "变量更新结果(按顺序展示多个变量更新结果)",
"user_select_result": "用户选择结果"
"user_select_result": "用户选择结果",
"loop_input": "输入数组",
"loop_output": "输出数组",
"loop_input_element": "输入数组元素",
"loop_output_element": "输出数组元素"
},
"retry": "重新生成",
"tts": {

View File

@ -1,10 +1,12 @@
{
"Array_element": "数组元素",
"Code": "代码",
"about_xxx_question": "关于 xxx 的问题",
"add_new_input": "新增输入",
"append_application_reply_to_history_as_new_context": "将该应用回复内容拼接到历史记录中,作为新的上下文返回",
"application_call": "应用调用",
"assigned_reply": "指定回复",
"can_not_loop": "该节点不支持循环嵌套",
"choose_another_application_to_call": "选择一个其他应用进行调用",
"classification_result": "分类结果",
"code": {
@ -66,6 +68,7 @@
"intro_http_request": "可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)",
"intro_knowledge_base_search_merge": "可以将多个知识库搜索结果进行合并输出。使用 RRF 的合并方式进行最终排序输出。",
"intro_laf_function_call": "可以调用Laf账号下的云函数。",
"intro_loop": "可以输入一个数组,数组内元素将独立执行循环体,并将所有结果作为数组输出。",
"intro_plugin_input": "可以配置插件需要哪些输入,利用这些输入来运行插件",
"intro_question_classification": "根据用户的历史记录和当前问题判断该次提问的类型。可以添加多组问题类型,下面是一个模板例子:\n类型1: 打招呼\n类型2: 关于商品“使用”问题\n类型3: 关于商品“购买”问题\n类型4: 其他问题",
"intro_question_optimization": "使用问题优化功能,可以提高知识库连续对话时搜索的精度。使用该功能后,会先利用 AI 根据上下文构建一个或多个新的检索词,这些检索词更利于进行知识库搜索。该模块已内置在知识库搜索模块中,如果您仅进行一次知识库搜索,可直接使用知识库内置的补全功能。",
@ -88,6 +91,13 @@
"length_not_equal_to": "长度不等于",
"less_than": "小于",
"less_than_or_equal_to": "小于等于",
"loop": "批量执行(测试)",
"loop_body": "循环体",
"loop_end": "循环体结束",
"loop_input_array": "数组",
"loop_result": "数组执行结果",
"loop_start": "循环体开始",
"loop_start_tip": "未输入数组",
"max_dialog_rounds": "最多携带多少轮对话记录",
"max_tokens": "最大 Tokens",
"mouse_priority": "鼠标优先",

View File

@ -22,7 +22,7 @@ importers:
version: 13.3.0
next-i18next:
specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
prettier:
specifier: 3.2.4
version: 3.2.4
@ -63,8 +63,8 @@ importers:
specifier: 14.2.5
version: 14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
openai:
specifier: 4.57.0
version: 4.57.0(encoding@0.1.13)
specifier: 4.61.0
version: 4.61.0(encoding@0.1.13)
openapi-types:
specifier: ^12.1.3
version: 12.1.3
@ -250,7 +250,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 2.1.5
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
'@chakra-ui/react':
specifier: 2.8.1
version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -313,7 +313,7 @@ importers:
version: 4.17.21
next-i18next:
specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
papaparse:
specifier: ^5.4.1
version: 5.4.1
@ -374,7 +374,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 2.1.5
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
'@chakra-ui/react':
specifier: 2.8.1
version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -470,7 +470,7 @@ importers:
version: 14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
next-i18next:
specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
nextjs-node-loader:
specifier: ^1.1.5
version: 1.1.5(webpack@5.92.1)
@ -6992,8 +6992,8 @@ packages:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
openai@4.57.0:
resolution: {integrity: sha512-JnwBSIYqiZ3jYjB5f2in8hQ0PRA092c6m+/6dYB0MzK0BEbn+0dioxZsPLBm5idJbg9xzLNOiGVm2OSuhZ+BdQ==}
openai@4.61.0:
resolution: {integrity: sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==}
hasBin: true
peerDependencies:
zod: ^3.23.8
@ -10087,7 +10087,7 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
'@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)':
'@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)':
dependencies:
'@chakra-ui/react': 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@emotion/cache': 11.11.0
@ -16690,7 +16690,7 @@ snapshots:
neo-async@2.6.2: {}
next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.24.8
'@types/hoist-non-react-statics': 3.3.5
@ -16887,7 +16887,7 @@ snapshots:
dependencies:
mimic-fn: 4.0.0
openai@4.57.0(encoding@0.1.13):
openai@4.61.0(encoding@0.1.13):
dependencies:
'@types/node': 18.19.40
'@types/node-fetch': 2.6.11

View File

@ -54,6 +54,60 @@
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {}
},
{
"model": "o1-mini",
"name": "o1-mini",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 120000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": false,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {
"temperature": 1,
"max_tokens": null,
"stream": false
}
},
{
"model": "o1-preview",
"name": "o1-preview",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 120000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": false,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {
"temperature": 1,
"max_tokens": null,
"stream": false
}
}
],
"vectorModels": [

View File

@ -39,7 +39,6 @@ const Markdown = ({
() => ({
img: Image,
pre: RewritePre,
p: (pProps: any) => <p {...pProps} dir="auto" />,
code: Code,
a: A
}),

View File

@ -1,7 +1,16 @@
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import Head from 'next/head';
import React from 'react';
import React, { useMemo } from 'react';
const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?: string }) => {
const formatIcon = useMemo(() => {
if (!icon) return LOGO_ICON;
if (icon.startsWith('http') || icon.startsWith('/')) {
return icon;
}
return LOGO_ICON;
}, [icon]);
return (
<Head>
<title>{title}</title>
@ -11,7 +20,7 @@ const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?:
/>
<meta httpEquiv="Content-Security-Policy" content="img-src * data:;" />
{desc && <meta name="description" content={desc} />}
{icon && <link rel="icon" href={icon} />}
{icon && <link rel="icon" href={formatIcon} />}
</Head>
);
};

View File

@ -504,7 +504,6 @@ const ChatInput = ({
const files = Array.from(items)
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
.filter((file) => {
console.log(file);
return file && fileTypeFilter(file);
}) as File[];
onSelectFile(files);

View File

@ -35,8 +35,7 @@ const RenderText = React.memo(function RenderText({
showAnimation: boolean;
text?: string;
}) {
let source = (text || '').trim();
let source = text || '';
// First empty line
// if (!source && !isLastChild) return null;

View File

@ -8,7 +8,6 @@ import Markdown from '@/components/Markdown';
import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
@ -337,6 +336,22 @@ export const WholeResponseContent = ({
label={t('common:core.chat.response.update_var_result')}
value={activeModule?.updateVarResult}
/>
{/* loop */}
<Row label={t('common:core.chat.response.loop_input')} value={activeModule?.loopInput} />
<Row label={t('common:core.chat.response.loop_output')} value={activeModule?.loopResult} />
{/* loopStart */}
<Row
label={t('common:core.chat.response.loop_input_element')}
value={activeModule?.loopInputValue}
/>
{/* loopEnd */}
<Row
label={t('common:core.chat.response.loop_output_element')}
value={activeModule?.loopOutputValue}
/>
</Box>
) : null;
};
@ -525,6 +540,9 @@ export const ResponseBox = React.memo(function ResponseBox({
if (Array.isArray(item.pluginDetail)) {
helper(item.pluginDetail);
}
if (Array.isArray(item.loopDetail)) {
helper(item.loopDetail);
}
}
});
}
@ -552,9 +570,10 @@ export const ResponseBox = React.memo(function ResponseBox({
function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[] {
return res.map((item) => {
let children: sideTabItemType[] = [];
if (!!(item?.toolDetail || item?.pluginDetail)) {
if (!!(item?.toolDetail || item?.pluginDetail || item?.loopDetail)) {
if (item?.toolDetail) children.push(...pretreatmentResponse(item?.toolDetail));
if (item?.pluginDetail) children.push(...pretreatmentResponse(item?.pluginDetail));
if (item?.loopDetail) children.push(...pretreatmentResponse(item?.loopDetail));
}
return {
@ -611,7 +630,7 @@ export const ResponseBox = React.memo(function ResponseBox({
<>
{isPc && !useMobile ? (
<Flex overflow={'hidden'} height={'100%'}>
<Box flex={'2 0 0'} borderRight={'sm'} p={3}>
<Box flex={'2 0 0'} w={0} borderRight={'sm'} p={3}>
<Box overflow={'auto'} height={'100%'}>
<WholeResponseSideTab
response={sliderResponseList}
@ -620,7 +639,7 @@ export const ResponseBox = React.memo(function ResponseBox({
/>
</Box>
</Box>
<Box flex={'5 0 0'} height={'100%'}>
<Box flex={'5 0 0'} w={0} height={'100%'}>
<WholeResponseContent
activeModule={activeModule}
hideTabs={hideTabs}

View File

@ -29,18 +29,12 @@ const InvoiceTable = () => {
data: invoices,
isLoading,
Pagination,
getData,
total
} = usePagination({
api: getInvoiceRecords,
pageSize: 20,
defaultRequest: false
pageSize: 20
});
useEffect(() => {
getData(1);
}, [getData]);
return (
<MyBox isLoading={isLoading} position={'relative'} h={'100%'} overflow={'overlay'}>
<TableContainer minH={'50vh'}>

View File

@ -18,69 +18,67 @@ async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> {
try {
await connectToDatabase();
const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } =
req.body as getHistoriesBody;
const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } =
req.body as getHistoriesBody;
const limit = shareId && outLinkUid ? 20 : 30;
const match = await (async () => {
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
const match = await (async () => {
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
};
}
if (appId && teamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
return {
teamId,
appId,
outLinkUid: uid,
source: ChatSourceEnum.team
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
tmbId,
appId,
source: ChatSourceEnum.online
};
}
return Promise.reject('Params are error');
})();
const [data, total] = await Promise.all([
await MongoChat.find(match, 'chatId title top customTitle appId updateTime')
.sort({ top: -1, updateTime: -1 })
.skip((current - 1) * pageSize)
.limit(pageSize),
MongoChat.countDocuments(match)
]);
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
};
}
if (appId && teamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
return {
teamId,
appId,
outLinkUid: uid,
source: ChatSourceEnum.team
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
tmbId,
appId,
source: ChatSourceEnum.online
};
}
})();
if (!match) {
return {
list: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
})),
total
list: [],
total: 0
};
} catch (err) {
return Promise.reject(err);
}
const [data, total] = await Promise.all([
await MongoChat.find(match, 'chatId title top customTitle appId updateTime')
.sort({ top: -1, updateTime: -1 })
.skip((current - 1) * pageSize)
.limit(pageSize),
MongoChat.countDocuments(match)
]);
return {
list: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
})),
total
};
}
export default NextAPI(handler);

View File

@ -20,6 +20,7 @@ async function handler(
const {
parentId,
name,
intro,
type = DatasetTypeEnum.dataset,
avatar,
vectorModel = global.vectorModels[0].model,
@ -47,6 +48,7 @@ async function handler(
const { _id } = await MongoDataset.create({
...parseParentIdInMongo(parentId),
name,
intro,
teamId,
tmbId,
vectorModel,

View File

@ -3,11 +3,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log';
import {
ChatItemValueTypeEnum,
ChatRoleEnum,
ChatSourceEnum
} from '@fastgpt/global/core/chat/constants';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';

View File

@ -30,6 +30,7 @@ import { compareSnapshot } from '@/web/core/workflow/utils';
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks';
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
@ -66,26 +67,32 @@ const Header = () => {
setPast
} = useContextSelector(WorkflowContext, (v) => v);
const isPublished = useMemo(() => {
/*
Find the last saved snapshot in the past and future snapshots
*/
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved);
const [isPublished, setIsPublished] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
return compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
}, [future, past, nodes, edges, appDetail.chatConfig]);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
setIsPublished(val);
},
[future, past, nodes, edges, appDetail.chatConfig],
{
wait: 500
}
);
const { runAsync: onClickSave, loading } = useRequest2(
async ({
@ -205,7 +212,7 @@ const Header = () => {
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);

View File

@ -124,7 +124,7 @@ const EditForm = ({
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
const tokenLimit = useMemo(() => {
return selectedModel?.quoteMaxToken || 3000;
}, [selectedModel.quoteMaxToken]);
}, [selectedModel?.quoteMaxToken]);
return (
<>
@ -343,7 +343,7 @@ const EditForm = ({
{/* File select */}
<Box {...BoxStyles}>
<FileSelectConfig
forbidVision={!selectedModel.vision}
forbidVision={!selectedModel?.vision}
value={appForm.chatConfig.fileSelectConfig}
onChange={(e) => {
setAppForm((state) => ({

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Flex,
@ -30,6 +30,7 @@ import { compareSnapshot } from '@/web/core/workflow/utils';
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks';
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
@ -66,26 +67,33 @@ const Header = () => {
setPast
} = useContextSelector(WorkflowContext, (v) => v);
const isPublished = useMemo(() => {
/*
Find the last saved snapshot in the past and future snapshots
*/
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved);
// Check if the workflow is published
const [isPublished, setIsPublished] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
return compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
}, [future, past, nodes, edges, appDetail.chatConfig]);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
setIsPublished(val);
},
[future, past, nodes, edges, appDetail.chatConfig],
{
wait: 500
}
);
const { runAsync: onClickSave, loading } = useRequest2(
async ({
@ -205,7 +213,7 @@ const Header = () => {
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);

View File

@ -6,7 +6,6 @@ import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { WorkflowContext } from './context';
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
import dynamic from 'next/dynamic';
@ -28,7 +27,6 @@ const AppCard = ({
isPublished: boolean;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { feConfigs } = useSystemStore();
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
@ -48,7 +46,7 @@ const AppCard = ({
children: [
{
icon: 'edit',
label: appT('edit_info'),
label: t('app:edit_info'),
onClick: onOpenInfoEdit
},
{
@ -63,7 +61,7 @@ const AppCard = ({
{
children: [
{
label: appT('import_configs'),
label: t('app:import_configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
@ -117,7 +115,6 @@ const AppCard = ({
appDetail.name,
appDetail.permission.hasWritePer,
appDetail.permission.isOwner,
appT,
currentTab,
feConfigs?.show_team_chat,
historiesDefaultData,

View File

@ -69,7 +69,6 @@ const ImportSettings = ({ onClose }: Props) => {
async (e: File[]) => {
const file = e[0];
readJSONFile(file);
console.log(file);
},
[readJSONFile]
);

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Flex,
@ -14,7 +14,7 @@ import type {
NodeTemplateListItemType,
NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import { useViewport, XYPosition } from 'reactflow';
import { useReactFlow, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
@ -36,9 +36,7 @@ import { useRouter } from 'next/router';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useI18n } from '@/web/context/I18n';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import FolderPath from '@/components/common/folder/Path';
@ -49,6 +47,8 @@ import { cloneDeep } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
type ModuleTemplateListProps = {
isOpen: boolean;
@ -397,7 +397,7 @@ const RenderList = React.memo(function RenderList({
const { isPc } = useSystem();
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
const { x, y, zoom } = useViewport();
const { screenToFlowPosition } = useReactFlow();
const { toast } = useToast();
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
@ -454,11 +454,11 @@ const RenderList = React.memo(function RenderList({
}
})();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
const nodePosition = screenToFlowPosition(position);
const mouseX = nodePosition.x - 100;
const mouseY = nodePosition.y - 20;
const node = nodeTemplate2FlowNode({
const newNode = nodeTemplate2FlowNode({
template: {
...templateNode,
name: computedNewNodeName({
@ -482,9 +482,28 @@ const RenderList = React.memo(function RenderList({
description: t(output.description as any)
}))
},
position: { x: mouseX, y: mouseY - 20 },
selected: true
position: { x: mouseX, y: mouseY },
selected: true,
t
});
const newNodes = [newNode];
if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) {
const startNode = nodeTemplate2FlowNode({
template: LoopStartNode,
position: { x: mouseX + 60, y: mouseY + 280 },
parentNodeId: newNode.id,
t
});
const endNode = nodeTemplate2FlowNode({
template: LoopEndNode,
position: { x: mouseX + 420, y: mouseY + 680 },
parentNodeId: newNode.id,
t
});
newNodes.push(startNode, endNode);
}
setNodes((state) => {
const newState = state
@ -493,11 +512,11 @@ const RenderList = React.memo(function RenderList({
selected: false
}))
// @ts-ignore
.concat(node);
.concat(newNodes);
return newState;
});
},
[computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
[computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, screenToFlowPosition]
);
const gridStyle = useMemo(() => {

View File

@ -7,7 +7,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
const { nodes, nodeList, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
WorkflowContext,
(v) => v
);
@ -27,6 +27,10 @@ const ButtonEdge = (props: EdgeProps) => {
targetHandleId,
style
} = props;
const defaultZIndex = useMemo(
() => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 1001 : 0),
[nodeList, source]
);
const onDelConnect = useCallback(
(id: string) => {
@ -135,7 +139,7 @@ const ButtonEdge = (props: EdgeProps) => {
bg={'white'}
borderRadius={'17px'}
cursor={'pointer'}
zIndex={1000}
zIndex={9999}
onClick={() => onDelConnect(id)}
>
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
@ -150,7 +154,7 @@ const ButtonEdge = (props: EdgeProps) => {
w={highlightEdge ? '14px' : '10px'}
h={highlightEdge ? '14px' : '10px'}
// bg={'white'}
zIndex={highlightEdge ? 1000 : 0}
zIndex={highlightEdge ? 1000 : defaultZIndex}
>
<MyIcon
name={'core/workflow/edgeArrow'}
@ -199,8 +203,7 @@ const ButtonEdge = (props: EdgeProps) => {
return {
...style,
strokeWidth: 3,
zIndex: 2
strokeWidth: 3
};
})();

View File

@ -1,10 +1,11 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Box
<Flex
flexDirection={'column'}
px={4}
mx={2}
mb={2}
@ -16,7 +17,7 @@ const Container = ({ children, ...props }: BoxProps) => {
{...props}
>
{children}
</Box>
</Flex>
);
};

View File

@ -12,7 +12,7 @@ const IOTitle = ({
return (
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
<Box>{text}</Box>
<Box color={'myGray.900'}>{text}</Box>
<Box flex={1} />
{inputExplanationUrl && (

View File

@ -8,6 +8,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { useWorkflowUtils } from './useUtils';
import { useKeyPress as useKeyPressEffect } from 'ahooks';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
export const useKeyboard = () => {
const { t } = useTranslation();
@ -50,7 +51,9 @@ export const useKeyboard = () => {
if (!Array.isArray(parseData)) return;
// filter workflow data
const newNodes = parseData
.filter((item) => !!item.type && item.data?.unique !== true)
.filter(
(item) => !!item.type && item.data?.unique !== true && item.type !== FlowNodeTypeEnum.loop
)
.map((item) => {
const nodeId = getNanoid();
return {
@ -64,7 +67,8 @@ export const useKeyboard = () => {
flowNodeType: item.data?.flowNodeType || '',
pluginId: item.data?.pluginId
}),
nodeId
nodeId,
parentNodeId: undefined
},
position: {
x: item.position.x + 100,
@ -73,6 +77,7 @@ export const useKeyboard = () => {
};
});
// Reset all node to not select and concat new node
setNodes((prev) =>
prev
.map((node) => ({

View File

@ -8,9 +8,14 @@ import {
Edge,
Node,
NodePositionChange,
XYPosition
XYPosition,
useReactFlow,
getNodesBounds,
Rect,
NodeRemoveChange,
NodeSelectionChange
} from 'reactflow';
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
@ -18,11 +23,18 @@ import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useMemoizedFn } from 'ahooks';
import {
Input_Template_Node_Height,
Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
/*
Compute helper lines for snapping nodes to each other
Refer: https://reactflow.dev/examples/interaction/helper-lines
*/
Compute helper lines for snapping nodes to each other
Refer: https://reactflow.dev/examples/interaction/helper-lines
*/
type GetHelperLinesResult = {
horizontal?: THelperLine;
vertical?: THelperLine;
@ -259,18 +271,64 @@ export const useWorkflow = () => {
const { t } = useTranslation();
const { isDowningCtrl } = useKeyboard();
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } =
useContextSelector(WorkflowContext, (v) => v);
const {
setConnectingEdge,
nodes,
onNodesChange,
setEdges,
onChangeNode,
onEdgesChange,
setHoverEdgeId
} = useContextSelector(WorkflowContext, (v) => v);
const { getIntersectingNodes } = useReactFlow();
// Loop node size and position
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
const width = rect.width + 110 > 900 ? rect.width + 110 : 900;
const height = rect.height + 380 > 900 ? rect.height + 380 : 900;
// Update parentNode size and position
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeWidth,
value: {
...Input_Template_Node_Width,
value: width
}
});
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeHeight,
value: {
...Input_Template_Node_Height,
value: height
}
});
// Update parentNode position
onNodesChange([
{
id: parentId,
type: 'position',
position: {
x: rect.x - 50,
y: rect.y - 280
}
}
]);
});
/* helper line */
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine>();
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
const customApplyNodeChanges = (changes: NodeChange[], nodes: Node[]) => {
const positionChange =
changes[0].type === 'position' && changes[0].dragging ? changes[0] : undefined;
const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => {
const positionChange = change.type === 'position' && change.dragging ? change : undefined;
if (changes.length === 1 && positionChange?.position) {
if (positionChange?.position) {
// 只判断3000px 内的 nodes并按从近到远的顺序排序
const filterNodes = nodes
.filter((node) => {
@ -303,42 +361,182 @@ export const useWorkflow = () => {
setHelperLineHorizontal(undefined);
setHelperLineVertical(undefined);
}
};
});
// Check if a node is placed on top of a loop node
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
if (!node) return;
// 获取所有与当前节点相交的节点
const intersections = getIntersectingNodes(node);
// 获取所有与当前节点相交的节点中,类型为 loop 的节点
const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop);
const unSupportedTypes = [
FlowNodeTypeEnum.workflowStart,
FlowNodeTypeEnum.loop,
FlowNodeTypeEnum.pluginInput,
FlowNodeTypeEnum.pluginOutput,
FlowNodeTypeEnum.systemConfig
];
if (parentNode && !node.data.parentNodeId) {
if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) {
return toast({
status: 'warning',
title: t('workflow:can_not_loop')
});
}
onChangeNode({
nodeId: node.id,
type: 'attr',
key: 'parentNodeId',
value: parentNode.id
});
// 删除当前节点与其他节点的连接
setEdges((state) =>
state.filter((edge) => edge.source !== node.id && edge.target !== node.id)
);
const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node];
const rect = getNodesBounds(childNodes);
resetParentNodeSizeAndPosition(rect, parentNode.id);
}
});
/* node */
const handleNodesChange = (changes: NodeChange[]) => {
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, node: Node) => {
if (node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('common:core.workflow.Can not delete node')
});
}
// If the node has child nodes, remove the child nodes
if (nodes.some((n) => n.data.parentNodeId === node.id)) {
const childNodes = nodes.filter((n) => n.data.parentNodeId === node.id);
const childNodeIds = childNodes.map((n) => n.id);
const childNodesChange = childNodes.map((node) => ({
...change,
id: node.id
}));
onNodesChange(childNodesChange);
setEdges((state) =>
state.filter(
(edge) =>
edge.source !== change.id &&
edge.target !== change.id &&
!childNodeIds.includes(edge.source) &&
!childNodeIds.includes(edge.target)
)
);
return;
}
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
});
const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => {
// If the node is not selected and the Ctrl key is pressed, select the node
if (change.selected === false && isDowningCtrl) {
change.selected = true;
}
});
const handlePositionNode = useMemoizedFn(
(change: NodePositionChange, node: Node<FlowNodeItemType>) => {
const parentNode: Record<string, 1> = {
[FlowNodeTypeEnum.loop]: 1
};
// If node is a child node, move child node and reset parent node
if (node.data.parentNodeId) {
const parentId = node.data.parentNodeId;
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
checkNodeHelpLine(change, childNodes);
resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId);
}
// If node is parent node, move parent node and child nodes
else if (parentNode[node.data.flowNodeType]) {
// It will update the change value.
checkNodeHelpLine(
change,
nodes.filter((node) => !node.data.parentNodeId)
);
// Compute the child nodes' position
const parentId = node.id;
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
const initPosition = node.position;
const deltaX = change.position?.x ? change.position.x - initPosition.x : 0;
const deltaY = change.position?.y ? change.position.y - initPosition.y : 0;
const childNodesChange: NodePositionChange[] = childNodes.map((node) => {
if (change.dragging) {
const position = {
x: node.position.x + deltaX,
y: node.position.y + deltaY
};
return {
...change,
id: node.id,
position,
positionAbsolute: position
};
} else {
return {
...change,
id: node.id
};
}
});
onNodesChange(childNodesChange);
} else {
checkNodeHelpLine(
change,
nodes.filter((node) => !node.data.parentNodeId)
);
}
}
);
const handleNodesChange = useMemoizedFn((changes: NodeChange[]) => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('common:core.workflow.Can not delete node')
});
} else {
return (() => {
onNodesChange(changes);
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
})();
if (node) {
handleRemoveNode(change, node);
}
} else if (change.type === 'select') {
handleSelectNode(change);
} else if (change.type === 'position') {
const node = nodes.find((n) => n.id === change.id);
if (node) {
handlePositionNode(change, node);
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
}
}
customApplyNodeChanges(changes, nodes);
// default changes
onNodesChange(changes);
};
});
const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
onEdgesChange(changes);
},
[onEdgesChange]
);
const onNodeDragStop = useCallback(
(_: any, node: Node) => {
checkNodeOverLoopNode(node);
},
[checkNodeOverLoopNode]
);
/* connect */
const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => {
@ -403,7 +601,8 @@ export const useWorkflow = () => {
onEdgeMouseEnter,
onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical
helperLineVertical,
onNodeDragStop
};
};

View File

@ -52,7 +52,10 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode')),
[FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect'))
[FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect')),
[FlowNodeTypeEnum.loop]: dynamic(() => import('./nodes/Loop/NodeLoop')),
[FlowNodeTypeEnum.loopStart]: dynamic(() => import('./nodes/Loop/NodeLoopStart')),
[FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/Loop/NodeLoopEnd'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
@ -73,7 +76,8 @@ const Workflow = () => {
onEdgeMouseEnter,
onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical
helperLineVertical,
onNodeDragStop
} = useWorkflow();
const {
@ -146,6 +150,7 @@ const Workflow = () => {
panOnScroll: true
}
: {})}
onNodeDragStop={onNodeDragStop}
>
<FlowController />
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />

View File

@ -0,0 +1,88 @@
/*
The loop node has controllable width and height properties, which serve as the parent node of loopFlow.
When the childNodes of loopFlow change, it automatically calculates the rectangular width, height, and position of the childNodes,
thereby further updating the width and height properties of the loop node.
*/
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React, { useEffect, useMemo } from 'react';
import { Background, NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import IOTitle from '../../components/IOTitle';
import { useTranslation } from 'react-i18next';
import RenderInput from '../render/RenderInput';
import { Box } from '@chakra-ui/react';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import RenderOutput from '../render/RenderOutput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const { nodeWidth, nodeHeight } = useMemo(() => {
return {
nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value,
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value
};
}, [inputs]);
const childrenNodeIdList = useMemo(() => {
return JSON.stringify(
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
);
}, [nodeId, nodeList]);
useEffect(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.childrenNodeIdList,
value: {
...Input_Template_Children_Node_List,
value: JSON.parse(childrenNodeIdList)
}
});
}, [childrenNodeIdList]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
maxW={'full'}
minW={900}
minH={900}
w={nodeWidth}
h={nodeHeight}
menuForbid={{
copy: true
}}
{...data}
>
<Container position={'relative'} flex={1}>
<IOTitle text={t('common:common.Input')} />
<Box mb={6} maxW={'360'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Box>
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
{t('workflow:loop_body')}
</FormLabel>
<Box flex={1} position={'relative'} border={'base'} bg={'myGray.100'} rounded={'8px'}>
<Background />
</Box>
</Container>
<Container>
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
}, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
return Render;
};
export default React.memo(NodeLoop);

View File

@ -0,0 +1,93 @@
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Reference from '../render/RenderInput/templates/Reference';
import { Box } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { AppContext } from '../../../../context';
import { useTranslation } from 'react-i18next';
import { getGlobalVariableNode } from '@/web/core/workflow/adapt';
const typeMap = {
[WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString,
[WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber,
[WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean,
[WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.any]: WorkflowIOValueTypeEnum.arrayAny
};
const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs, parentNodeId } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { t } = useTranslation();
const inputItem = useMemo(
() => inputs.find((input) => input.key === NodeInputKeyEnum.loopEndInput),
[inputs]
);
// Get loopEnd input value type
const valueType = useMemo(() => {
if (!inputItem) return;
const referenceNode = [
...nodeList,
getGlobalVariableNode({ nodes: nodeList, t, chatConfig: appDetail.chatConfig })
].find((node) => node.nodeId === inputItem.value[0]);
return referenceNode?.outputs.find((output) => output.id === inputItem?.value[1])
?.valueType as keyof typeof typeMap;
}, [appDetail.chatConfig, inputItem, nodeList, t]);
useEffect(() => {
if (!valueType) return;
const parentNode = nodeList.find((node) => node.nodeId === parentNodeId);
const parentNodeOutput = parentNode?.outputs.find(
(output) => output.key === NodeOutputKeyEnum.loopArray
);
if (parentNode && parentNodeOutput) {
onChangeNode({
nodeId: parentNode.nodeId,
type: 'updateOutput',
key: NodeOutputKeyEnum.loopArray,
value: {
...parentNodeOutput,
valueType: typeMap[valueType] ?? WorkflowIOValueTypeEnum.arrayAny
}
});
}
}, [valueType, nodeList, nodeId, onChangeNode, parentNodeId]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
{...data}
w={'420px'}
menuForbid={{
copy: true,
delete: true,
debug: true
}}
>
<Box px={4} pb={4}>
{inputItem && <Reference item={inputItem} nodeId={nodeId} />}
</Box>
</NodeCard>
);
}, [data, inputItem, nodeId, selected]);
return Render;
};
export default React.memo(NodeLoopEnd);

View File

@ -0,0 +1,148 @@
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useTranslation } from 'react-i18next';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { Box, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
const typeMap = {
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object,
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
};
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const loopStartNode = useMemo(
() => nodeList.find((node) => node.nodeId === nodeId),
[nodeList, nodeId]
);
// According to the variable referenced by parentInput, find the output of the corresponding node and take its output valueType
const loopItemInputType = useMemo(() => {
const parentNode = nodeList.find((node) => node.nodeId === loopStartNode?.parentNodeId);
const parentArrayInput = parentNode?.inputs.find(
(input) => input.key === NodeInputKeyEnum.loopInputArray
);
return parentArrayInput?.value
? (nodeList
.find((node) => node.nodeId === parentArrayInput?.value[0])
?.outputs.find((output) => output.id === parentArrayInput?.value[1])
?.valueType as keyof typeof typeMap)
: undefined;
}, [loopStartNode?.parentNodeId, nodeList]);
// Auth update loopStartInput output
useEffect(() => {
const loopArrayOutput = loopStartNode?.outputs.find(
(output) => output.key === NodeOutputKeyEnum.loopStartInput
);
// if loopItemInputType is undefined, delete loopStartInput output
if (!loopItemInputType && loopArrayOutput) {
onChangeNode({
nodeId,
type: 'delOutput',
key: NodeOutputKeyEnum.loopStartInput
});
}
// if loopItemInputType is not undefined, and has no loopArrayOutput, add loopStartInput output
if (loopItemInputType && !loopArrayOutput) {
onChangeNode({
nodeId,
type: 'addOutput',
value: {
id: NodeOutputKeyEnum.loopStartInput,
key: NodeOutputKeyEnum.loopStartInput,
label: t('workflow:Array_element'),
type: FlowNodeOutputTypeEnum.static,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
}
});
}
// if loopItemInputType is not undefined, and has loopArrayOutput, update loopStartInput output
if (loopItemInputType && loopArrayOutput) {
onChangeNode({
nodeId,
type: 'updateOutput',
key: NodeOutputKeyEnum.loopStartInput,
value: {
...loopArrayOutput,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
}
});
}
}, [loopStartNode?.outputs, nodeId, onChangeNode, loopItemInputType, t]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
{...data}
w={'420px'}
h={'176px'}
menuForbid={{
copy: true,
delete: true,
debug: true
}}
>
<Box px={4}>
{!loopItemInputType ? (
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={0} iconSize={'32px'} />
) : (
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('common:core.module.variable.variable name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<Flex alignItems={'center'}>
<MyIcon
name={'core/workflow/inputType/array'}
w={'14px'}
mr={1}
color={'primary.600'}
/>
{t('workflow:Array_element')}
</Flex>
</Td>
<Td>{typeMap[loopItemInputType]}</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
)}
</Box>
</NodeCard>
);
}, [data, loopItemInputType, selected, t]);
return Render;
};
export default React.memo(NodeLoopStart);

View File

@ -1,34 +1,37 @@
import React from 'react';
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
}, [splitToolInputs, inputs, nodeId, selected, data]);
return Render;
};
export default React.memo(NodeAnswer);

View File

@ -130,12 +130,16 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);
const Render = useMemo(() => {
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);
}, [CustomComponent, data, inputs, nodeId, selected]);
return Render;
};
export default React.memo(NodeCQNode);

View File

@ -23,11 +23,11 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const { splitToolInputs, onChangeNode, onResetNode } = useContextSelector(
WorkflowContext,
(ctx) => ctx
);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const { ConfirmModal, openConfirm } = useConfirm({
content: workflowT('code.Reset template confirm')
});
@ -73,31 +73,37 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [nodeId, onChangeNode, openConfirm, workflowT]);
}, [data, nodeId, onChangeNode, onResetNode, openConfirm, workflowT]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
}, [ConfirmModal, CustomComponent, data, inputs, nodeId, outputs, selected, splitToolInputs, t]);
return Render;
};
export default React.memo(NodeCode);

View File

@ -26,33 +26,110 @@ import ValueTypeLabel from './render/ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import { getWebLLMModel } from '@/web/common/system/utils';
import { useMemoizedFn } from 'ahooks';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodeId, inputs, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const quoteList = useMemo(() => inputs.filter((item) => item.canEdit), [inputs]);
const Reference = useMemoizedFn(
({ nodeId, inputChildren }: { nodeId: string; inputChildren: FlowNodeInputItemType }) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const tokenLimit = useMemo(() => {
let maxTokens = 13000;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
nodeList.forEach((item) => {
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
return maxTokens;
}, [nodeList, llmModelList]);
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
{/* value */}
<ValueTypeLabel
valueType={inputChildren.valueType}
valueDesc={inputChildren.valueDesc}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(
(inputChildren.referencePlaceholder as any) ||
t('common:core.module.Dataset quote.select')
)}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
</>
);
}
);
const CustomComponent = useMemo(() => {
const quoteList = inputs.filter((item) => item.canEdit);
const tokenLimit = (() => {
let maxTokens = 13000;
nodeList.forEach((item) => {
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
})();
return {
[NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
<Box px={2}>
@ -115,98 +192,23 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [nodeId, onChangeNode, quoteList, t, tokenLimit]);
}, [Reference, inputs, nodeId, nodeList, onChangeNode, t, llmModelList]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
const Render = useMemo(() => {
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
}, [CustomComponent, data, inputs, nodeId, outputs, selected, t]);
return Render;
};
export default React.memo(NodeDatasetConcat);
function Reference({
nodeId,
inputChildren
}: {
nodeId: string;
inputChildren: FlowNodeInputItemType;
}) {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
{/* value */}
<ValueTypeLabel valueType={inputChildren.valueType} valueDesc={inputChildren.valueDesc} />
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(
(inputChildren.referencePlaceholder as any) ||
t('common:core.module.Dataset quote.select')
)}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
</>
);
}

View File

@ -47,9 +47,10 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const requestUrl = inputs.find(
(item) => item.key === NodeInputKeyEnum.httpReqUrl
) as FlowNodeInputItemType;
const requestUrl = useMemo(
() => inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl) as FlowNodeInputItemType,
[inputs]
);
const { userInfo, initUserInfo } = useUserStore();
@ -217,63 +218,90 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
}
);
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
} else {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
{/* select function */}
<MySelect
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('common:core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
}
});
}}
value={selectedFunction}
/>
{/* auto set params and go to edit */}
{!!selectedFunction && (
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
<Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}>
{t('common:core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
const Render = useMemo(() => {
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
} else {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
{/* select function */}
<MySelect
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('common:core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
}
});
}}
value={selectedFunction}
/>
{/* auto set params and go to edit */}
{!!selectedFunction && (
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
<Button
isLoading={isSyncing}
variant={'grayBase'}
size={'sm'}
onClick={onSyncParams}
>
{t('common:core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank');
}}
>
{t('common:plugin.go to laf')}
</Button>
</Flex>
)}
</Container>
{!!selectedFunction && <RenderIO {...props} />}
</NodeCard>
);
}
if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank');
}}
>
{t('common:plugin.go to laf')}
</Button>
</Flex>
)}
</Container>
{!!selectedFunction && <RenderIO {...props} />}
</NodeCard>
);
}
}, [
token,
appid,
selected,
data,
isLoadingFunctions,
lafFunctionSelectList,
t,
selectedFunction,
isSyncing,
onSyncParams,
props,
onChangeNode,
nodeId,
requestUrl,
lafData?.lafFunctions,
lafData?.lafApp?.appid,
feConfigs.lafEnv
]);
return Render;
};
export default React.memo(NodeLaf);

View File

@ -414,7 +414,9 @@ const FieldEditModal = ({
{showValueTypeSelect ? (
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
list={valueTypeSelectList}
list={valueTypeSelectList.filter(
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
setValue('valueType', e);

View File

@ -2,7 +2,6 @@ import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
const VariableTable = ({
variables = [],
@ -14,7 +13,6 @@ const VariableTable = ({
onDelete: (key: string) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const showToolColumn = variables.some((item) => item.isTool);
return (
@ -27,7 +25,7 @@ const VariableTable = ({
{t('common:core.module.variable.variable name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
{showToolColumn && <Th>{workflowT('tool_input')}</Th>}
{showToolColumn && <Th>{t('workflow:tool_input')}</Th>}
<Th borderBottomRightRadius={'none !important'}></Th>
</Tr>
</Thead>

View File

@ -21,39 +21,43 @@ const NodeSimple = ({
const { t } = useTranslation();
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { nodeId, inputs, outputs } = data;
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const filterHiddenInputs = useMemo(() => commonInputs.filter((item) => true), [commonInputs]);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const filterHiddenInputs = commonInputs.filter((item) => true);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Container>
<IOTitle
text={t('common:common.Input')}
inputExplanationUrl={data.inputExplanationUrl}
/>
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Container>
<IOTitle
text={t('common:common.Input')}
inputExplanationUrl={data.inputExplanationUrl}
/>
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
}, [splitToolInputs, inputs, nodeId, minW, maxW, selected, data, t, outputs]);
return Render;
};
export default React.memo(NodeSimple);

View File

@ -1,4 +1,4 @@
import React, { Dispatch, useMemo, useTransition } from 'react';
import React, { Dispatch, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { Box } from '@chakra-ui/react';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';

View File

@ -119,23 +119,49 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
}: {
nodeId: string;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const { showHandle, LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId);
const connectingNode = nodeList.find((node) => node.nodeId === connectingEdge?.nodeId);
const sourceEdges = edges.filter((edge) => edge.target === connectingNode?.nodeId);
const connectingNodeSourceNodeIds = sourceEdges.map((edge) => edge.source);
const connectingNodeSourceNodeIdMap = new Map<string, number>();
let forbidConnect = false;
edges.forEach((edge) => {
if (edge.target === connectingNode?.nodeId) {
connectingNodeSourceNodeIdMap.set(edge.source, 1);
} else if (edge.target === nodeId) {
// Node has be connected tool, it cannot be connect by other handle
if (edge.targetHandle === NodeOutputKeyEnum.selectedTools) {
forbidConnect = true;
}
// The same source handle cannot connect to the same target node
if (
connectingEdge &&
connectingEdge.handleId === edge.sourceHandle &&
edge.target === nodeId
) {
forbidConnect = true;
}
}
});
const showHandle = (() => {
if (forbidConnect) return false;
if (!node) return false;
// Tool connecting
if (connectingEdge && connectingEdge.handleId === NodeOutputKeyEnum.selectedTools)
return false;
// Unable to connect oneself
if (connectingEdge && connectingEdge.nodeId === nodeId) return false;
// Not the same parent node
if (connectingNode && connectingNode?.parentNodeId !== node?.parentNodeId) return false;
// Unable to connect to the source node
if (connectingNodeSourceNodeIds.includes(nodeId)) return false;
if (connectingNodeSourceNodeIdMap.has(nodeId)) return false;
return true;
})();
@ -150,6 +176,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Left}
translate={[-2, 0]}
showHandle={showHandle}
/>
);
})();
@ -164,6 +191,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
showHandle={showHandle}
/>
);
})();
@ -178,6 +206,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
showHandle={showHandle}
/>
);
})();
@ -192,6 +221,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
showHandle={showHandle}
/>
);
})();
@ -205,14 +235,14 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
};
}, [connectingEdge, edges, nodeId, nodeList]);
return showHandle ? (
return (
<>
{LeftHandle}
{rightHandle}
{topHandle}
{bottomHandle}
</>
) : null;
);
});
export default function Dom() {

View File

@ -117,6 +117,7 @@ const MySourceHandle = React.memo(function MySourceHandle({
if (!node) return null;
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
return <>{RenderHandle}</>;
});
@ -136,18 +137,16 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
position,
translate,
highlightStyle,
connectedStyle
connectedStyle,
showHandle
}: Props & {
showHandle: boolean;
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { connectingEdge, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const connected = edges.some((edge) => edge.targetHandle === handleId);
const connectedEdges = edges.filter((edge) => edge.target === nodeId);
const translateStr = useMemo(() => {
if (!translate) return '';
@ -190,30 +189,6 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
return;
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
const showHandle = useMemo(() => {
if (!node) return false;
// check tool connected
if (
edges.some(
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
)
) {
return false;
}
if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false;
// From same source node and same handle
if (
connectedEdges.some(
(item) => item.sourceHandle === connectingEdge?.handleId && item.target === nodeId
)
)
return false;
return true;
}, [connectedEdges, connectingEdge?.handleId, edges, node, nodeId]);
const RenderHandle = useMemo(() => {
return (
<Handle
@ -237,7 +212,11 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
return RenderHandle;
});
export const TargetHandle = (props: Props) => {
export const TargetHandle = (
props: Props & {
showHandle: boolean;
}
) => {
return (
<MyTargetHandle
{...props}

View File

@ -30,6 +30,9 @@ type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
maxW?: string | number;
minH?: string | number;
w?: string | number;
h?: string | number;
selected?: boolean;
menuForbid?: {
debug?: boolean;
@ -50,6 +53,9 @@ const NodeCard = (props: Props) => {
intro,
minW = '300px',
maxW = '600px',
minH = 0,
w = 'full',
h = 'full',
nodeId,
selected,
menuForbid,
@ -222,7 +228,7 @@ const NodeCard = (props: Props) => {
</MyTooltip>
)}
</Flex>
<MenuRender nodeId={nodeId} menuForbid={menuForbid} />
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
<ConfirmSyncModal />
@ -234,11 +240,12 @@ const NodeCard = (props: Props) => {
avatar,
t,
name,
menuForbid,
hasNewVersion,
onOpenConfirmSync,
onClickSyncVersion,
nodeTemplate?.diagram,
menuForbid,
nodeList,
intro,
ConfirmSyncModal,
onOpenCustomTitleModal,
@ -255,13 +262,17 @@ const NodeCard = (props: Props) => {
}, [nodeId]);
return (
<Box
<Flex
flexDirection={'column'}
minW={minW}
maxW={maxW}
minH={minH}
bg={'white'}
borderWidth={'1px'}
borderRadius={'md'}
boxShadow={'1'}
w={w}
h={h}
_hover={{
boxShadow: '4',
'& .controller-menu': {
@ -291,7 +302,7 @@ const NodeCard = (props: Props) => {
{RenderHandle}
<EditTitleModal maxLength={20} />
</Box>
</Flex>
);
};
@ -299,16 +310,17 @@ export default React.memo(NodeCard);
const MenuRender = React.memo(function MenuRender({
nodeId,
menuForbid
menuForbid,
nodeList
}: {
nodeId: string;
menuForbid?: Props['menuForbid'];
nodeList: FlowNodeItemType[];
}) {
const { t } = useTranslation();
const { openDebugNode, DebugInputModal } = useDebug();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
const { setNodes, setEdges, onNodesChange } = useContextSelector(WorkflowContext, (v) => v);
const { computedNewNodeName } = useWorkflowUtils();
const onCopyNode = useCallback(
@ -347,6 +359,7 @@ const MenuRender = React.memo(function MenuRender({
version: template.version
},
selected: true,
parentNodeId: undefined,
t
})
);
@ -356,10 +369,26 @@ const MenuRender = React.memo(function MenuRender({
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
// Remove node and its child nodes
setNodes((state) =>
state.filter((item) => item.data.nodeId !== nodeId && item.data.parentNodeId !== nodeId)
);
// Remove edges connected to the node and its child nodes
const childNodeIds = nodeList
.filter((node) => node.parentNodeId === nodeId)
.map((node) => node.nodeId);
setEdges((state) =>
state.filter(
(edge) =>
edge.source !== nodeId &&
edge.target !== nodeId &&
!childNodeIds.includes(edge.target) &&
!childNodeIds.includes(edge.source)
)
);
},
[setEdges, setNodes]
[nodeList, setEdges, setNodes]
);
const Render = useMemo(() => {

View File

@ -1,4 +1,3 @@
import { useI18n } from '@/web/context/I18n';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import {
Box,
@ -39,7 +38,6 @@ const FieldModal = ({
onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT, commonT } = useI18n();
const { toast } = useToast();
const isEdit = !!defaultInput.key;
@ -56,7 +54,7 @@ const FieldModal = ({
return false;
}, [customInputConfig.selectValueTypeList, inputType]);
const valueTypeSelectLit = useMemo(() => {
const valueTypeSelectList = useMemo(() => {
if (!customInputConfig.selectValueTypeList) return [];
const dataTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
@ -88,7 +86,7 @@ const FieldModal = ({
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('field_name_already_exists')
title: t('workflow:field_name_already_exists')
});
return;
}
@ -103,7 +101,7 @@ const FieldModal = ({
});
onClose();
},
[defaultInput.key, isEdit, keys, onClose, onSubmit, toast, workflowT]
[defaultInput.key, isEdit, keys, onClose, onSubmit, toast, t]
);
const onSubmitError = useCallback(
(e: Object) => {
@ -124,18 +122,20 @@ const FieldModal = ({
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('edit_input') : workflowT('add_new_input')}
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
overflow={'unset'}
>
<ModalBody w={'100%'} overflow={'auto'} display={'flex'} flexDirection={['column', 'row']}>
<Stack w={'100%'} spacing={3}>
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{commonT('core.module.Data Type')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('common:core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
w={'full'}
list={valueTypeSelectLit}
list={valueTypeSelectList.filter(
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
@ -159,7 +159,7 @@ const FieldModal = ({
</Flex>
{customInputConfig.showDescription && (
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('input_description')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('workflow:input_description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex>
)}
@ -167,10 +167,10 @@ const FieldModal = ({
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{commonT('common.Close')}
{t('common:common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{commonT('common.Confirm')}
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@ -48,13 +48,10 @@ const InputLabel = ({ nodeId, input }: Props) => {
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
<FormLabel required={required}>{t(label as any)}</FormLabel>
<Flex alignItems={'center'} position={'relative'} fontWeight={'medium'}>
<FormLabel required={required} color={'myGray.600'}>
{t(label as any)}
</FormLabel>
{description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>}
</Flex>
{/* value type */}

View File

@ -30,9 +30,11 @@ type SelectProps = {
children: {
label: string;
value: string;
valueType?: WorkflowIOValueTypeEnum;
}[];
}[];
onSelect: (val: ReferenceValueProps) => void;
popDirection?: 'top' | 'bottom';
styles?: ButtonProps;
};
@ -77,12 +79,19 @@ const Reference = ({ item, nodeId }: RenderInputProps) => {
value: item.value
});
const popDirection = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId);
if (!node) return 'bottom';
return node.flowNodeType === FlowNodeTypeEnum.loop ? 'top' : 'bottom';
}, [nodeId, nodeList]);
return (
<ReferSelector
placeholder={t((item.referencePlaceholder as any) || 'select_reference_variable')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
popDirection={popDirection}
/>
);
};
@ -130,13 +139,17 @@ export const useReference = ({
(output) =>
valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === valueType
output.valueType === valueType ||
// When valueType is arrayAny, return all array type outputs
(valueType === WorkflowIOValueTypeEnum.arrayAny &&
output.valueType?.includes('array'))
)
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => {
return {
label: t((output.label as any) || ''),
value: output.id
value: output.id,
valueType: output.valueType
};
})
};
@ -163,7 +176,13 @@ export const useReference = ({
formatValue
};
};
export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => {
export const ReferSelector = ({
placeholder,
value,
list = [],
onSelect,
popDirection
}: SelectProps) => {
const selectItemLabel = useMemo(() => {
if (!value) {
return;
@ -198,9 +217,10 @@ export const ReferSelector = ({ placeholder, value, list = [], onSelect }: Selec
onSelect={(e) => {
onSelect(e as ReferenceValueProps);
}}
popDirection={popDirection}
/>
);
}, [list, onSelect, placeholder, selectItemLabel, value]);
}, [list, onSelect, placeholder, popDirection, selectItemLabel, value]);
return Render;
};

View File

@ -1,4 +1,3 @@
import { useI18n } from '@/web/context/I18n';
import {
FlowNodeOutputTypeEnum,
FlowValueTypeMap
@ -41,7 +40,6 @@ const FieldModal = ({
onSubmit: (e: { data: FlowNodeOutputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT, commonT } = useI18n();
const { toast } = useToast();
const isEdit = !!defaultValue.key;
@ -57,7 +55,7 @@ const FieldModal = ({
return true;
}, [customFieldConfig.selectValueTypeList]);
const valueTypeSelectLit = useMemo(() => {
const valueTypeSelectList = useMemo(() => {
if (!customFieldConfig.selectValueTypeList) return [];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
@ -81,7 +79,7 @@ const FieldModal = ({
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('field_name_already_exists')
title: t('workflow:field_name_already_exists')
});
return;
}
@ -97,7 +95,7 @@ const FieldModal = ({
});
onClose();
},
[defaultValue.key, isEdit, keys, onClose, onSubmit, toast, workflowT]
[defaultValue.key, isEdit, keys, onClose, onSubmit, toast, t]
);
const onSubmitError = useCallback(
(e: Object) => {
@ -118,18 +116,20 @@ const FieldModal = ({
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('edit_input') : workflowT('add_new_input')}
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
overflow={'unset'}
>
<ModalBody w={'100%'} overflow={'auto'} display={'flex'} flexDirection={['column', 'row']}>
<Stack w={'100%'} spacing={3}>
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{commonT('core.module.Data Type')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('common:core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
w={'full'}
list={valueTypeSelectLit}
list={valueTypeSelectList.filter(
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
@ -154,7 +154,7 @@ const FieldModal = ({
</Flex>
{customFieldConfig.showDescription && (
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('input_description')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('workflow:input_description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex>
)}
@ -162,10 +162,10 @@ const FieldModal = ({
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{commonT('common.Close')}
{t('common:common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{commonT('common.Confirm')}
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@ -118,7 +118,6 @@ type WorkflowContextType = {
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
// common function
onFixView: () => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
nodeId: string
@ -201,9 +200,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
): void {
throw new Error('Function not implemented.');
},
onFixView: function (): void {
throw new Error('Function not implemented.');
},
basicNodeTemplates: [],
reactFlowWrapper: null,
nodes: [],
@ -409,13 +405,30 @@ const WorkflowContextProvider = ({
[nodeListString]
);
// Elevate childNodes
useEffect(() => {
setNodes((nodes) =>
nodes.map((node) => (node.data.parentNodeId ? { ...node, zIndex: 1001 } : node))
);
}, [nodeList]);
// Elevate edges of childNodes
useEffect(() => {
setEdges((state) =>
state.map((item) =>
nodeList.some((node) => item.source === node.nodeId && node.parentNodeId)
? { ...item, zIndex: 1001 }
: item
)
);
}, [edges.length]);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodes]);
return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodeList]);
const onUpdateNodeError = useMemoizedFn((nodeId: string, isError: Boolean) => {
setNodes((nodes) => {
return nodes.map((item) => {
setNodes((state) => {
return state.map((item) => {
if (item.data?.nodeId === nodeId) {
item.selected = true;
//@ts-ignore
@ -535,17 +548,8 @@ const WorkflowContextProvider = ({
[nodeList]
);
/* function */
const onFixView = useMemoizedFn(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
});
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = (inputs: FlowNodeInputItemType[], nodeId: string) => {
const splitToolInputs = useMemoizedFn((inputs: FlowNodeInputItemType[], nodeId: string) => {
const isTool = !!edges.find(
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
);
@ -558,40 +562,7 @@ const WorkflowContextProvider = ({
return !item.toolDescription;
})
};
};
const initData = useMemoizedFn(
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
/*
Refresh web page, load init
*/
if (isInit && past.length > 0) {
return resetSnapshot(past[0]);
}
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
// If it is the initial data, save the initial snapshot
if (isInit) {
saveSnapshot({
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
customTitle: t(`app:app.version_initial`),
chatConfig: appDetail.chatConfig,
isSaved: true
});
}
}
);
});
/* ui flow to store data */
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
@ -611,9 +582,7 @@ const WorkflowContextProvider = ({
});
const flowData2StoreData = useMemoizedFn(() => {
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return storeNodes;
return uiWorkflow2StoreWorkflow({ nodes, edges });
});
/* debug */
@ -762,7 +731,7 @@ const WorkflowContextProvider = ({
},
[appId, onChangeNode, setNodes, workflowDebugData]
);
const onStopNodeDebug = useCallback(() => {
const onStopNodeDebug = useMemoizedFn(() => {
setWorkflowDebugData(undefined);
setNodes((state) =>
state.map((node) => ({
@ -774,8 +743,8 @@ const WorkflowContextProvider = ({
}
}))
);
}, [setNodes]);
const onStartNodeDebug = useCallback(
});
const onStartNodeDebug = useMemoizedFn(
async ({
entryNodeId,
runtimeNodes,
@ -795,26 +764,9 @@ const WorkflowContextProvider = ({
setWorkflowDebugData(data);
onNextNodeDebug(data);
},
[onNextNodeDebug, onStopNodeDebug]
}
);
/* Version histories */
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes,
edges
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [edges, nodes]);
/* chat test */
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
const [workflowTestData, setWorkflowTestData] = useState<{
@ -829,24 +781,20 @@ const WorkflowContextProvider = ({
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past`, {
defaultValue: []
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
const [future, setFuture] = useLocalStorageState<SnapshotsType[]>(`${appId}-future`, {
defaultValue: []
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
const resetSnapshot = useCallback(
(state: SnapshotsType) => {
setNodes(state.nodes);
setEdges(state.edges);
setAppDetail((detail) => ({
...detail,
chatConfig: state.chatConfig
}));
},
[setAppDetail, setEdges, setNodes]
);
const resetSnapshot = useMemoizedFn((state: SnapshotsType) => {
setNodes(state.nodes);
setEdges(state.edges);
setAppDetail((detail) => ({
...detail,
chatConfig: state.chatConfig
}));
});
const { runAsync: saveSnapshot } = useRequest2(
const saveSnapshot = useMemoizedFn(
async ({
pastNodes,
pastEdges,
@ -893,12 +841,10 @@ const WorkflowContextProvider = ({
setFuture([]);
return true;
},
{
refreshDeps: [nodes, edges, appDetail.chatConfig, past]
}
);
// Auto save snapshot
useDebounceEffect(
() => {
if (!nodes.length) return;
@ -913,15 +859,14 @@ const WorkflowContextProvider = ({
{ wait: 500 }
);
const undo = useCallback(() => {
const undo = useMemoizedFn(() => {
if (past[1]) {
setFuture((future) => [past[0], ...future]);
setPast((past) => past.slice(1));
resetSnapshot(past[1]);
}
}, [past, setFuture, setPast, resetSnapshot]);
const redo = useCallback(() => {
});
const redo = useMemoizedFn(() => {
const futureState = future[0];
if (futureState) {
@ -929,7 +874,40 @@ const WorkflowContextProvider = ({
setFuture((future) => future.slice(1));
resetSnapshot(futureState);
}
}, [future, setPast, setFuture, resetSnapshot]);
});
const initData = useMemoizedFn(
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
/*
Refresh web page, load init
*/
if (isInit && past.length > 0) {
return resetSnapshot(past[0]);
}
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
// If it is the initial data, save the initial snapshot
if (isInit) {
saveSnapshot({
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
customTitle: t(`app:app.version_initial`),
chatConfig: appDetail.chatConfig,
isSaved: true
});
}
}
);
// remove other app's snapshot
useEffect(() => {
@ -943,6 +921,22 @@ const WorkflowContextProvider = ({
});
}, [appId]);
/* Version histories */
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes,
edges
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [edges, nodes]);
const value = {
appId,
reactFlowWrapper,
@ -986,7 +980,6 @@ const WorkflowContextProvider = ({
canRedo: !!future.length,
// function
onFixView,
splitToolInputs,
initData,
flowData2StoreDataAndCheck,

View File

@ -25,17 +25,10 @@ export const uiWorkflow2StoreWorkflow = ({
version: item.data.version,
inputs: item.data.inputs,
outputs: item.data.outputs,
pluginId: item.data.pluginId
pluginId: item.data.pluginId,
parentNodeId: item.data.parentNodeId
}));
// get all handle
const reactFlowViewport = document.querySelector('.react-flow__viewport');
// Gets the value of data-handleid on all elements below it whose data-handleid is not empty
const handleList =
reactFlowViewport?.querySelectorAll('[data-handleid]:not([data-handleid=""])') || [];
const handleIdList = Array.from(handleList).map(
(item) => item.getAttribute('data-handleid') || ''
);
const formatEdges: StoreEdgeItemType[] = edges
.map((item) => ({
source: item.source,
@ -43,11 +36,7 @@ export const uiWorkflow2StoreWorkflow = ({
sourceHandle: item.sourceHandle || '',
targetHandle: item.targetHandle || ''
}))
.filter((item) => item.sourceHandle && item.targetHandle)
.filter(
// Filter out edges that do not have both sourceHandle and targetHandle
(item) => handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle)
);
.filter((item) => item.sourceHandle && item.targetHandle);
return {
nodes: formatNodes,

View File

@ -96,7 +96,7 @@ export async function generateQA(): Promise<any> {
addLog.info(`[QA Queue] Start`);
try {
const model = getLLMModel(data.model)?.model;
const modelData = getLLMModel(data.model);
const prompt = `${data.prompt || Prompt_AgentQA.description}
${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
@ -112,10 +112,11 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
timeout: 600000
});
const chatResponse = await ai.chat.completions.create({
model,
model: modelData.model,
temperature: 0.3,
messages: await loadRequestMessages({ messages, useVision: false }),
stream: false
stream: false,
...modelData.defaultConfig
});
const answer = chatResponse.choices?.[0].message?.content || '';
@ -150,7 +151,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
tmbId: data.tmbId,
tokens: await countGptMessagesTokens(messages),
billId: data.billId,
model
model: modelData.model
});
} else {
addLog.info(`QA result 0:`, { answer });

View File

@ -16,9 +16,5 @@ export const getWebLLMModel = (model?: string) => {
export const watchWindowHidden = () => {
// @ts-ignore
if (document.hidden) {
window.windowHidden = true;
} else {
window.windowHidden = false;
}
window.windowHidden = document.hidden;
};

View File

@ -40,16 +40,24 @@ import { workflowSystemVariables } from '../app/utils';
export const nodeTemplate2FlowNode = ({
template,
position,
selected
selected,
parentNodeId,
zIndex,
t
}: {
template: FlowNodeTemplateType;
position: XYPosition;
selected?: boolean;
parentNodeId?: string;
zIndex?: number;
t: TFunction;
}): Node<FlowNodeItemType> => {
// replace item data
const moduleItem: FlowNodeItemType = {
...template,
nodeId: getNanoid()
name: t(template.name as any),
nodeId: getNanoid(),
parentNodeId
};
return {
@ -57,16 +65,21 @@ export const nodeTemplate2FlowNode = ({
type: moduleItem.flowNodeType,
data: moduleItem,
position: position,
selected
selected,
zIndex
};
};
export const storeNode2FlowNode = ({
item: storeNode,
selected = false,
zIndex,
parentNodeId,
t
}: {
item: StoreNodeItemType;
selected?: boolean;
zIndex?: number;
parentNodeId?: string;
t: TFunction;
}): Node<FlowNodeItemType> => {
// init some static data
@ -84,11 +97,11 @@ export const storeNode2FlowNode = ({
// replace item data
const nodeItem: FlowNodeItemType = {
parentNodeId,
...template,
...storeNode,
avatar: template.avatar ?? storeNode.avatar,
version: storeNode.version ?? template.version ?? defaultNodeVersion,
/*
Inputs and outputs, New fields are added, not reduced
*/
@ -150,7 +163,8 @@ export const storeNode2FlowNode = ({
type: storeNode.flowNodeType,
data: nodeItem,
selected,
position: storeNode.position || { x: 0, y: 0 }
position: storeNode.position || { x: 0, y: 0 },
zIndex
};
};
export const storeEdgesRenderEdge = ({ edge }: { edge: StoreEdgeItemType }) => {