Commit 64708ea4 by Archer Committed by GitHub

Update userselect ux (#2610)

* perf: user select ux and api

* perf: http variables replace code

* perf: http variables replace code

* perf: chat box question guide adapt interactive

* remove comment
parent 85a11d08
...@@ -71,16 +71,17 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4810' \ ...@@ -71,16 +71,17 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4810' \
22. 优化 - 知识库详情页 UI。 22. 优化 - 知识库详情页 UI。
23. 优化 - 支持无网络配置情况下运行。 23. 优化 - 支持无网络配置情况下运行。
24. 优化 - 部分全局变量,增加数据类型约束。 24. 优化 - 部分全局变量,增加数据类型约束。
25. 修复 - 全局变量 key 可能重复。 25. 优化 - 查看工作流详情,切换 tab 时,自动滚动到顶部。
26. 修复 - Prompt 模式调用工具,stream=false 模式下,会携带 0: 开头标记。 26. 修复 - 全局变量 key 可能重复。
27. 修复 - 对话日志鉴权问题:仅为 APP 管理员的用户,无法查看对话日志详情。 27. 修复 - Prompt 模式调用工具,stream=false 模式下,会携带 0: 开头标记。
28. 修复 - 选择 Milvus 部署时,无法导出知识库。 28. 修复 - 对话日志鉴权问题:仅为 APP 管理员的用户,无法查看对话日志详情。
29. 修复 - 创建 APP 副本,无法复制系统配置。 29. 修复 - 选择 Milvus 部署时,无法导出知识库。
30. 修复 - 图片识别模式下,自动解析图片链接正则不够严谨问题。 30. 修复 - 创建 APP 副本,无法复制系统配置。
31. 修复 - 内容提取的数据类型与输出数据类型未一致。 31. 修复 - 图片识别模式下,自动解析图片链接正则不够严谨问题。
32. 修复 - 工作流运行时间统计错误。 32. 修复 - 内容提取的数据类型与输出数据类型未一致。
33. 修复 - stream 模式下,工具调用有可能出现 undefined 33. 修复 - 工作流运行时间统计错误。
34. 修复 - 全局变量在 API 中无法持久化。 34. 修复 - stream 模式下,工具调用有可能出现 undefined
35. 修复 - OpenAPI,detail=false模式下,不应该返回 tool 调用结果,仅返回文字。(可解决 cow 不适配问题) 35. 修复 - 全局变量在 API 中无法持久化。
36. 修复 - 知识库标签重复加载。 36. 修复 - OpenAPI,detail=false模式下,不应该返回 tool 调用结果,仅返回文字。(可解决 cow 不适配问题)
37. 修复 - Debug 模式下,循环调用边问题。 37. 修复 - 知识库标签重复加载。
38. 修复 - Debug 模式下,循环调用边问题。
...@@ -118,7 +118,7 @@ export const chats2GPTMessages = ({ ...@@ -118,7 +118,7 @@ export const chats2GPTMessages = ({
tool_calls tool_calls
}) })
.concat(toolResponse); .concat(toolResponse);
} else if (value.text) { } else if (value.text?.content) {
results.push({ results.push({
dataId, dataId,
role: ChatCompletionRequestMessageRoleEnum.Assistant, role: ChatCompletionRequestMessageRoleEnum.Assistant,
...@@ -142,7 +142,7 @@ export const GPTMessages2Chats = ( ...@@ -142,7 +142,7 @@ export const GPTMessages2Chats = (
messages: ChatCompletionMessageParam[], messages: ChatCompletionMessageParam[],
reserveTool = true reserveTool = true
): ChatItemType[] => { ): ChatItemType[] => {
return messages const chatMessages = messages
.map((item) => { .map((item) => {
const value: ChatItemType['value'] = []; const value: ChatItemType['value'] = [];
const obj = GPT2Chat[item.role]; const obj = GPT2Chat[item.role];
...@@ -288,6 +288,22 @@ export const GPTMessages2Chats = ( ...@@ -288,6 +288,22 @@ export const GPTMessages2Chats = (
} as ChatItemType; } as ChatItemType;
}) })
.filter((item) => item.value.length > 0); .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 => { export const chatValue2RuntimePrompt = (value: ChatItemValueItemType[]): RuntimeUserPromptType => {
......
...@@ -22,6 +22,7 @@ export enum DispatchNodeResponseKeyEnum { ...@@ -22,6 +22,7 @@ export enum DispatchNodeResponseKeyEnum {
childrenResponses = 'childrenResponses', // Some nodes make recursive calls that need to be returned 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 toolResponses = 'toolResponses', // The result is passed back to the tool node for use
assistantResponses = 'assistantResponses', // assistant response assistantResponses = 'assistantResponses', // assistant response
rewriteHistories = 'rewriteHistories', // If have the response, workflow histories will be rewrite
interactive = 'INTERACTIVE' // is interactive interactive = 'INTERACTIVE' // is interactive
} }
......
...@@ -2,9 +2,9 @@ import { ChatNodeUsageType } from '../../../support/wallet/bill/type'; ...@@ -2,9 +2,9 @@ import { ChatNodeUsageType } from '../../../support/wallet/bill/type';
import { import {
ChatItemType, ChatItemType,
UserChatItemValueItemType, UserChatItemValueItemType,
ChatItemValueItemType,
ToolRunResponseItemType, ToolRunResponseItemType,
NodeOutputItemType NodeOutputItemType,
AIChatItemValueItemType
} from '../../chat/type'; } from '../../chat/type';
import { FlowNodeInputItemType, FlowNodeOutputItemType } from '../type/io.d'; import { FlowNodeInputItemType, FlowNodeOutputItemType } from '../type/io.d';
import { StoreNodeItemType } from '../type/node'; import { StoreNodeItemType } from '../type/node';
...@@ -173,13 +173,14 @@ export type DispatchNodeResponseType = { ...@@ -173,13 +173,14 @@ export type DispatchNodeResponseType = {
updateVarResult?: any[]; updateVarResult?: any[];
}; };
export type DispatchNodeResultType<T> = { export type DispatchNodeResultType<T = {}> = {
[DispatchNodeResponseKeyEnum.skipHandleId]?: string[]; // skip some edge handle id [DispatchNodeResponseKeyEnum.skipHandleId]?: string[]; // skip some edge handle id
[DispatchNodeResponseKeyEnum.nodeResponse]?: DispatchNodeResponseType; // The node response detail [DispatchNodeResponseKeyEnum.nodeResponse]?: DispatchNodeResponseType; // The node response detail
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]?: ChatNodeUsageType[]; // Node total usage [DispatchNodeResponseKeyEnum.nodeDispatchUsages]?: ChatNodeUsageType[]; // Node total usage
[DispatchNodeResponseKeyEnum.childrenResponses]?: DispatchNodeResultType[]; // Children node response [DispatchNodeResponseKeyEnum.childrenResponses]?: DispatchNodeResultType[]; // Children node response
[DispatchNodeResponseKeyEnum.toolResponses]?: ToolRunResponseItemType; // Tool 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; } & T;
/* Single node props */ /* Single node props */
......
...@@ -27,16 +27,24 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number ...@@ -27,16 +27,24 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
return limit * 2; 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[]) => { export const getLastInteractiveValue = (histories: ChatItemType[]) => {
const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI); const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI);
if (lastAIMessage) { if (lastAIMessage) {
const interactiveValue = lastAIMessage.value.find( const lastValue = lastAIMessage.value[lastAIMessage.value.length - 1];
(v) => v.type === ChatItemValueTypeEnum.interactive
); if (
lastValue &&
if (interactiveValue && 'interactive' in interactiveValue) { lastValue.type === ChatItemValueTypeEnum.interactive &&
return interactiveValue.interactive; !!lastValue.interactive
) {
return lastValue.interactive;
} }
} }
......
...@@ -15,7 +15,7 @@ type InteractiveBasicType = { ...@@ -15,7 +15,7 @@ type InteractiveBasicType = {
type UserSelectInteractive = { type UserSelectInteractive = {
type: 'userSelect'; type: 'userSelect';
params: { params: {
// description: string; description: string;
userSelectOptions: UserSelectOptionItemType[]; userSelectOptions: UserSelectOptionItemType[];
userSelectedVal?: string; userSelectedVal?: string;
}; };
......
...@@ -3,7 +3,7 @@ import { getAIApi } from '../config'; ...@@ -3,7 +3,7 @@ import { getAIApi } from '../config';
import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; import { countGptMessagesTokens } from '../../../common/string/tiktoken/index';
import { loadRequestMessages } from '../../chat/utils'; 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({ export async function createQuestionGuide({
messages, messages,
...@@ -19,6 +19,7 @@ export async function createQuestionGuide({ ...@@ -19,6 +19,7 @@ export async function createQuestionGuide({
content: Prompt_QuestionGuide content: Prompt_QuestionGuide
} }
]; ];
const ai = getAIApi({ const ai = getAIApi({
timeout: 480000 timeout: 480000
}); });
......
import type { ChatItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema'; import { MongoChatItem } from './chatItemSchema';
import { addLog } from '../../common/system/log'; 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 { delFileByFileIdList, getGFSCollection } from '../../common/file/gridfs/controller';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { MongoChat } from './chatSchema'; import { MongoChat } from './chatSchema';
...@@ -80,52 +80,6 @@ export const addCustomFeedbacks = async ({ ...@@ -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 Delete chat files
1. ChatId: Delete one chat files 1. ChatId: Delete one chat files
......
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d'; import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d';
import { MongoApp } from '../app/schema'; 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 { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema'; import { MongoChat } from './chatSchema';
import { addLog } from '../../common/system/log'; import { addLog } from '../../common/system/log';
...@@ -111,3 +115,85 @@ export async function saveChat({ ...@@ -111,3 +115,85 @@ export async function saveChat({
addLog.error(`update chat history error`, error); 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<string, any>;
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
}
);
});
};
...@@ -211,11 +211,40 @@ export const loadRequestMessages = async ({ ...@@ -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; return item;
}) })
.filter(Boolean) as ChatCompletionMessageParam[]; .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) { if (messages.length === 0) {
return Promise.reject('core.chat.error.Messages empty'); return Promise.reject('core.chat.error.Messages empty');
...@@ -245,11 +274,22 @@ export const loadRequestMessages = async ({ ...@@ -245,11 +274,22 @@ export const loadRequestMessages = async ({
...item, ...item,
content: await parseUserContent(item.content) 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 { } else {
return item; return item;
} }
}) })
)) as ChatCompletionMessageParam[]; )) as ChatCompletionMessageParam[];
return clearInvalidMessages(loadMessages) as SdkChatCompletionMessageParam[]; return mergeConsecutiveMessages(
clearInvalidMessages(loadMessages)
) as SdkChatCompletionMessageParam[];
}; };
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { import type {
ChatDispatchProps, ChatDispatchProps,
DispatchNodeResultType,
ModuleDispatchProps, ModuleDispatchProps,
SystemVariablesType SystemVariablesType
} from '@fastgpt/global/core/workflow/runtime/type'; } from '@fastgpt/global/core/workflow/runtime/type';
...@@ -145,14 +146,15 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons ...@@ -145,14 +146,15 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
responseData, responseData,
nodeDispatchUsages, nodeDispatchUsages,
toolResponses, toolResponses,
assistantResponses assistantResponses,
}: { rewriteHistories
[NodeOutputKeyEnum.answerText]?: string; }: Omit<
[DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType; DispatchNodeResultType<{
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]?: ChatNodeUsageType[]; [NodeOutputKeyEnum.answerText]?: string;
[DispatchNodeResponseKeyEnum.toolResponses]?: ToolRunResponseItemType; [DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType;
[DispatchNodeResponseKeyEnum.assistantResponses]?: AIChatItemValueItemType[]; // tool module, save the response value }>,
} 'nodeResponse'
>
) { ) {
if (responseData) { if (responseData) {
chatResponses.push(responseData); chatResponses.push(responseData);
...@@ -182,6 +184,10 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons ...@@ -182,6 +184,10 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}); });
} }
} }
if (rewriteHistories) {
histories = rewriteHistories;
}
} }
/* Pass the output of the node, to get next nodes and update edge status */ /* Pass the output of the node, to get next nodes and update edge status */
function nodeOutput( function nodeOutput(
......
...@@ -12,8 +12,6 @@ import type { ...@@ -12,8 +12,6 @@ import type {
UserSelectInteractive, UserSelectInteractive,
UserSelectOptionItemType UserSelectOptionItemType
} from '@fastgpt/global/core/workflow/template/system/userSelect/type'; } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
import { updateUserSelectedResult } from '../../../chat/controller';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
...@@ -30,6 +28,7 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon ...@@ -30,6 +28,7 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
const { const {
workflowStreamResponse, workflowStreamResponse,
runningAppInfo: { id: appId }, runningAppInfo: { id: appId },
histories,
chatId, chatId,
node: { nodeId, isEntry }, node: { nodeId, isEntry },
params: { description, userSelectOptions }, params: { description, userSelectOptions },
...@@ -38,21 +37,11 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon ...@@ -38,21 +37,11 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
// Interactive node is not the entry node, return interactive result // Interactive node is not the entry node, return interactive result
if (!isEntry) { if (!isEntry) {
const answerText = description ? `\n${description}` : undefined;
if (answerText) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
text: answerText
})
});
}
return { return {
[NodeOutputKeyEnum.answerText]: answerText,
[DispatchNodeResponseKeyEnum.interactive]: { [DispatchNodeResponseKeyEnum.interactive]: {
type: 'userSelect', type: 'userSelect',
params: { params: {
description,
userSelectOptions userSelectOptions
} }
} }
...@@ -70,14 +59,8 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon ...@@ -70,14 +59,8 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
}; };
} }
// Update db
updateUserSelectedResult({
appId,
chatId,
userSelectedVal
});
return { return {
[DispatchNodeResponseKeyEnum.rewriteHistories]: histories.slice(0, -2), // Removes the current session record as the history of subsequent nodes
[DispatchNodeResponseKeyEnum.skipHandleId]: userSelectOptions [DispatchNodeResponseKeyEnum.skipHandleId]: userSelectOptions
.filter((item) => item.value !== userSelectedVal) .filter((item) => item.value !== userSelectedVal)
.map((item: any) => getHandleId(nodeId, 'source', item.key)), .map((item: any) => getHandleId(nodeId, 'source', item.key)),
......
...@@ -99,6 +99,18 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -99,6 +99,18 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
}; };
httpReqUrl = replaceVariable(httpReqUrl, allVariables); httpReqUrl = replaceVariable(httpReqUrl, allVariables);
const replaceStringVariables = (text: string) => {
return replaceVariable(
replaceEditorVariable({
text,
nodes: runtimeNodes,
variables: allVariables,
runningNode: node
}),
allVariables
);
};
// parse header // parse header
const headers = await (() => { const headers = await (() => {
try { try {
...@@ -110,24 +122,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -110,24 +122,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
if (!httpHeader || httpHeader.length === 0) return {}; if (!httpHeader || httpHeader.length === 0) return {};
// array // array
return httpHeader.reduce((acc: Record<string, string>, item) => { return httpHeader.reduce((acc: Record<string, string>, item) => {
const key = replaceVariable( const key = replaceStringVariables(item.key);
replaceEditorVariable({ const value = replaceStringVariables(item.value);
text: item.key,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
);
const value = replaceVariable(
replaceEditorVariable({
text: item.value,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
);
acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string); acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string);
return acc; return acc;
}, {}); }, {});
...@@ -137,24 +133,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -137,24 +133,8 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
})(); })();
const params = httpParams.reduce((acc: Record<string, string>, item) => { const params = httpParams.reduce((acc: Record<string, string>, item) => {
const key = replaceVariable( const key = replaceStringVariables(item.key);
replaceEditorVariable({ const value = replaceStringVariables(item.value);
text: item.key,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
);
const value = replaceVariable(
replaceEditorVariable({
text: item.value,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
);
acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string); acc[key] = valueTypeFormat(value, WorkflowIOValueTypeEnum.string);
return acc; return acc;
}, {}); }, {});
...@@ -165,25 +145,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -165,25 +145,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
if (httpContentType === ContentTypes.formData) { if (httpContentType === ContentTypes.formData) {
if (!Array.isArray(httpFormBody)) return {}; if (!Array.isArray(httpFormBody)) return {};
httpFormBody = httpFormBody.map((item) => ({ httpFormBody = httpFormBody.map((item) => ({
key: replaceVariable( key: replaceStringVariables(item.key),
replaceEditorVariable({
text: item.key,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
),
type: item.type, type: item.type,
value: replaceVariable( value: replaceStringVariables(item.value)
replaceEditorVariable({
text: item.value,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
)
})); }));
const formData = new FormData(); const formData = new FormData();
for (const { key, value } of httpFormBody) { for (const { key, value } of httpFormBody) {
...@@ -194,25 +158,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -194,25 +158,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
if (httpContentType === ContentTypes.xWwwFormUrlencoded) { if (httpContentType === ContentTypes.xWwwFormUrlencoded) {
if (!Array.isArray(httpFormBody)) return {}; if (!Array.isArray(httpFormBody)) return {};
httpFormBody = httpFormBody.map((item) => ({ httpFormBody = httpFormBody.map((item) => ({
key: replaceVariable( key: replaceStringVariables(item.key),
replaceEditorVariable({
text: item.key,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
),
type: item.type, type: item.type,
value: replaceVariable( value: replaceStringVariables(item.value)
replaceEditorVariable({
text: item.value,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
)
})); }));
const urlSearchParams = new URLSearchParams(); const urlSearchParams = new URLSearchParams();
for (const { key, value } of httpFormBody) { for (const { key, value } of httpFormBody) {
...@@ -228,15 +176,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -228,15 +176,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
const removeSignJson = removeUndefinedSign(jsonParse); const removeSignJson = removeUndefinedSign(jsonParse);
return removeSignJson; return removeSignJson;
} }
httpJsonBody = replaceVariable( httpJsonBody = replaceStringVariables(httpJsonBody);
replaceEditorVariable({
text: httpJsonBody,
nodes: runtimeNodes,
variables,
runningNode: node
}),
allVariables
);
return httpJsonBody.replaceAll(UNDEFINED_SIGN, 'null'); return httpJsonBody.replaceAll(UNDEFINED_SIGN, 'null');
} catch (error) { } catch (error) {
console.log(error); console.log(error);
......
...@@ -45,7 +45,7 @@ import ChatBoxDivider from '../../Divider'; ...@@ -45,7 +45,7 @@ import ChatBoxDivider from '../../Divider';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { formatChatValue2InputType } from './utils'; import { checkIsInteractiveByHistories, formatChatValue2InputType } from './utils';
import { textareaMinH } from './constants'; import { textareaMinH } from './constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import ChatProvider, { ChatBoxContext, ChatProviderProps } from './Provider'; import ChatProvider, { ChatBoxContext, ChatProviderProps } from './Provider';
...@@ -156,15 +156,11 @@ const ChatBox = ( ...@@ -156,15 +156,11 @@ const ChatBox = (
isChatting isChatting
} = useContextSelector(ChatBoxContext, (v) => v); } = useContextSelector(ChatBoxContext, (v) => v);
const isInteractive = useMemo(() => { // Workflow running, there are user input or selection
const lastAIHistory = chatHistories[chatHistories.length - 1]; const isInteractive = useMemo(
if (!lastAIHistory) return false; () => checkIsInteractiveByHistories(chatHistories),
const lastAIMessage = lastAIHistory.value as AIChatItemValueItemType[]; [chatHistories]
const interactiveContent = lastAIMessage?.find( );
(item) => item.type === ChatItemValueTypeEnum.interactive
)?.interactive?.params;
return !!interactiveContent;
}, [chatHistories]);
// compute variable input is finish. // compute variable input is finish.
const chatForm = useForm<ChatBoxInputFormType>({ const chatForm = useForm<ChatBoxInputFormType>({
...@@ -343,16 +339,15 @@ const ChatBox = ( ...@@ -343,16 +339,15 @@ const ChatBox = (
// create question guide // create question guide
const createQuestionGuide = useCallback( const createQuestionGuide = useCallback(
async ({ history }: { history: ChatSiteItemType[] }) => { async ({ histories }: { histories: ChatSiteItemType[] }) => {
if (!questionGuide || chatController.current?.signal?.aborted) return; if (!questionGuide || chatController.current?.signal?.aborted) return;
try { try {
const abortSignal = new AbortController(); const abortSignal = new AbortController();
questionGuideController.current = abortSignal; questionGuideController.current = abortSignal;
const result = await postQuestionGuide( const result = await postQuestionGuide(
{ {
messages: chats2GPTMessages({ messages: history, reserveId: false }).slice(-6), messages: chats2GPTMessages({ messages: histories, reserveId: false }).slice(-6),
shareId, shareId,
outLinkUid, outLinkUid,
teamId, teamId,
...@@ -464,8 +459,9 @@ const ChatBox = ( ...@@ -464,8 +459,9 @@ const ChatBox = (
} }
]; ];
// 插入内容 const isInteractive = checkIsInteractiveByHistories(history);
setChatHistories(newChatList); // Update histories(Interactive input does not require new session rounds)
setChatHistories(isInteractive ? newChatList.slice(0, -2) : newChatList);
// 清空输入内容 // 清空输入内容
resetInputVal({}); resetInputVal({});
...@@ -476,6 +472,7 @@ const ChatBox = ( ...@@ -476,6 +472,7 @@ const ChatBox = (
const abortSignal = new AbortController(); const abortSignal = new AbortController();
chatController.current = abortSignal; chatController.current = abortSignal;
// Last empty ai message will be removed
const messages = chats2GPTMessages({ messages: newChatList, reserveId: true }); const messages = chats2GPTMessages({ messages: newChatList, reserveId: true });
const { const {
...@@ -483,7 +480,7 @@ const ChatBox = ( ...@@ -483,7 +480,7 @@ const ChatBox = (
responseText, responseText,
isNewChat = false isNewChat = false
} = await onStartChat({ } = await onStartChat({
messages: messages.slice(0, -1), messages: messages,
responseChatItemId: responseChatId, responseChatItemId: responseChatId,
controller: abortSignal, controller: abortSignal,
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }), generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
...@@ -492,35 +489,29 @@ const ChatBox = ( ...@@ -492,35 +489,29 @@ const ChatBox = (
isNewChatReplace.current = isNewChat; isNewChatReplace.current = isNewChat;
// set finish status // Set last chat finish status
setChatHistories((state) => let newChatHistories: ChatSiteItemType[] = [];
state.map((item, index) => { setChatHistories((state) => {
newChatHistories = state.map((item, index) => {
if (index !== state.length - 1) return item; if (index !== state.length - 1) return item;
return { return {
...item, ...item,
status: 'finish', 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(); generatingScroll();
isPc && TextareaDom.current?.focus(); isPc && TextareaDom.current?.focus();
}, 100); }, 100);
......
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 { ChatBoxInputType, UserInputFileItemType } from './type';
import { getFileIcon } from '@fastgpt/global/common/file/icon'; 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 => { export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
if (!value) { if (!value) {
...@@ -38,6 +42,20 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat ...@@ -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 = ( export const setUserSelectResultToHistories = (
histories: ChatSiteItemType[], histories: ChatSiteItemType[],
selectVal: string selectVal: string
...@@ -47,9 +65,14 @@ export const setUserSelectResultToHistories = ( ...@@ -47,9 +65,14 @@ export const setUserSelectResultToHistories = (
// @ts-ignore // @ts-ignore
return histories.map((item, i) => { return histories.map((item, i) => {
if (i !== histories.length - 1) return item; if (i !== histories.length - 1) return item;
item.value;
const value = item.value.map((val) => { const value = item.value.map((val, i) => {
if (val.type !== ChatItemValueTypeEnum.interactive || !val.interactive) return val; if (
i !== item.value.length - 1 ||
val.type !== ChatItemValueTypeEnum.interactive ||
!val.interactive
)
return val;
return { return {
...val, ...val,
...@@ -67,6 +90,7 @@ export const setUserSelectResultToHistories = ( ...@@ -67,6 +90,7 @@ export const setUserSelectResultToHistories = (
return { return {
...item, ...item,
status: ChatStatusEnum.loading,
value value
}; };
}); });
......
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
ChatSiteItemType, ChatSiteItemType,
UserChatItemValueItemType UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import React from 'react'; import React, { useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { SendPromptFnType } from '../ChatContainer/ChatBox/type'; import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
...@@ -45,144 +45,168 @@ const AIResponseBox = ({ ...@@ -45,144 +45,168 @@ const AIResponseBox = ({
}: props) => { }: props) => {
const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories); const chatHistories = useContextSelector(ChatBoxContext, (v) => v.chatHistories);
if (value.type === ChatItemValueTypeEnum.text && value.text) { // Question guide
let source = (value.text?.content || '').trim(); const RenderQuestionGuide = useMemo(() => {
// First empty line
if (!source && chat.value.length > 1) return null;
// computed question guide
if ( if (
isLastChild && isLastChild &&
!isChatting && !isChatting &&
questionGuides.length > 0 && questionGuides.length > 0 &&
index === chat.value.length - 1 index === chat.value.length - 1
) { ) {
source = `${source} return (
\`\`\`${CodeClassNameEnum.questionGuide} <Markdown
${JSON.stringify(questionGuides)}`; source={`\`\`\`${CodeClassNameEnum.questionGuide}
${JSON.stringify(questionGuides)}`}
/>
);
} }
return null;
}, [chat.value.length, index, isChatting, isLastChild, questionGuides]);
return ( const Render = useMemo(() => {
<Markdown if (value.type === ChatItemValueTypeEnum.text && value.text) {
source={source} let source = (value.text?.content || '').trim();
showAnimation={isLastChild && isChatting && index === chat.value.length - 1}
/>
);
}
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
return (
<Box>
{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 ( // First empty line
<Accordion key={tool.id} allowToggle> if (!source && chat.value.length > 1) return null;
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton return (
w={'auto'} <Markdown
bg={'white'} source={source}
borderRadius={'md'} showAnimation={isLastChild && isChatting && index === chat.value.length - 1}
borderWidth={'1px'} />
borderColor={'myGray.200'} );
boxShadow={'1'} }
pl={3} if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
pr={2.5} return (
_hover={{ <Box>
bg: 'auto' {value.tools.map((tool) => {
}} const toolParams = (() => {
> try {
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} /> return JSON.stringify(JSON.parse(tool.params), null, 2);
<Box mx={2} fontSize={'sm'} color={'myGray.900'}> } catch (error) {
{tool.toolName} return tool.params;
</Box> }
{isChatting && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />} })();
<AccordionIcon color={'myGray.600'} ml={5} /> const toolResponse = (() => {
</AccordionButton> try {
<AccordionPanel return JSON.stringify(JSON.parse(tool.response), null, 2);
py={0} } catch (error) {
px={0} return tool.response;
mt={3} }
borderRadius={'md'} })();
overflow={'hidden'}
maxH={'500px'} return (
overflowY={'auto'} <Accordion key={tool.id} allowToggle>
> <AccordionItem borderTop={'none'} borderBottom={'none'}>
{toolParams && toolParams !== '{}' && ( <AccordionButton
<Box mb={3}> w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
pl={3}
pr={2.5}
_hover={{
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} />
<Box mx={2} fontSize={'sm'} color={'myGray.900'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
px={0}
mt={3}
borderRadius={'md'}
overflow={'hidden'}
maxH={'500px'}
overflowY={'auto'}
>
{toolParams && toolParams !== '{}' && (
<Box mb={3}>
<Markdown
source={`~~~json#Input
${toolParams}`}
/>
</Box>
)}
{toolResponse && (
<Markdown <Markdown
source={`~~~json#Input source={`~~~json#Response
${toolParams}`} ${toolResponse}`}
/> />
</Box> )}
)} </AccordionPanel>
{toolResponse && ( </AccordionItem>
<Markdown </Accordion>
source={`~~~json#Response );
${toolResponse}`} })}
/> </Box>
)} );
</AccordionPanel> }
</AccordionItem> if (
</Accordion> value.type === ChatItemValueTypeEnum.interactive &&
); value.interactive &&
})} value.interactive.type === 'userSelect'
</Box> ) {
); return (
} <>
if ( {value.interactive?.params?.description && (
value.type === ChatItemValueTypeEnum.interactive && <Markdown source={value.interactive.params.description} />
value.interactive && )}
value.interactive.type === 'userSelect' <Flex flexDirection={'column'} gap={2} w={'250px'}>
) { {value.interactive.params.userSelectOptions?.map((option) => {
return ( const selected = option.value === value.interactive?.params?.userSelectedVal;
<Flex flexDirection={'column'} gap={2} minW={'200px'} maxW={'250px'}>
{value.interactive.params.userSelectOptions?.map((option) => { return (
const selected = option.value === value.interactive?.params?.userSelectedVal; <Button
key={option.key}
variant={'whitePrimary'}
whiteSpace={'pre-wrap'}
isDisabled={value.interactive?.params?.userSelectedVal !== undefined}
{...(selected
? {
_disabled: {
cursor: 'default',
borderColor: 'primary.300',
bg: 'primary.50 !important',
color: 'primary.600'
}
}
: {})}
onClick={() => {
onSendMessage?.({
text: option.value,
history: setUserSelectResultToHistories(chatHistories, option.value)
});
}}
>
{option.value}
</Button>
);
})}
</Flex>
{/* Animation */}
{isLastChild && isChatting && index === chat.value.length - 1 && (
<Markdown source={''} showAnimation />
)}
</>
);
}
}, [chat.value.length, chatHistories, index, isChatting, isLastChild, onSendMessage, value]);
return ( return (
<Button <>
key={option.key} {Render}
variant={'whitePrimary'} {RenderQuestionGuide}
isDisabled={!isLastChild && value.interactive?.params?.userSelectedVal !== undefined} </>
{...(selected );
? {
_disabled: {
cursor: 'default',
borderColor: 'primary.300',
bg: 'primary.50 !important',
color: 'primary.600'
}
}
: {})}
onClick={() => {
onSendMessage?.({
text: option.value,
history: setUserSelectResultToHistories(chatHistories, option.value)
});
}}
>
{option.value}
</Button>
);
})}
</Flex>
);
}
return null;
}; };
export default React.memo(AIResponseBox); export default React.memo(AIResponseBox);
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 { Box, Flex, BoxProps, useDisclosure, HStack } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d'; import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -29,114 +29,472 @@ type sideTabItemType = { ...@@ -29,114 +29,472 @@ type sideTabItemType = {
children: sideTabItemType[]; children: sideTabItemType[];
}; };
function RowRender({ /* Per response value */
children, export const WholeResponseContent = ({
mb, activeModule,
label, hideTabs,
...props showDetail
}: { children: React.ReactNode; label: string } & BoxProps) {
return (
<Box mb={3}>
<Box fontSize={'sm'} mb={mb} color={'myGray.800'} flex={'0 0 90px'}>
{label}:
</Box>
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
{children}
</Box>
</Box>
);
}
function Row({
label,
value,
rawDom
}: { }: {
label: string; activeModule: ChatHistoryItemResType;
value?: string | number | boolean | object; hideTabs?: boolean;
rawDom?: React.ReactNode; showDetail: boolean;
}) { }) => {
const val = value || rawDom; const { t } = useTranslation();
const isObject = typeof value === 'object';
const formatValue = useMemo(() => { // Auto scroll to top
if (isObject) { const ContentRef = useRef<HTMLDivElement>(null);
return `~~~json\n${JSON.stringify(value, null, 2)}`; useEffect(() => {
if (ContentRef.current) {
ContentRef.current.scrollTop = 0;
} }
return `${value}`; }, [activeModule]);
}, [isObject, value]);
const RowRender = useCallback(
if (rawDom) { ({
return ( children,
<RowRender label={label} mb={1}> mb,
{rawDom} label,
</RowRender> ...props
); }: { children: React.ReactNode; label: string } & BoxProps) => {
} return (
<Box mb={3}>
<Box fontSize={'sm'} mb={mb} color={'myGray.800'} flex={'0 0 90px'}>
{label}:
</Box>
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
{children}
</Box>
</Box>
);
},
[]
);
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 (
<RowRender label={label} mb={1}>
{rawDom}
</RowRender>
);
}
if (val === undefined || val === '' || val === 'undefined') return null; if (val === undefined || val === '' || val === 'undefined') return null;
return (
<RowRender
label={label}
mb={isObject ? 0 : 1}
{...(isObject
? { py: 2, transform: 'translateY(-3px)' }
: value
? { px: 3, py: 2, border: 'base' }
: {})}
>
<Markdown source={formatValue} />
</RowRender>
);
},
[RowRender]
);
return ( return activeModule ? (
<RowRender <Box
label={label} h={'100%'}
mb={isObject ? 0 : 1} ref={ContentRef}
{...(isObject py={2}
? { py: 2, transform: 'translateY(-3px)' } px={4}
: value {...(hideTabs
? { px: 3, py: 2, border: 'base' } ? {}
: {})} : {
flex: '1 0 0',
overflow: 'auto'
})}
> >
<Markdown source={formatValue} /> {/* common info */}
</RowRender> <>
); <Row
} label={t('common:core.chat.response.module name')}
value={t(activeModule.moduleName as any)}
/>
{activeModule?.totalPoints !== undefined && (
<Row
label={t('common:support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
label={t('common:core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('common:core.chat.response.module model')} value={activeModule?.model} />
<Row
label={t('common:core.chat.response.module tokens')}
value={`${activeModule?.tokens}`}
/>
<Row
label={t('common:core.chat.response.Tool call tokens')}
value={`${activeModule?.toolCallTokens}`}
/>
const WholeResponseModal = ({ <Row label={t('common:core.chat.response.module query')} value={activeModule?.query} />
showDetail, <Row
onClose, label={t('common:core.chat.response.context total length')}
dataId value={activeModule?.contextTotalLen}
/>
<Row label={t('workflow:response.Error')} value={activeModule?.error} />
<Row label={t('chat:response.node_inputs')} value={activeModule?.nodeInputs} />
</>
{/* ai chat */}
<>
<Row
label={t('common:core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row
label={t('common:core.chat.response.module maxToken')}
value={activeModule?.maxToken}
/>
<Row
label={t('common:core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<Box px={3} py={2} border={'base'} borderRadius={'md'}>
{activeModule.historyPreview?.map((item, i) => (
<Box
key={i}
_notLast={{
borderBottom: '1px solid',
borderBottomColor: 'myWhite.700',
mb: 2
}}
pb={2}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
</Box>
))}
</Box>
) : (
''
)
}
/>
</>
{/* dataset search */}
<>
{activeModule?.searchMode && (
<Row
label={t('common:core.dataset.search.search mode')}
// @ts-ignore
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row
label={t('common:core.chat.response.module similarity')}
value={activeModule?.similarity}
/>
<Row label={t('common:core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('common:core.chat.response.search using reRank')}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('common:core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('common:support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('common:core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* classify question */}
<>
<Row
label={t('common:core.chat.response.module cq result')}
value={activeModule?.cqResult}
/>
<Row
label={t('common:core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
</>
{/* if-else */}
<>
<Row
label={t('common:core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}
<>
<Row
label={t('common:core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
<Row
label={t('common:core.chat.response.module extract result')}
value={activeModule?.extractResult}
/>
</>
{/* http */}
<>
<Row label={'Headers'} value={activeModule?.headers} />
<Row label={'Params'} value={activeModule?.params} />
<Row label={'Body'} value={activeModule?.body} />
<Row
label={t('common:core.chat.response.module http result')}
value={activeModule?.httpResult}
/>
</>
{/* plugin */}
<>
<Row
label={t('common:core.chat.response.plugin output')}
value={activeModule?.pluginOutput}
/>
</>
{/* text output */}
<Row label={t('common:core.chat.response.text output')} value={activeModule?.textOutput} />
{/* code */}
<>
<Row label={t('workflow:response.Custom inputs')} value={activeModule?.customInputs} />
<Row label={t('workflow:response.Custom outputs')} value={activeModule?.customOutputs} />
<Row label={t('workflow:response.Code log')} value={activeModule?.codeLog} />
</>
{/* read files */}
<>
{activeModule?.readFiles && activeModule?.readFiles.length > 0 && (
<Row
label={t('workflow:response.read files')}
rawDom={
<Flex flexWrap={'wrap'} gap={3} px={4} py={2}>
{activeModule?.readFiles.map((file, i) => (
<HStack
key={i}
bg={'white'}
boxShadow={'base'}
borderRadius={'sm'}
py={1}
px={2}
{...(file.url
? {
cursor: 'pointer',
onClick: () => window.open(file.url)
}
: {})}
>
<MyIcon name={getFileIcon(file.name) as any} w={'1rem'} />
<Box>{file.name}</Box>
</HStack>
))}
</Flex>
}
/>
)}
<Row
label={t('workflow:response.Read file result')}
value={activeModule?.readFilesResult}
/>
</>
{/* user select */}
<Row
label={t('common:core.chat.response.user_select_result')}
value={activeModule?.userSelectResult}
/>
{/* update var */}
<Row
label={t('common:core.chat.response.update_var_result')}
value={activeModule?.updateVarResult}
/>
</Box>
) : null;
};
/* Side tab: With and without children */
const SideTabItem = ({
sideBarItem,
onChange,
value,
index
}: { }: {
showDetail: boolean; sideBarItem: sideTabItemType;
onClose: () => void; onChange: (id: string) => void;
dataId: string; value: string;
index: number;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v); if (!sideBarItem) return null;
const { loading: isLoading, data: response } = useRequest2(
() => getHistoryResponseData({ dataId }), const AccordionSideTabItem = useCallback(
{ ({
manual: false sideBarItem,
} onChange,
value,
index
}: {
sideBarItem: sideTabItemType;
onChange: (id: string) => void;
value: string;
index: number;
}) => {
const { isOpen: isShowAccordion, onToggle: onToggleShowAccordion } = useDisclosure({
defaultIsOpen: false
});
return (
<>
<Flex align={'center'} position={'relative'}>
<NormalSideTabItem
index={index}
value={value}
onChange={onChange}
sideBarItem={sideBarItem}
>
<MyIcon
h={'20px'}
w={'20px'}
name={isShowAccordion ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
onClick={(e) => {
e.stopPropagation();
onToggleShowAccordion();
}}
_hover={{ color: 'primary.600', cursor: 'pointer' }}
/>
</NormalSideTabItem>
</Flex>
{isShowAccordion && (
<Box position={'relative'}>
{sideBarItem.children.map((item) => (
<SideTabItem
value={value}
key={item.id}
sideBarItem={item}
onChange={onChange}
index={index + 1}
/>
))}
</Box>
)}
</>
);
},
[]
); );
return ( const NormalSideTabItem = useCallback(
<MyModal ({
isCentered sideBarItem,
isOpen={true} onChange,
onClose={onClose} value,
h={['90vh', '80vh']} index,
isLoading={isLoading} children
maxH={['90vh', '700px']} }: {
minW={['90vw', '880px']} sideBarItem: sideTabItemType;
iconSrc="/imgs/modal/wholeRecord.svg" onChange: (id: string) => void;
title={ value: string;
<Flex alignItems={'center'}> index: number;
{t('common:core.chat.response.Complete Response')} children?: React.ReactNode;
<QuestionTip ml={2} label={t('chat:question_tip')}></QuestionTip> }) => {
const leftIndex = index > 3 ? 3 : index;
return (
<Flex
alignItems={'center'}
onClick={() => {
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'}
>
<Avatar
src={
sideBarItem.moduleLogo ||
moduleTemplatesFlat.find(
(template) => sideBarItem.moduleType === template.flowNodeType
)?.avatar
}
alt={''}
w={'1.5rem'}
h={'1.5rem'}
borderRadius={'sm'}
/>
<Box ml={2}>
<Box fontSize={'xs'} fontWeight={'bold'}>
{t(sideBarItem.moduleName as any)}
</Box>
<Box fontSize={'2xs'} color={'myGray.500'}>
{t(sideBarItem.runningTime as any) + 's'}
</Box>
</Box>
<Box
h={'20px'}
w={'20px'}
position={'absolute'}
right={2}
top={'50%'}
transform={'translateY(-50%)'}
>
{children}
</Box>
</Flex> </Flex>
} );
> },
{!!response?.length ? ( [t]
<ResponseBox response={response} showDetail={showDetail} />
) : (
<EmptyTip text={t('chat:no_workflow_response')} />
)}
</MyModal>
); );
};
export default WholeResponseModal; return sideBarItem.children.length !== 0 ? (
<>
<Box>
<AccordionSideTabItem
sideBarItem={sideBarItem}
onChange={onChange}
value={value}
index={index}
/>
</Box>
</>
) : (
<NormalSideTabItem index={index} value={value} onChange={onChange} sideBarItem={sideBarItem} />
);
};
/* Modal main container */
export const ResponseBox = React.memo(function ResponseBox({ export const ResponseBox = React.memo(function ResponseBox({
response, response,
showDetail, showDetail,
...@@ -151,14 +509,35 @@ export const ResponseBox = React.memo(function ResponseBox({ ...@@ -151,14 +509,35 @@ export const ResponseBox = React.memo(function ResponseBox({
const { t } = useTranslation(); const { t } = useTranslation();
const { isPc } = useSystem(); const { isPc } = useSystem();
const flattedResponse = useMemo( const flattedResponse = useMemo(() => {
() => /* Flat response */
flattenArray(response).map((item) => ({ function flattenArray(arr: ChatHistoryItemResType[]) {
...item, const result: ChatHistoryItemResType[] = [];
id: item.id ?? item.nodeId
})), function helper(currentArray: ChatHistoryItemResType[]) {
[response] 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( const [currentNodeId, setCurrentNodeId] = useState(
flattedResponse[0]?.id ?? flattedResponse[0]?.nodeId ?? '' flattedResponse[0]?.id ?? flattedResponse[0]?.nodeId ?? ''
); );
...@@ -169,6 +548,25 @@ export const ResponseBox = React.memo(function ResponseBox({ ...@@ -169,6 +548,25 @@ export const ResponseBox = React.memo(function ResponseBox({
); );
const sliderResponseList: sideTabItemType[] = useMemo(() => { 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); return pretreatmentResponse(response);
}, [response]); }, [response]);
...@@ -178,6 +576,37 @@ export const ResponseBox = React.memo(function ResponseBox({ ...@@ -178,6 +576,37 @@ export const ResponseBox = React.memo(function ResponseBox({
onClose: onCloseMobileModal onClose: onCloseMobileModal
} = useDisclosure(); } = useDisclosure();
const WholeResponseSideTab = useCallback(
({
response,
value,
onChange,
isMobile = false
}: {
response: sideTabItemType[];
value: string;
onChange: (index: string) => void;
isMobile?: boolean;
}) => {
return (
<>
{response.map((item) => (
<Box
key={item.id}
bg={isMobile ? 'myGray.100' : ''}
m={isMobile ? 3 : 0}
borderRadius={'md'}
minW={'12rem'}
>
<SideTabItem value={value} onChange={onChange} sideBarItem={item} index={0} />
</Box>
))}
</>
);
},
[]
);
return ( return (
<> <>
{isPc && !useMobile ? ( {isPc && !useMobile ? (
...@@ -191,7 +620,7 @@ export const ResponseBox = React.memo(function ResponseBox({ ...@@ -191,7 +620,7 @@ export const ResponseBox = React.memo(function ResponseBox({
/> />
</Box> </Box>
</Box> </Box>
<Box flex={'5 0 0'} overflowY={'auto'} overflowX={'hidden'} height={'100%'}> <Box flex={'5 0 0'} height={'100%'}>
<WholeResponseContent <WholeResponseContent
activeModule={activeModule} activeModule={activeModule}
hideTabs={hideTabs} hideTabs={hideTabs}
...@@ -255,7 +684,7 @@ export const ResponseBox = React.memo(function ResponseBox({ ...@@ -255,7 +684,7 @@ export const ResponseBox = React.memo(function ResponseBox({
{t(activeModule.moduleName as any)} {t(activeModule.moduleName as any)}
</Box> </Box>
</Flex> </Flex>
<Box flex={'1 0 0'} overflow={'auto'}> <Box flex={'1 0 0'}>
<WholeResponseContent <WholeResponseContent
activeModule={activeModule} activeModule={activeModule}
hideTabs={hideTabs} hideTabs={hideTabs}
...@@ -270,461 +699,49 @@ export const ResponseBox = React.memo(function ResponseBox({ ...@@ -270,461 +699,49 @@ export const ResponseBox = React.memo(function ResponseBox({
); );
}); });
export const WholeResponseContent = ({ const WholeResponseModal = ({
activeModule, showDetail,
hideTabs, onClose,
showDetail dataId
}: { }: {
activeModule: ChatHistoryItemResType;
hideTabs?: boolean;
showDetail: boolean; showDetail: boolean;
onClose: () => void;
dataId: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { workflowT } = useI18n();
return ( const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v);
<> const { loading: isLoading, data: response } = useRequest2(
{activeModule && ( () => getHistoryResponseData({ dataId }),
<Box {
py={2} manual: false
px={4} }
{...(hideTabs
? {}
: {
flex: '1 0 0',
overflow: 'auto'
})}
>
{/* common info */}
<>
<Row
label={t('common:core.chat.response.module name')}
value={t(activeModule.moduleName as any)}
/>
{activeModule?.totalPoints !== undefined && (
<Row
label={t('common:support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
label={t('common:core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('common:core.chat.response.module model')} value={activeModule?.model} />
<Row
label={t('common:core.chat.response.module tokens')}
value={`${activeModule?.tokens}`}
/>
<Row
label={t('common:core.chat.response.Tool call tokens')}
value={`${activeModule?.toolCallTokens}`}
/>
<Row label={t('common:core.chat.response.module query')} value={activeModule?.query} />
<Row
label={t('common:core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
<Row label={t('workflow:response.Error')} value={activeModule?.error} />
<Row label={t('chat:response.node_inputs')} value={activeModule?.nodeInputs} />
</>
{/* ai chat */}
<>
<Row
label={t('common:core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row
label={t('common:core.chat.response.module maxToken')}
value={activeModule?.maxToken}
/>
<Row
label={t('common:core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<Box px={3} py={2} border={'base'} borderRadius={'md'}>
{activeModule.historyPreview?.map((item, i) => (
<Box
key={i}
_notLast={{
borderBottom: '1px solid',
borderBottomColor: 'myWhite.700',
mb: 2
}}
pb={2}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
</Box>
))}
</Box>
) : (
''
)
}
/>
</>
{/* dataset search */}
<>
{activeModule?.searchMode && (
<Row
label={t('common:core.dataset.search.search mode')}
// @ts-ignore
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row
label={t('common:core.chat.response.module similarity')}
value={activeModule?.similarity}
/>
<Row label={t('common:core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('common:core.chat.response.search using reRank')}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('common:core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('common:support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('common:core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* classify question */}
<>
<Row
label={t('common:core.chat.response.module cq result')}
value={activeModule?.cqResult}
/>
<Row
label={t('common:core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
</>
{/* if-else */}
<>
<Row
label={t('common:core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}
<>
<Row
label={t('common:core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
<Row
label={t('common:core.chat.response.module extract result')}
value={activeModule?.extractResult}
/>
</>
{/* http */}
<>
<Row label={'Headers'} value={activeModule?.headers} />
<Row label={'Params'} value={activeModule?.params} />
<Row label={'Body'} value={activeModule?.body} />
<Row
label={t('common:core.chat.response.module http result')}
value={activeModule?.httpResult}
/>
</>
{/* plugin */}
<>
<Row
label={t('common:core.chat.response.plugin output')}
value={activeModule?.pluginOutput}
/>
</>
{/* text output */}
<Row
label={t('common:core.chat.response.text output')}
value={activeModule?.textOutput}
/>
{/* code */}
<>
<Row label={t('workflow:response.Custom inputs')} value={activeModule?.customInputs} />
<Row
label={t('workflow:response.Custom outputs')}
value={activeModule?.customOutputs}
/>
<Row label={t('workflow:response.Code log')} value={activeModule?.codeLog} />
</>
{/* read files */}
<>
{activeModule?.readFiles && activeModule?.readFiles.length > 0 && (
<Row
label={t('workflow:response.read files')}
rawDom={
<Flex flexWrap={'wrap'} gap={3} px={4} py={2}>
{activeModule?.readFiles.map((file, i) => (
<HStack
key={i}
bg={'white'}
boxShadow={'base'}
borderRadius={'sm'}
py={1}
px={2}
{...(file.url
? {
cursor: 'pointer',
onClick: () => window.open(file.url)
}
: {})}
>
<MyIcon name={getFileIcon(file.name) as any} w={'1rem'} />
<Box>{file.name}</Box>
</HStack>
))}
</Flex>
}
/>
)}
<Row
label={t('workflow:response.Read file result')}
value={activeModule?.readFilesResult}
/>
</>
{/* user select */}
<Row
label={t('common:core.chat.response.user_select_result')}
value={activeModule?.userSelectResult}
/>
{/* update var */}
<Row
label={t('common:core.chat.response.update_var_result')}
value={activeModule?.updateVarResult}
/>
</Box>
)}
</>
);
};
const WholeResponseSideTab = ({
response,
value,
onChange,
isMobile = false
}: {
response: sideTabItemType[];
value: string;
onChange: (index: string) => void;
isMobile?: boolean;
}) => {
return (
<>
{response.map((item) => (
<Box
key={item.id}
bg={isMobile ? 'myGray.100' : ''}
m={isMobile ? 3 : 0}
borderRadius={'md'}
minW={'12rem'}
>
<SideTabItem value={value} onChange={onChange} sideBarItem={item} index={0} />
</Box>
))}
</>
);
};
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 (
<>
<Flex align={'center'} position={'relative'}>
<NormalSideTabItem
index={index}
value={value}
onChange={onChange}
sideBarItem={sideBarItem}
>
<MyIcon
h={'20px'}
w={'20px'}
name={isShowAccordion ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
onClick={(e) => {
e.stopPropagation();
onToggleShowAccordion();
}}
_hover={{ color: 'primary.600', cursor: 'pointer' }}
/>
</NormalSideTabItem>
</Flex>
{isShowAccordion && (
<Box position={'relative'}>
{sideBarItem.children.map((item) => (
<SideTabItem
value={value}
key={item.id}
sideBarItem={item}
onChange={onChange}
index={index + 1}
/>
))}
</Box>
)}
</>
); );
};
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 ( return (
<Flex <MyModal
alignItems={'center'} isCentered
onClick={() => { isOpen={true}
onChange(sideBarItem.id); onClose={onClose}
}} h={['90vh', '80vh']}
background={value === sideBarItem.id ? 'myGray.100' : ''} isLoading={isLoading}
_hover={{ background: 'myGray.100' }} maxH={['90vh', '700px']}
p={2} minW={['90vw', '880px']}
width={'100%'} iconSrc="/imgs/modal/wholeRecord.svg"
cursor={'pointer'} title={
pl={leftIndex === 0 ? '0.5rem' : `${1.5 * leftIndex + 0.5}rem`} <Flex alignItems={'center'}>
borderRadius={'md'} {t('common:core.chat.response.Complete Response')}
position={'relative'} <QuestionTip ml={2} label={t('chat:question_tip')}></QuestionTip>
</Flex>
}
> >
<Avatar {!!response?.length ? (
src={ <ResponseBox response={response} showDetail={showDetail} />
sideBarItem.moduleLogo || ) : (
moduleTemplatesFlat.find((template) => sideBarItem.moduleType === template.flowNodeType) <EmptyTip text={t('chat:no_workflow_response')} />
?.avatar )}
} </MyModal>
alt={''}
w={'1.5rem'}
h={'1.5rem'}
borderRadius={'sm'}
/>
<Box ml={2}>
<Box fontSize={'xs'} fontWeight={'bold'}>
{t(sideBarItem.moduleName as any)}
</Box>
<Box fontSize={'2xs'} color={'myGray.500'}>
{t(sideBarItem.runningTime as any) + 's'}
</Box>
</Box>
<Box
h={'20px'}
w={'20px'}
position={'absolute'}
right={2}
top={'50%'}
transform={'translateY(-50%)'}
>
{children}
</Box>
</Flex>
);
};
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 ? (
<>
<Box>
<AccordionSideTabItem
sideBarItem={sideBarItem}
onChange={onChange}
value={value}
index={index}
/>
</Box>
</>
) : (
<NormalSideTabItem index={index} value={value} onChange={onChange} sideBarItem={sideBarItem} />
); );
}; };
/* Format response data to slider data */ export default WholeResponseModal;
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;
}
...@@ -59,6 +59,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ...@@ -59,6 +59,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const chatMessages = GPTMessages2Chats(messages); const chatMessages = GPTMessages2Chats(messages);
// console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length);
const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined; const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined;
/* user auth */ /* user auth */
......
...@@ -17,11 +17,12 @@ import { ...@@ -17,11 +17,12 @@ import {
getMaxHistoryLimitFromNodes, getMaxHistoryLimitFromNodes,
initWorkflowEdgeStatus, initWorkflowEdgeStatus,
storeNodes2RuntimeNodes, storeNodes2RuntimeNodes,
textAdaptGptResponse textAdaptGptResponse,
getLastInteractiveValue
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { getChatItems } from '@fastgpt/service/core/chat/controller'; 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 { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push'; import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
...@@ -45,7 +46,7 @@ import { AuthOutLinkChatProps } from '@fastgpt/global/support/outLink/api'; ...@@ -45,7 +46,7 @@ import { AuthOutLinkChatProps } from '@fastgpt/global/support/outLink/api';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/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 { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
...@@ -210,9 +211,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ...@@ -210,9 +211,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
MongoChat.findOne({ appId: app._id, chatId }, 'source variableList variables') MongoChat.findOne({ appId: app._id, chatId }, 'source variableList variables')
]); ]);
// Get chat histories
const newHistories = concatHistories(histories, chatMessages);
// Get store variables(Api variable precedence) // Get store variables(Api variable precedence)
if (chatDetail?.variables) { if (chatDetail?.variables) {
variables = { variables = {
...@@ -221,6 +219,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ...@@ -221,6 +219,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}; };
} }
// Get chat histories
const newHistories = concatHistories(histories, chatMessages);
// Get runtimeNodes // Get runtimeNodes
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories)); let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories));
if (isPlugin) { if (isPlugin) {
...@@ -286,36 +287,51 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { ...@@ -286,36 +287,51 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return ChatSourceEnum.online; return ChatSourceEnum.online;
})(); })();
const isInteractiveRequest = !!getLastInteractiveValue(histories);
const { text: userSelectedVal } = chatValue2RuntimePrompt(userQuestion.value);
const newTitle = isPlugin const newTitle = isPlugin
? variables.cTime ?? getSystemTime(user.timezone) ? variables.cTime ?? getSystemTime(user.timezone)
: getChatTitleFromChatMessage(userQuestion); : getChatTitleFromChatMessage(userQuestion);
await saveChat({ const aiResponse: AIChatItemType & { dataId?: string } = {
chatId, dataId: responseChatItemId,
appId: app._id, obj: ChatRoleEnum.AI,
teamId, value: assistantResponses,
tmbId: tmbId, [DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses
nodes, };
appChatConfig: chatConfig,
variables: newVariables, if (isInteractiveRequest) {
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time await updateInteractiveChat({
newTitle, chatId,
shareId, appId: app._id,
outLinkUid: outLinkUserId, teamId,
source, tmbId: tmbId,
content: [ userSelectedVal,
userQuestion, aiResponse,
{ newVariables,
dataId: responseChatItemId, newTitle
obj: ChatRoleEnum.AI, });
value: assistantResponses, } else {
[DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses 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`); addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`);
......
...@@ -466,7 +466,21 @@ const RenderList = React.memo(function RenderList({ ...@@ -466,7 +466,21 @@ const RenderList = React.memo(function RenderList({
flowNodeType: templateNode.flowNodeType, flowNodeType: templateNode.flowNodeType,
pluginId: templateNode.pluginId 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 }, position: { x: mouseX, y: mouseY - 20 },
selected: true selected: true
......
...@@ -26,6 +26,7 @@ import { AiChatModule } from '@fastgpt/global/core/workflow/template/system/aiCh ...@@ -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 { DatasetSearchModule } from '@fastgpt/global/core/workflow/template/system/datasetSearch';
import { ReadFilesNodes } from '@fastgpt/global/core/workflow/template/system/readFiles'; import { ReadFilesNodes } from '@fastgpt/global/core/workflow/template/system/readFiles';
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
import { Input_Template_UserChatInput } from '@fastgpt/global/core/workflow/template/input';
type WorkflowType = { type WorkflowType = {
nodes: StoreNodeItemType[]; nodes: StoreNodeItemType[];
...@@ -259,12 +260,8 @@ export function form2AppWorkflow( ...@@ -259,12 +260,8 @@ export function form2AppWorkflow(
value: formData.dataset.datasetSearchExtensionBg value: formData.dataset.datasetSearchExtensionBg
}, },
{ {
key: 'userChatInput', ...Input_Template_UserChatInput,
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea], toolDescription: i18nT('workflow:content_to_search'),
valueType: WorkflowIOValueTypeEnum.string,
label: '用户问题',
required: true,
toolDescription: '需要检索的内容',
value: question value: question
} }
], ],
...@@ -503,6 +500,18 @@ export function form2AppWorkflow( ...@@ -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; return config;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment