diff --git a/docSite/content/zh-cn/docs/development/upgrading/4810.md b/docSite/content/zh-cn/docs/development/upgrading/4810.md index 667c6200d30d..39ce8cacd32c 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4810.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4810.md @@ -71,16 +71,17 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4810' \ 22. 优化 - 知识库详情页 UI。 23. 优化 - 支持无网络配置情况下运行。 24. 优化 - 部分全局变量,增加数据类型约束。 -25. 修复 - 全局变量 key 可能重复。 -26. 修复 - Prompt 模式调用工具,stream=false 模式下,会携带 0: 开头标记。 -27. 修复 - 对话日志鉴权问题:仅为 APP 管理员的用户,无法查看对话日志详情。 -28. 修复 - 选择 Milvus 部署时,无法导出知识库。 -29. 修复 - 创建 APP 副本,无法复制系统配置。 -30. 修复 - 图片识别模式下,自动解析图片链接正则不够严谨问题。 -31. 修复 - 内容提取的数据类型与输出数据类型未一致。 -32. 修复 - 工作流运行时间统计错误。 -33. 修复 - stream 模式下,工具调用有可能出现 undefined -34. 修复 - 全局变量在 API 中无法持久化。 -35. 修复 - OpenAPI,detail=false模式下,不应该返回 tool 调用结果,仅返回文字。(可解决 cow 不适配问题) -36. 修复 - 知识库标签重复加载。 -37. 修复 - Debug 模式下,循环调用边问题。 +25. 优化 - 查看工作流详情,切换 tab 时,自动滚动到顶部。 +26. 修复 - 全局变量 key 可能重复。 +27. 修复 - Prompt 模式调用工具,stream=false 模式下,会携带 0: 开头标记。 +28. 修复 - 对话日志鉴权问题:仅为 APP 管理员的用户,无法查看对话日志详情。 +29. 修复 - 选择 Milvus 部署时,无法导出知识库。 +30. 修复 - 创建 APP 副本,无法复制系统配置。 +31. 修复 - 图片识别模式下,自动解析图片链接正则不够严谨问题。 +32. 修复 - 内容提取的数据类型与输出数据类型未一致。 +33. 修复 - 工作流运行时间统计错误。 +34. 修复 - stream 模式下,工具调用有可能出现 undefined +35. 修复 - 全局变量在 API 中无法持久化。 +36. 修复 - OpenAPI,detail=false模式下,不应该返回 tool 调用结果,仅返回文字。(可解决 cow 不适配问题) +37. 修复 - 知识库标签重复加载。 +38. 修复 - Debug 模式下,循环调用边问题。 diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 0426f617aab4..472dd290ab6c 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -118,7 +118,7 @@ export const chats2GPTMessages = ({ tool_calls }) .concat(toolResponse); - } else if (value.text) { + } else if (value.text?.content) { results.push({ dataId, role: ChatCompletionRequestMessageRoleEnum.Assistant, @@ -142,7 +142,7 @@ export const GPTMessages2Chats = ( messages: ChatCompletionMessageParam[], reserveTool = true ): ChatItemType[] => { - return messages + const chatMessages = messages .map((item) => { const value: ChatItemType['value'] = []; const obj = GPT2Chat[item.role]; @@ -288,6 +288,22 @@ export const GPTMessages2Chats = ( } as ChatItemType; }) .filter((item) => item.value.length > 0); + + // Merge data with the same dataId + const result = chatMessages.reduce((result: ChatItemType[], currentItem) => { + const lastItem = result[result.length - 1]; + + if (lastItem && lastItem.dataId === currentItem.dataId && lastItem.obj === currentItem.obj) { + // @ts-ignore + lastItem.value = lastItem.value.concat(currentItem.value); + } else { + result.push(currentItem); + } + + return result; + }, []); + + return result; }; export const chatValue2RuntimePrompt = (value: ChatItemValueItemType[]): RuntimeUserPromptType => { diff --git a/packages/global/core/workflow/runtime/constants.ts b/packages/global/core/workflow/runtime/constants.ts index 743d6a4febf6..6ad42301a9ee 100644 --- a/packages/global/core/workflow/runtime/constants.ts +++ b/packages/global/core/workflow/runtime/constants.ts @@ -22,6 +22,7 @@ export enum DispatchNodeResponseKeyEnum { childrenResponses = 'childrenResponses', // Some nodes make recursive calls that need to be returned toolResponses = 'toolResponses', // The result is passed back to the tool node for use assistantResponses = 'assistantResponses', // assistant response + rewriteHistories = 'rewriteHistories', // If have the response, workflow histories will be rewrite interactive = 'INTERACTIVE' // is interactive } diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 2de90ada4ed6..efa005fbae8b 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -2,9 +2,9 @@ import { ChatNodeUsageType } from '../../../support/wallet/bill/type'; import { ChatItemType, UserChatItemValueItemType, - ChatItemValueItemType, ToolRunResponseItemType, - NodeOutputItemType + NodeOutputItemType, + AIChatItemValueItemType } from '../../chat/type'; import { FlowNodeInputItemType, FlowNodeOutputItemType } from '../type/io.d'; import { StoreNodeItemType } from '../type/node'; @@ -173,13 +173,14 @@ export type DispatchNodeResponseType = { updateVarResult?: any[]; }; -export type DispatchNodeResultType = { +export type DispatchNodeResultType = { [DispatchNodeResponseKeyEnum.skipHandleId]?: string[]; // skip some edge handle id [DispatchNodeResponseKeyEnum.nodeResponse]?: DispatchNodeResponseType; // The node response detail [DispatchNodeResponseKeyEnum.nodeDispatchUsages]?: ChatNodeUsageType[]; // Node total usage [DispatchNodeResponseKeyEnum.childrenResponses]?: DispatchNodeResultType[]; // Children node response [DispatchNodeResponseKeyEnum.toolResponses]?: ToolRunResponseItemType; // Tool response - [DispatchNodeResponseKeyEnum.assistantResponses]?: ChatItemValueItemType[]; // Assistant response(Store to db) + [DispatchNodeResponseKeyEnum.assistantResponses]?: AIChatItemValueItemType[]; // Assistant response(Store to db) + [DispatchNodeResponseKeyEnum.rewriteHistories]?: ChatItemType[]; } & T; /* Single node props */ diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index c8e2b66e4e8f..f2e7ce6b1986 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -27,16 +27,24 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number return limit * 2; }; +/* + Get interaction information (if any) from the last AI message. + What can be done: + 1. Get the interactive data + 2. Check that the workflow starts at the interaction node +*/ export const getLastInteractiveValue = (histories: ChatItemType[]) => { const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI); if (lastAIMessage) { - const interactiveValue = lastAIMessage.value.find( - (v) => v.type === ChatItemValueTypeEnum.interactive - ); - - if (interactiveValue && 'interactive' in interactiveValue) { - return interactiveValue.interactive; + const lastValue = lastAIMessage.value[lastAIMessage.value.length - 1]; + + if ( + lastValue && + lastValue.type === ChatItemValueTypeEnum.interactive && + !!lastValue.interactive + ) { + return lastValue.interactive; } } diff --git a/packages/global/core/workflow/template/system/userSelect/type.d.ts b/packages/global/core/workflow/template/system/userSelect/type.d.ts index 056f6dbd107e..50e977a6e4b3 100644 --- a/packages/global/core/workflow/template/system/userSelect/type.d.ts +++ b/packages/global/core/workflow/template/system/userSelect/type.d.ts @@ -15,7 +15,7 @@ type InteractiveBasicType = { type UserSelectInteractive = { type: 'userSelect'; params: { - // description: string; + description: string; userSelectOptions: UserSelectOptionItemType[]; userSelectedVal?: string; }; diff --git a/packages/service/core/ai/functions/createQuestionGuide.ts b/packages/service/core/ai/functions/createQuestionGuide.ts index 95db092c16f0..711b045f3534 100644 --- a/packages/service/core/ai/functions/createQuestionGuide.ts +++ b/packages/service/core/ai/functions/createQuestionGuide.ts @@ -3,7 +3,7 @@ import { getAIApi } from '../config'; import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; import { loadRequestMessages } from '../../chat/utils'; -export const Prompt_QuestionGuide = `你是一个AI智能助手,可以回答和解决我的问题。请结合前面的对话记录,帮我生成 3 个问题,引导我继续提问。问题的长度应小于20个字符,按 JSON 格式返回: ["问题1", "问题2", "问题3"]`; +export const Prompt_QuestionGuide = `你是一个AI智能助手,可以回答和解决我的问题。请结合前面的对话记录,帮我生成 3 个问题,引导我继续提问,生成问题的语言要与原问题相同。问题的长度应小于20个字符,按 JSON 格式返回: ["问题1", "问题2", "问题3"]`; export async function createQuestionGuide({ messages, @@ -19,6 +19,7 @@ export async function createQuestionGuide({ content: Prompt_QuestionGuide } ]; + const ai = getAIApi({ timeout: 480000 }); diff --git a/packages/service/core/chat/controller.ts b/packages/service/core/chat/controller.ts index aef458ce98d0..95bf6208e695 100644 --- a/packages/service/core/chat/controller.ts +++ b/packages/service/core/chat/controller.ts @@ -1,7 +1,7 @@ import type { ChatItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { MongoChatItem } from './chatItemSchema'; import { addLog } from '../../common/system/log'; -import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { delFileByFileIdList, getGFSCollection } from '../../common/file/gridfs/controller'; import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { MongoChat } from './chatSchema'; @@ -80,52 +80,6 @@ export const addCustomFeedbacks = async ({ } }; -/* - Update the user selected index of the interactive module -*/ -export const updateUserSelectedResult = async ({ - appId, - chatId, - userSelectedVal -}: { - appId: string; - chatId?: string; - userSelectedVal: string; -}) => { - if (!chatId) return; - try { - const chatItem = await MongoChatItem.findOne( - { appId, chatId, obj: ChatRoleEnum.AI }, - 'value' - ).sort({ _id: -1 }); - - if (!chatItem) return; - - const interactiveValue = chatItem.value.find( - (v) => v.type === ChatItemValueTypeEnum.interactive - ); - - if ( - !interactiveValue || - interactiveValue.type !== ChatItemValueTypeEnum.interactive || - !interactiveValue.interactive?.params - ) - return; - - interactiveValue.interactive = { - ...interactiveValue.interactive, - params: { - ...interactiveValue.interactive.params, - userSelectedVal - } - }; - - await chatItem.save(); - } catch (error) { - addLog.error('updateUserSelectedResult error', error); - } -}; - /* Delete chat files 1. ChatId: Delete one chat files diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 4e7d34d21d7a..97f2bcdb608f 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -1,6 +1,10 @@ import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d'; import { MongoApp } from '../app/schema'; -import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; +import { + ChatItemValueTypeEnum, + ChatRoleEnum, + ChatSourceEnum +} from '@fastgpt/global/core/chat/constants'; import { MongoChatItem } from './chatItemSchema'; import { MongoChat } from './chatSchema'; import { addLog } from '../../common/system/log'; @@ -111,3 +115,85 @@ export async function saveChat({ addLog.error(`update chat history error`, error); } } + +export const updateInteractiveChat = async ({ + chatId, + appId, + teamId, + tmbId, + userSelectedVal, + aiResponse, + newVariables, + newTitle +}: { + chatId: string; + appId: string; + teamId: string; + tmbId: string; + userSelectedVal: string; + aiResponse: AIChatItemType & { dataId?: string }; + newVariables?: Record; + newTitle: string; +}) => { + if (!chatId) return; + + const chatItem = await MongoChatItem.findOne({ appId, chatId, obj: ChatRoleEnum.AI }).sort({ + _id: -1 + }); + + if (!chatItem || chatItem.obj !== ChatRoleEnum.AI) return; + + const interactiveValue = chatItem.value[chatItem.value.length - 1]; + + if ( + !interactiveValue || + interactiveValue.type !== ChatItemValueTypeEnum.interactive || + !interactiveValue.interactive?.params + ) { + return; + } + + interactiveValue.interactive = { + ...interactiveValue.interactive, + params: { + ...interactiveValue.interactive.params, + userSelectedVal + } + }; + + if (aiResponse.customFeedbacks) { + chatItem.customFeedbacks = chatItem.customFeedbacks + ? [...chatItem.customFeedbacks, ...aiResponse.customFeedbacks] + : aiResponse.customFeedbacks; + } + + if (aiResponse.responseData) { + chatItem.responseData = chatItem.responseData + ? [...chatItem.responseData, ...aiResponse.responseData] + : aiResponse.responseData; + } + + if (aiResponse.value) { + chatItem.value = chatItem.value ? [...chatItem.value, ...aiResponse.value] : aiResponse.value; + } + + await mongoSessionRun(async (session) => { + await chatItem.save({ session }); + await MongoChat.updateOne( + { + appId, + chatId + }, + { + $set: { + variables: newVariables, + title: newTitle, + updateTime: new Date() + } + }, + { + session + } + ); + }); +}; diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index 7e9725b4a6c0..1212f3ec79e6 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -211,11 +211,40 @@ export const loadRequestMessages = async ({ }; } } + if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant) { + if (item.content !== undefined && !item.content) return; + if (Array.isArray(item.content) && item.content.length === 0) return; + } return item; }) .filter(Boolean) as ChatCompletionMessageParam[]; }; + /* + Merge data for some consecutive roles + 1. Contiguous assistant and both have content, merge content + */ + const mergeConsecutiveMessages = ( + messages: ChatCompletionMessageParam[] + ): ChatCompletionMessageParam[] => { + return messages.reduce((mergedMessages: ChatCompletionMessageParam[], currentMessage) => { + const lastMessage = mergedMessages[mergedMessages.length - 1]; + + if ( + lastMessage && + currentMessage.role === ChatCompletionRequestMessageRoleEnum.Assistant && + lastMessage.role === ChatCompletionRequestMessageRoleEnum.Assistant && + typeof lastMessage.content === 'string' && + typeof currentMessage.content === 'string' + ) { + lastMessage.content += currentMessage ? `\n${currentMessage.content}` : ''; + } else { + mergedMessages.push(currentMessage); + } + + return mergedMessages; + }, []); + }; if (messages.length === 0) { return Promise.reject('core.chat.error.Messages empty'); @@ -245,11 +274,22 @@ export const loadRequestMessages = async ({ ...item, content: await parseUserContent(item.content) }; + } else if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant) { + return { + role: item.role, + content: item.content, + function_call: item.function_call, + name: item.name, + refusal: item.refusal, + tool_calls: item.tool_calls + }; } else { return item; } }) )) as ChatCompletionMessageParam[]; - return clearInvalidMessages(loadMessages) as SdkChatCompletionMessageParam[]; + return mergeConsecutiveMessages( + clearInvalidMessages(loadMessages) + ) as SdkChatCompletionMessageParam[]; }; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index c4f97e1c1574..f864d8de8db1 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -6,6 +6,7 @@ import { import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { ChatDispatchProps, + DispatchNodeResultType, ModuleDispatchProps, SystemVariablesType } from '@fastgpt/global/core/workflow/runtime/type'; @@ -145,14 +146,15 @@ export async function dispatchWorkFlow(data: Props): Promise, + 'nodeResponse' + > ) { if (responseData) { chatResponses.push(responseData); @@ -182,6 +184,10 @@ export async function dispatchWorkFlow(data: Props): Promise item.value !== userSelectedVal) .map((item: any) => getHandleId(nodeId, 'source', item.key)), diff --git a/packages/service/core/workflow/dispatch/tools/http468.ts b/packages/service/core/workflow/dispatch/tools/http468.ts index 22029b3c7b18..ac977e001190 100644 --- a/packages/service/core/workflow/dispatch/tools/http468.ts +++ b/packages/service/core/workflow/dispatch/tools/http468.ts @@ -99,6 +99,18 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise { + return replaceVariable( + replaceEditorVariable({ + text, + nodes: runtimeNodes, + variables: allVariables, + runningNode: node + }), + allVariables + ); + }; + // parse header const headers = await (() => { try { @@ -110,24 +122,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise, item) => { - const key = replaceVariable( - replaceEditorVariable({ - text: item.key, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ); - const value = replaceVariable( - replaceEditorVariable({ - text: item.value, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ); + const key = replaceStringVariables(item.key); + const value = replaceStringVariables(item.value); acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string); return acc; }, {}); @@ -137,24 +133,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise, item) => { - const key = replaceVariable( - replaceEditorVariable({ - text: item.key, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ); - const value = replaceVariable( - replaceEditorVariable({ - text: item.value, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ); + const key = replaceStringVariables(item.key); + const value = replaceStringVariables(item.value); acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string); return acc; }, {}); @@ -165,25 +145,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise ({ - key: replaceVariable( - replaceEditorVariable({ - text: item.key, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ), + key: replaceStringVariables(item.key), type: item.type, - value: replaceVariable( - replaceEditorVariable({ - text: item.value, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ) + value: replaceStringVariables(item.value) })); const formData = new FormData(); for (const { key, value } of httpFormBody) { @@ -194,25 +158,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise ({ - key: replaceVariable( - replaceEditorVariable({ - text: item.key, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ), + key: replaceStringVariables(item.key), type: item.type, - value: replaceVariable( - replaceEditorVariable({ - text: item.value, - nodes: runtimeNodes, - variables, - runningNode: node - }), - allVariables - ) + value: replaceStringVariables(item.value) })); const urlSearchParams = new URLSearchParams(); for (const { key, value } of httpFormBody) { @@ -228,15 +176,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise v); - const isInteractive = useMemo(() => { - const lastAIHistory = chatHistories[chatHistories.length - 1]; - if (!lastAIHistory) return false; - const lastAIMessage = lastAIHistory.value as AIChatItemValueItemType[]; - const interactiveContent = lastAIMessage?.find( - (item) => item.type === ChatItemValueTypeEnum.interactive - )?.interactive?.params; - return !!interactiveContent; - }, [chatHistories]); + // Workflow running, there are user input or selection + const isInteractive = useMemo( + () => checkIsInteractiveByHistories(chatHistories), + [chatHistories] + ); // compute variable input is finish. const chatForm = useForm({ @@ -343,16 +339,15 @@ const ChatBox = ( // create question guide const createQuestionGuide = useCallback( - async ({ history }: { history: ChatSiteItemType[] }) => { + async ({ histories }: { histories: ChatSiteItemType[] }) => { if (!questionGuide || chatController.current?.signal?.aborted) return; - try { const abortSignal = new AbortController(); questionGuideController.current = abortSignal; const result = await postQuestionGuide( { - messages: chats2GPTMessages({ messages: history, reserveId: false }).slice(-6), + messages: chats2GPTMessages({ messages: histories, reserveId: false }).slice(-6), shareId, outLinkUid, teamId, @@ -464,8 +459,9 @@ const ChatBox = ( } ]; - // 插入内容 - setChatHistories(newChatList); + const isInteractive = checkIsInteractiveByHistories(history); + // Update histories(Interactive input does not require new session rounds) + setChatHistories(isInteractive ? newChatList.slice(0, -2) : newChatList); // 清空输入内容 resetInputVal({}); @@ -476,6 +472,7 @@ const ChatBox = ( const abortSignal = new AbortController(); chatController.current = abortSignal; + // Last empty ai message will be removed const messages = chats2GPTMessages({ messages: newChatList, reserveId: true }); const { @@ -483,7 +480,7 @@ const ChatBox = ( responseText, isNewChat = false } = await onStartChat({ - messages: messages.slice(0, -1), + messages: messages, responseChatItemId: responseChatId, controller: abortSignal, generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }), @@ -492,35 +489,29 @@ const ChatBox = ( isNewChatReplace.current = isNewChat; - // set finish status - setChatHistories((state) => - state.map((item, index) => { + // Set last chat finish status + let newChatHistories: ChatSiteItemType[] = []; + setChatHistories((state) => { + newChatHistories = state.map((item, index) => { if (index !== state.length - 1) return item; return { ...item, status: 'finish', - responseData + responseData: item.responseData + ? [...item.responseData, ...responseData] + : responseData }; - }) - ); - setTimeout(() => { - createQuestionGuide({ - history: newChatList.map((item, i) => - i === newChatList.length - 1 - ? { - ...item, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: responseText - } - } - ] - } - : item - ) }); + return newChatHistories; + }); + + setTimeout(() => { + if (!checkIsInteractiveByHistories(newChatHistories)) { + createQuestionGuide({ + histories: newChatHistories + }); + } + generatingScroll(); isPc && TextareaDom.current?.focus(); }, 100); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts index 7fef58950d37..b7e6160c5486 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts @@ -1,7 +1,11 @@ -import { ChatItemValueItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type'; +import { + AIChatItemValueItemType, + ChatItemValueItemType, + ChatSiteItemType +} from '@fastgpt/global/core/chat/type'; import { ChatBoxInputType, UserInputFileItemType } from './type'; import { getFileIcon } from '@fastgpt/global/common/file/icon'; -import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatItemValueTypeEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants'; export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => { if (!value) { @@ -38,6 +42,20 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat }; }; +export const checkIsInteractiveByHistories = (chatHistories: ChatSiteItemType[]) => { + const lastAIHistory = chatHistories[chatHistories.length - 1]; + if (!lastAIHistory) return false; + + const lastMessageValue = lastAIHistory.value[ + lastAIHistory.value.length - 1 + ] as AIChatItemValueItemType; + + return ( + lastMessageValue.type === ChatItemValueTypeEnum.interactive && + !!lastMessageValue?.interactive?.params + ); +}; + export const setUserSelectResultToHistories = ( histories: ChatSiteItemType[], selectVal: string @@ -47,9 +65,14 @@ export const setUserSelectResultToHistories = ( // @ts-ignore return histories.map((item, i) => { if (i !== histories.length - 1) return item; - item.value; - const value = item.value.map((val) => { - if (val.type !== ChatItemValueTypeEnum.interactive || !val.interactive) return val; + + const value = item.value.map((val, i) => { + if ( + i !== item.value.length - 1 || + val.type !== ChatItemValueTypeEnum.interactive || + !val.interactive + ) + return val; return { ...val, @@ -67,6 +90,7 @@ export const setUserSelectResultToHistories = ( return { ...item, + status: ChatStatusEnum.loading, value }; }); diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 1daf2f4f4b04..b9dc41cb4ffb 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -16,7 +16,7 @@ import { ChatSiteItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; -import React from 'react'; +import React, { useMemo } from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { SendPromptFnType } from '../ChatContainer/ChatBox/type'; @@ -45,144 +45,168 @@ const AIResponseBox = ({ }: props) => { const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories); - if (value.type === ChatItemValueTypeEnum.text && value.text) { - let source = (value.text?.content || '').trim(); - - // First empty line - if (!source && chat.value.length > 1) return null; - - // computed question guide + // Question guide + const RenderQuestionGuide = useMemo(() => { if ( isLastChild && !isChatting && questionGuides.length > 0 && index === chat.value.length - 1 ) { - source = `${source} -\`\`\`${CodeClassNameEnum.questionGuide} -${JSON.stringify(questionGuides)}`; + return ( + + ); } + return null; + }, [chat.value.length, index, isChatting, isLastChild, questionGuides]); - return ( - - ); - } - if (value.type === ChatItemValueTypeEnum.tool && value.tools) { - return ( - - {value.tools.map((tool) => { - const toolParams = (() => { - try { - return JSON.stringify(JSON.parse(tool.params), null, 2); - } catch (error) { - return tool.params; - } - })(); - const toolResponse = (() => { - try { - return JSON.stringify(JSON.parse(tool.response), null, 2); - } catch (error) { - return tool.response; - } - })(); + const Render = useMemo(() => { + if (value.type === ChatItemValueTypeEnum.text && value.text) { + let source = (value.text?.content || '').trim(); - return ( - - - - - - {tool.toolName} - - {isChatting && !tool.response && } - - - - {toolParams && toolParams !== '{}' && ( - + // First empty line + if (!source && chat.value.length > 1) return null; + + return ( + + ); + } + if (value.type === ChatItemValueTypeEnum.tool && value.tools) { + return ( + + {value.tools.map((tool) => { + const toolParams = (() => { + try { + return JSON.stringify(JSON.parse(tool.params), null, 2); + } catch (error) { + return tool.params; + } + })(); + const toolResponse = (() => { + try { + return JSON.stringify(JSON.parse(tool.response), null, 2); + } catch (error) { + return tool.response; + } + })(); + + return ( + + + + + + {tool.toolName} + + {isChatting && !tool.response && } + + + + {toolParams && toolParams !== '{}' && ( + + + + )} + {toolResponse && ( - - )} - {toolResponse && ( - - )} - - - - ); - })} - - ); - } - if ( - value.type === ChatItemValueTypeEnum.interactive && - value.interactive && - value.interactive.type === 'userSelect' - ) { - return ( - - {value.interactive.params.userSelectOptions?.map((option) => { - const selected = option.value === value.interactive?.params?.userSelectedVal; + )} + + + + ); + })} + + ); + } + if ( + value.type === ChatItemValueTypeEnum.interactive && + value.interactive && + value.interactive.type === 'userSelect' + ) { + return ( + <> + {value.interactive?.params?.description && ( + + )} + + {value.interactive.params.userSelectOptions?.map((option) => { + const selected = option.value === value.interactive?.params?.userSelectedVal; + + return ( + + ); + })} + + {/* Animation */} + {isLastChild && isChatting && index === chat.value.length - 1 && ( + + )} + + ); + } + }, [chat.value.length, chatHistories, index, isChatting, isLastChild, onSendMessage, value]); - return ( - - ); - })} - - ); - } - return null; + return ( + <> + {Render} + {RenderQuestionGuide} + + ); }; export default React.memo(AIResponseBox); diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index ea10fe620910..b5c8a19c9619 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Box, Flex, BoxProps, useDisclosure, HStack } from '@chakra-ui/react'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d'; import { useTranslation } from 'next-i18next'; @@ -29,114 +29,472 @@ type sideTabItemType = { children: sideTabItemType[]; }; -function RowRender({ - children, - mb, - label, - ...props -}: { children: React.ReactNode; label: string } & BoxProps) { - return ( - - - {label}: - - - {children} - - - ); -} -function Row({ - label, - value, - rawDom +/* Per response value */ +export const WholeResponseContent = ({ + activeModule, + hideTabs, + showDetail }: { - label: string; - value?: string | number | boolean | object; - rawDom?: React.ReactNode; -}) { - const val = value || rawDom; - const isObject = typeof value === 'object'; + activeModule: ChatHistoryItemResType; + hideTabs?: boolean; + showDetail: boolean; +}) => { + const { t } = useTranslation(); - const formatValue = useMemo(() => { - if (isObject) { - return `~~~json\n${JSON.stringify(value, null, 2)}`; + // Auto scroll to top + const ContentRef = useRef(null); + useEffect(() => { + if (ContentRef.current) { + ContentRef.current.scrollTop = 0; } - return `${value}`; - }, [isObject, value]); - - if (rawDom) { - return ( - - {rawDom} - - ); - } + }, [activeModule]); + + const RowRender = useCallback( + ({ + children, + mb, + label, + ...props + }: { children: React.ReactNode; label: string } & BoxProps) => { + return ( + + + {label}: + + + {children} + + + ); + }, + [] + ); + const Row = useCallback( + ({ + label, + value, + rawDom + }: { + label: string; + value?: string | number | boolean | object; + rawDom?: React.ReactNode; + }) => { + const val = value || rawDom; + const isObject = typeof value === 'object'; + + const formatValue = useMemo(() => { + if (isObject) { + return `~~~json\n${JSON.stringify(value, null, 2)}`; + } + return `${value}`; + }, [isObject, value]); + + if (rawDom) { + return ( + + {rawDom} + + ); + } - if (val === undefined || val === '' || val === 'undefined') return null; + if (val === undefined || val === '' || val === 'undefined') return null; + + return ( + + + + ); + }, + [RowRender] + ); - return ( - - - - ); -} + {/* common info */} + <> + + {activeModule?.totalPoints !== undefined && ( + + )} + + + + -const WholeResponseModal = ({ - showDetail, - onClose, - dataId + + + + + + {/* ai chat */} + <> + + + + {activeModule.historyPreview?.map((item, i) => ( + + {item.obj} + {item.value} + + ))} + + ) : ( + '' + ) + } + /> + + {/* dataset search */} + <> + {activeModule?.searchMode && ( + + )} + + + + + + {activeModule.quoteList && activeModule.quoteList.length > 0 && ( + } + /> + )} + + {/* classify question */} + <> + + { + if (!activeModule?.cqList) return ''; + return activeModule.cqList.map((item) => `* ${item.value}`).join('\n'); + })()} + /> + + {/* if-else */} + <> + + + {/* extract */} + <> + + + + {/* http */} + <> + + + + + + {/* plugin */} + <> + + + {/* text output */} + + {/* code */} + <> + + + + + + {/* read files */} + <> + {activeModule?.readFiles && activeModule?.readFiles.length > 0 && ( + + {activeModule?.readFiles.map((file, i) => ( + window.open(file.url) + } + : {})} + > + + {file.name} + + ))} + + } + /> + )} + + + + {/* user select */} + + + {/* update var */} + + + ) : null; +}; + +/* Side tab: With and without children */ +const SideTabItem = ({ + sideBarItem, + onChange, + value, + index }: { - showDetail: boolean; - onClose: () => void; - dataId: string; + sideBarItem: sideTabItemType; + onChange: (id: string) => void; + value: string; + index: number; }) => { const { t } = useTranslation(); - const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v); - const { loading: isLoading, data: response } = useRequest2( - () => getHistoryResponseData({ dataId }), - { - manual: false - } + if (!sideBarItem) return null; + + const AccordionSideTabItem = useCallback( + ({ + sideBarItem, + onChange, + value, + index + }: { + sideBarItem: sideTabItemType; + onChange: (id: string) => void; + value: string; + index: number; + }) => { + const { isOpen: isShowAccordion, onToggle: onToggleShowAccordion } = useDisclosure({ + defaultIsOpen: false + }); + return ( + <> + + + { + e.stopPropagation(); + onToggleShowAccordion(); + }} + _hover={{ color: 'primary.600', cursor: 'pointer' }} + /> + + + {isShowAccordion && ( + + {sideBarItem.children.map((item) => ( + + ))} + + )} + + ); + }, + [] ); - return ( - - {t('common:core.chat.response.Complete Response')} - + const NormalSideTabItem = useCallback( + ({ + sideBarItem, + onChange, + value, + index, + children + }: { + sideBarItem: sideTabItemType; + onChange: (id: string) => void; + value: string; + index: number; + children?: React.ReactNode; + }) => { + const leftIndex = index > 3 ? 3 : index; + return ( + { + onChange(sideBarItem.id); + }} + background={value === sideBarItem.id ? 'myGray.100' : ''} + _hover={{ background: 'myGray.100' }} + p={2} + width={'100%'} + cursor={'pointer'} + pl={leftIndex === 0 ? '0.5rem' : `${1.5 * leftIndex + 0.5}rem`} + borderRadius={'md'} + position={'relative'} + > + sideBarItem.moduleType === template.flowNodeType + )?.avatar + } + alt={''} + w={'1.5rem'} + h={'1.5rem'} + borderRadius={'sm'} + /> + + + {t(sideBarItem.moduleName as any)} + + + {t(sideBarItem.runningTime as any) + 's'} + + + + {children} + - } - > - {!!response?.length ? ( - - ) : ( - - )} - + ); + }, + [t] ); -}; -export default WholeResponseModal; + return sideBarItem.children.length !== 0 ? ( + <> + + + + + ) : ( + + ); +}; +/* Modal main container */ export const ResponseBox = React.memo(function ResponseBox({ response, showDetail, @@ -151,14 +509,35 @@ export const ResponseBox = React.memo(function ResponseBox({ const { t } = useTranslation(); const { isPc } = useSystem(); - const flattedResponse = useMemo( - () => - flattenArray(response).map((item) => ({ - ...item, - id: item.id ?? item.nodeId - })), - [response] - ); + const flattedResponse = useMemo(() => { + /* Flat response */ + function flattenArray(arr: ChatHistoryItemResType[]) { + const result: ChatHistoryItemResType[] = []; + + function helper(currentArray: ChatHistoryItemResType[]) { + currentArray.forEach((item) => { + if (item && typeof item === 'object') { + result.push(item); + + if (Array.isArray(item.toolDetail)) { + helper(item.toolDetail); + } + if (Array.isArray(item.pluginDetail)) { + helper(item.pluginDetail); + } + } + }); + } + + helper(arr); + return result; + } + + return flattenArray(response).map((item) => ({ + ...item, + id: item.id ?? item.nodeId + })); + }, [response]); const [currentNodeId, setCurrentNodeId] = useState( flattedResponse[0]?.id ?? flattedResponse[0]?.nodeId ?? '' ); @@ -169,6 +548,25 @@ export const ResponseBox = React.memo(function ResponseBox({ ); const sliderResponseList: sideTabItemType[] = useMemo(() => { + /* Format response data to slider data */ + function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[] { + return res.map((item) => { + let children: sideTabItemType[] = []; + if (!!(item?.toolDetail || item?.pluginDetail)) { + if (item?.toolDetail) children.push(...pretreatmentResponse(item?.toolDetail)); + if (item?.pluginDetail) children.push(...pretreatmentResponse(item?.pluginDetail)); + } + + return { + moduleLogo: item.moduleLogo, + moduleName: item.moduleName, + runningTime: item.runningTime, + moduleType: item.moduleType, + id: item.id ?? item.nodeId, + children + }; + }); + } return pretreatmentResponse(response); }, [response]); @@ -178,6 +576,37 @@ export const ResponseBox = React.memo(function ResponseBox({ onClose: onCloseMobileModal } = useDisclosure(); + const WholeResponseSideTab = useCallback( + ({ + response, + value, + onChange, + isMobile = false + }: { + response: sideTabItemType[]; + value: string; + onChange: (index: string) => void; + isMobile?: boolean; + }) => { + return ( + <> + {response.map((item) => ( + + + + ))} + + ); + }, + [] + ); + return ( <> {isPc && !useMobile ? ( @@ -191,7 +620,7 @@ export const ResponseBox = React.memo(function ResponseBox({ /> - + - + void; + dataId: string; }) => { const { t } = useTranslation(); - const { workflowT } = useI18n(); - return ( - <> - {activeModule && ( - - {/* common info */} - <> - - {activeModule?.totalPoints !== undefined && ( - - )} - - - - - - - - - - - {/* ai chat */} - <> - - - - {activeModule.historyPreview?.map((item, i) => ( - - {item.obj} - {item.value} - - ))} - - ) : ( - '' - ) - } - /> - - {/* dataset search */} - <> - {activeModule?.searchMode && ( - - )} - - - - - - {activeModule.quoteList && activeModule.quoteList.length > 0 && ( - } - /> - )} - - {/* classify question */} - <> - - { - if (!activeModule?.cqList) return ''; - return activeModule.cqList.map((item) => `* ${item.value}`).join('\n'); - })()} - /> - - {/* if-else */} - <> - - - {/* extract */} - <> - - - - {/* http */} - <> - - - - - - {/* plugin */} - <> - - - {/* text output */} - - {/* code */} - <> - - - - - - {/* read files */} - <> - {activeModule?.readFiles && activeModule?.readFiles.length > 0 && ( - - {activeModule?.readFiles.map((file, i) => ( - window.open(file.url) - } - : {})} - > - - {file.name} - - ))} - - } - /> - )} - - - - {/* user select */} - - - {/* update var */} - - - )} - - ); -}; - -const WholeResponseSideTab = ({ - response, - value, - onChange, - isMobile = false -}: { - response: sideTabItemType[]; - value: string; - onChange: (index: string) => void; - isMobile?: boolean; -}) => { - return ( - <> - {response.map((item) => ( - - - - ))} - - ); -}; - -const AccordionSideTabItem = ({ - sideBarItem, - onChange, - value, - index -}: { - sideBarItem: sideTabItemType; - onChange: (id: string) => void; - value: string; - index: number; -}) => { - const { isOpen: isShowAccordion, onToggle: onToggleShowAccordion } = useDisclosure({ - defaultIsOpen: false - }); - return ( - <> - - - { - e.stopPropagation(); - onToggleShowAccordion(); - }} - _hover={{ color: 'primary.600', cursor: 'pointer' }} - /> - - - {isShowAccordion && ( - - {sideBarItem.children.map((item) => ( - - ))} - - )} - + const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v); + const { loading: isLoading, data: response } = useRequest2( + () => getHistoryResponseData({ dataId }), + { + manual: false + } ); -}; -const NormalSideTabItem = ({ - sideBarItem, - onChange, - value, - index, - children -}: { - sideBarItem: sideTabItemType; - onChange: (id: string) => void; - value: string; - index: number; - children?: React.ReactNode; -}) => { - const { t } = useTranslation(); - const leftIndex = index > 3 ? 3 : index; return ( - { - onChange(sideBarItem.id); - }} - background={value === sideBarItem.id ? 'myGray.100' : ''} - _hover={{ background: 'myGray.100' }} - p={2} - width={'100%'} - cursor={'pointer'} - pl={leftIndex === 0 ? '0.5rem' : `${1.5 * leftIndex + 0.5}rem`} - borderRadius={'md'} - position={'relative'} + + {t('common:core.chat.response.Complete Response')} + + + } > - sideBarItem.moduleType === template.flowNodeType) - ?.avatar - } - alt={''} - w={'1.5rem'} - h={'1.5rem'} - borderRadius={'sm'} - /> - - - {t(sideBarItem.moduleName as any)} - - - {t(sideBarItem.runningTime as any) + 's'} - - - - {children} - - - ); -}; - -const SideTabItem = ({ - sideBarItem, - onChange, - value, - index -}: { - sideBarItem: sideTabItemType; - onChange: (id: string) => void; - value: string; - index: number; -}) => { - if (!sideBarItem) return null; - return sideBarItem.children.length !== 0 ? ( - <> - - - - - ) : ( - + {!!response?.length ? ( + + ) : ( + + )} + ); }; -/* Format response data to slider data */ -function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[] { - return res.map((item) => { - let children: sideTabItemType[] = []; - if (!!(item?.toolDetail || item?.pluginDetail)) { - if (item?.toolDetail) children.push(...pretreatmentResponse(item?.toolDetail)); - if (item?.pluginDetail) children.push(...pretreatmentResponse(item?.pluginDetail)); - } - - return { - moduleLogo: item.moduleLogo, - moduleName: item.moduleName, - runningTime: item.runningTime, - moduleType: item.moduleType, - id: item.id ?? item.nodeId, - children - }; - }); -} - -/* Flat response */ -function flattenArray(arr: ChatHistoryItemResType[]) { - const result: ChatHistoryItemResType[] = []; - - function helper(currentArray: ChatHistoryItemResType[]) { - currentArray.forEach((item) => { - if (item && typeof item === 'object') { - result.push(item); - - if (Array.isArray(item.toolDetail)) { - helper(item.toolDetail); - } - if (Array.isArray(item.pluginDetail)) { - helper(item.pluginDetail); - } - } - }); - } - - helper(arr); - return result; -} +export default WholeResponseModal; diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index ad029650d81f..59092c8c030f 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -59,6 +59,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { try { const chatMessages = GPTMessages2Chats(messages); + // console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length); + const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined; /* user auth */ diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index b022a0bb119d..48b4685f6f8c 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -17,11 +17,12 @@ import { getMaxHistoryLimitFromNodes, initWorkflowEdgeStatus, storeNodes2RuntimeNodes, - textAdaptGptResponse + textAdaptGptResponse, + getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils'; import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { getChatItems } from '@fastgpt/service/core/chat/controller'; -import { saveChat } from '@fastgpt/service/core/chat/saveChat'; +import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; import { pushChatUsage } from '@/service/support/wallet/usage/push'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; @@ -45,7 +46,7 @@ import { AuthOutLinkChatProps } from '@fastgpt/global/support/outLink/api'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; -import { UserChatItemType } from '@fastgpt/global/core/chat/type'; +import { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { NextAPI } from '@/service/middleware/entry'; @@ -210,9 +211,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { MongoChat.findOne({ appId: app._id, chatId }, 'source variableList variables') ]); - // Get chat histories - const newHistories = concatHistories(histories, chatMessages); - // Get store variables(Api variable precedence) if (chatDetail?.variables) { variables = { @@ -221,6 +219,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }; } + // Get chat histories + const newHistories = concatHistories(histories, chatMessages); + // Get runtimeNodes let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); if (isPlugin) { @@ -286,36 +287,51 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return ChatSourceEnum.online; })(); + const isInteractiveRequest = !!getLastInteractiveValue(histories); + const { text: userSelectedVal } = chatValue2RuntimePrompt(userQuestion.value); + const newTitle = isPlugin ? variables.cTime ?? getSystemTime(user.timezone) : getChatTitleFromChatMessage(userQuestion); - await saveChat({ - chatId, - appId: app._id, - teamId, - tmbId: tmbId, - nodes, - appChatConfig: chatConfig, - variables: newVariables, - isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time - newTitle, - shareId, - outLinkUid: outLinkUserId, - source, - content: [ - userQuestion, - { - dataId: responseChatItemId, - obj: ChatRoleEnum.AI, - value: assistantResponses, - [DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses + const aiResponse: AIChatItemType & { dataId?: string } = { + dataId: responseChatItemId, + obj: ChatRoleEnum.AI, + value: assistantResponses, + [DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses + }; + + if (isInteractiveRequest) { + await updateInteractiveChat({ + chatId, + appId: app._id, + teamId, + tmbId: tmbId, + userSelectedVal, + aiResponse, + newVariables, + newTitle + }); + } else { + await saveChat({ + chatId, + appId: app._id, + teamId, + tmbId: tmbId, + nodes, + appChatConfig: chatConfig, + variables: newVariables, + isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time + newTitle, + shareId, + outLinkUid: outLinkUserId, + source, + content: [userQuestion, aiResponse], + metadata: { + originIp } - ], - metadata: { - originIp - } - }); + }); + } } addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx index 0b57fee04721..7fdbed40f749 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -466,7 +466,21 @@ const RenderList = React.memo(function RenderList({ flowNodeType: templateNode.flowNodeType, pluginId: templateNode.pluginId }), - intro: t(templateNode.intro as any) + intro: t(templateNode.intro as any), + inputs: templateNode.inputs.map((input) => ({ + ...input, + valueDesc: t(input.valueDesc as any), + label: t(input.label as any), + description: t(input.description as any), + debugLabel: t(input.debugLabel as any), + toolDescription: t(input.toolDescription as any) + })), + outputs: templateNode.outputs.map((output) => ({ + ...output, + valueDesc: t(output.valueDesc as any), + label: t(output.label as any), + description: t(output.description as any) + })) }, position: { x: mouseX, y: mouseY - 20 }, selected: true diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts index 43646d90dfd5..21d4abeafb69 100644 --- a/projects/app/src/web/core/app/utils.ts +++ b/projects/app/src/web/core/app/utils.ts @@ -26,6 +26,7 @@ import { AiChatModule } from '@fastgpt/global/core/workflow/template/system/aiCh import { DatasetSearchModule } from '@fastgpt/global/core/workflow/template/system/datasetSearch'; import { ReadFilesNodes } from '@fastgpt/global/core/workflow/template/system/readFiles'; import { i18nT } from '@fastgpt/web/i18n/utils'; +import { Input_Template_UserChatInput } from '@fastgpt/global/core/workflow/template/input'; type WorkflowType = { nodes: StoreNodeItemType[]; @@ -259,12 +260,8 @@ export function form2AppWorkflow( value: formData.dataset.datasetSearchExtensionBg }, { - key: 'userChatInput', - renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], - valueType: WorkflowIOValueTypeEnum.string, - label: '用户问题', - required: true, - toolDescription: '需要检索的内容', + ...Input_Template_UserChatInput, + toolDescription: i18nT('workflow:content_to_search'), value: question } ], @@ -503,6 +500,18 @@ export function form2AppWorkflow( ] }; + // Add t + config.nodes.forEach((node) => { + node.name = t(node.name); + node.intro = t(node.intro); + + node.inputs.forEach((input) => { + input.label = t(input.label); + input.description = t(input.description); + input.toolDescription = t(input.toolDescription); + }); + }); + return config; }