4.8.11 test fix (#2746)

* fix: refresh tool param

* perf: load image message

* fix: cannot remoce externalReadUrl

* perf: variables update dom

* perf: empty response tip

* fix: conflict
This commit is contained in:
Archer
2024-09-20 11:28:58 +08:00
committed by GitHub
parent 6c2a7574c3
commit 5e7c97b7b8
19 changed files with 292 additions and 247 deletions

View File

@@ -114,7 +114,7 @@ ${content}
它接收一个`string`类型的输入,除了可以引用文档解析结果外,还可以实现自定义内容引用,最终会进行提示词拼接,放置在 role=system 的消息中。提示词模板如下: 它接收一个`string`类型的输入,除了可以引用文档解析结果外,还可以实现自定义内容引用,最终会进行提示词拼接,放置在 role=system 的消息中。提示词模板如下:
``` ```
将 <Quote></Quote> 中的内容作为本次对话的参考内容: 将 <Quote></Quote> 中的内容作为本次对话的参考:
<Quote> <Quote>
{{quote}} {{quote}}
</Quote> </Quote>

View File

@@ -227,7 +227,7 @@ curl --location --request POST '{{host}}/shareAuth/finish' \
"historyPreview": [ "historyPreview": [
{ {
"obj": "Human", "obj": "Human",
"value": "使用 <Data></Data> 标记中的内容作为本次对话的参考内容:\n\n<Data>\n导演是谁\n电影《铃芽之旅》的导演是新海诚。\n------\n电影《铃芽之旅》的编剧是谁22\n新海诚是本片的编剧。\n------\n电影《铃芽之旅》的女主角是谁\n电影的女主角是铃芽。\n------\n电影《铃芽之旅》的制作团队中有哪位著名人士2\n川村元气是本片的制作团队成员之一。\n------\n你是谁\n我是电影《铃芽之旅》助手\n------\n电影《铃芽之旅》男主角是谁\n电影《铃芽之旅》男主角是宗像草太由松村北斗配音。\n------\n电影《铃芽之旅》的作者新海诚写了一本小说叫什么名字\n小说名字叫《铃芽之旅》。\n------\n电影《铃芽之旅》的女主角是谁\n电影《铃芽之旅》的女主角是岩户铃芽由原菜乃华配音。\n------\n电影《铃芽之旅》的故事背景是什么\n日本\n------\n谁担任电影《铃芽之旅》中岩户环的配音\n深津绘里担任电影《铃芽之旅》中岩户环的配音。\n</Data>\n\n回答要求\n- 如果你不清楚答案,你需要澄清。\n- 避免提及你是从 <Data></Data> 获取的知识。\n- 保持答案与 <Data></Data> 中描述的一致。\n- 使用 Markdown 语法优化回答格式。\n- 使用与问题相同的语言回答。\n\n问题:\"\"\"导演是谁\"\"\"" "value": "使用 <Data></Data> 标记中的内容作为本次对话的参考:\n\n<Data>\n导演是谁\n电影《铃芽之旅》的导演是新海诚。\n------\n电影《铃芽之旅》的编剧是谁22\n新海诚是本片的编剧。\n------\n电影《铃芽之旅》的女主角是谁\n电影的女主角是铃芽。\n------\n电影《铃芽之旅》的制作团队中有哪位著名人士2\n川村元气是本片的制作团队成员之一。\n------\n你是谁\n我是电影《铃芽之旅》助手\n------\n电影《铃芽之旅》男主角是谁\n电影《铃芽之旅》男主角是宗像草太由松村北斗配音。\n------\n电影《铃芽之旅》的作者新海诚写了一本小说叫什么名字\n小说名字叫《铃芽之旅》。\n------\n电影《铃芽之旅》的女主角是谁\n电影《铃芽之旅》的女主角是岩户铃芽由原菜乃华配音。\n------\n电影《铃芽之旅》的故事背景是什么\n日本\n------\n谁担任电影《铃芽之旅》中岩户环的配音\n深津绘里担任电影《铃芽之旅》中岩户环的配音。\n</Data>\n\n回答要求\n- 如果你不清楚答案,你需要澄清。\n- 避免提及你是从 <Data></Data> 获取的知识。\n- 保持答案与 <Data></Data> 中描述的一致。\n- 使用 Markdown 语法优化回答格式。\n- 使用与问题相同的语言回答。\n\n问题:\"\"\"导演是谁\"\"\""
}, },
{ {
"obj": "AI", "obj": "AI",

View File

@@ -965,7 +965,7 @@ export default async function (ctx: FunctionContext) {
"required": true, "required": true,
"description": "", "description": "",
"canEdit": false, "canEdit": false,
"value": "请使用下面<data> </data>中的数据作为本次对话的参考内容。请直接输出答案,不要提及你是从<data> </data>中获取的知识。\n\n当前时间:{{cTime}}\n\n<data>\n{{response}}\n</data>\n\n我的问题:\"{{q}}\"", "value": "请使用下面<data> </data>中的数据作为本次对话的参考。请直接输出答案,不要提及你是从<data> </data>中获取的知识。\n\n当前时间:{{cTime}}\n\n<data>\n{{response}}\n</data>\n\n我的问题:\"{{q}}\"",
"editField": { "editField": {
"key": true "key": true
}, },

View File

@@ -48,7 +48,7 @@ export const Prompt_userQuotePromptList: PromptTemplateItem[] = [
{ {
title: i18nT('app:template.standard_template'), title: i18nT('app:template.standard_template'),
desc: '', desc: '',
value: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考内容: value: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference> <Reference>
{{quote}} {{quote}}
@@ -83,7 +83,7 @@ export const Prompt_userQuotePromptList: PromptTemplateItem[] = [
{ {
title: i18nT('app:template.standard_strict'), title: i18nT('app:template.standard_strict'),
desc: '', desc: '',
value: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考内容: value: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference> <Reference>
{{quote}} {{quote}}
@@ -133,7 +133,7 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
{ {
title: i18nT('app:template.standard_template'), title: i18nT('app:template.standard_template'),
desc: '', desc: '',
value: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考内容: value: `使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference> <Reference>
{{quote}} {{quote}}
@@ -164,7 +164,7 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
{ {
title: i18nT('app:template.standard_strict'), title: i18nT('app:template.standard_strict'),
desc: '', desc: '',
value: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考内容: value: `忘记你已有的知识,仅使用 <Reference></Reference> 标记中的内容作为本次对话的参考:
<Reference> <Reference>
{{quote}} {{quote}}
@@ -207,7 +207,7 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [
]; ];
// Document quote prompt // Document quote prompt
export const Prompt_DocumentQuote = `将 <Reference></Reference> 中的内容作为本次对话的参考内容: export const Prompt_DocumentQuote = `将 <Reference></Reference> 中的内容作为本次对话的参考:
<Reference> <Reference>
{{quote}} {{quote}}
</Reference> </Reference>

View File

@@ -8,6 +8,8 @@ import axios from 'axios';
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
import { getFileContentTypeFromHeader, guessBase64ImageType } from '../../common/file/utils'; import { getFileContentTypeFromHeader, guessBase64ImageType } from '../../common/file/utils';
import { serverRequestBaseUrl } from '../../common/api/serverRequest'; import { serverRequestBaseUrl } from '../../common/api/serverRequest';
import { i18nT } from '../../../web/i18n/utils';
import { addLog } from '../../common/system/log';
/* slice chat context by tokens */ /* slice chat context by tokens */
const filterEmptyMessages = (messages: ChatCompletionMessageParam[]) => { const filterEmptyMessages = (messages: ChatCompletionMessageParam[]) => {
@@ -111,20 +113,62 @@ export const loadRequestMessages = async ({
useVision?: boolean; useVision?: boolean;
origin?: string; origin?: string;
}) => { }) => {
// Load image to base64
const loadImageToBase64 = async (messages: ChatCompletionContentPart[]) => {
return Promise.all(
messages.map(async (item) => {
if (item.type === 'image_url') {
// Remove url origin
const imgUrl = (() => {
if (origin && item.image_url.url.startsWith(origin)) {
return item.image_url.url.replace(origin, '');
}
return item.image_url.url;
})();
// If imgUrl is a local path, load image from local, and set url to base64
if (imgUrl.startsWith('/') || process.env.VISION_FOCUS_BASE64 === 'true') {
addLog.debug('Load image from local server', {
baseUrl: serverRequestBaseUrl,
requestUrl: imgUrl
});
const response = await axios.get(imgUrl, {
baseURL: serverRequestBaseUrl,
responseType: 'arraybuffer',
proxy: false
});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const imageType =
getFileContentTypeFromHeader(response.headers['content-type']) ||
guessBase64ImageType(base64);
return {
...item,
image_url: {
...item.image_url,
url: `data:${imageType};base64,${base64}`
}
};
}
}
return item;
})
);
};
// Split question text and image // Split question text and image
function parseStringWithImages(input: string): ChatCompletionContentPart[] { const parseStringWithImages = (input: string): ChatCompletionContentPart[] => {
if (!useVision) { if (!useVision) {
return [{ type: 'text', text: input || '' }]; return [{ type: 'text', text: input || '' }];
} }
// 正则表达式匹配图片URL // 正则表达式匹配图片URL
const imageRegex = const imageRegex =
/(https?:\/\/[^\s/$.?#].[^\s]*\.(?:png|jpe?g|gif|webp|bmp|tiff?|svg|ico|heic|avif))/i; /(https?:\/\/[^\s/$.?#].[^\s]*\.(?:png|jpe?g|gif|webp|bmp|tiff?|svg|ico|heic|avif))/gi;
const result: ChatCompletionContentPart[] = []; const result: ChatCompletionContentPart[] = [];
// 提取所有HTTPS图片URL并添加到result开头 // 提取所有HTTPS图片URL并添加到result开头
const httpsImages = input.match(imageRegex) || []; const httpsImages = [...new Set(Array.from(input.matchAll(imageRegex), (m) => m[0]))];
httpsImages.forEach((url) => { httpsImages.forEach((url) => {
result.push({ result.push({
type: 'image_url', type: 'image_url',
@@ -137,54 +181,27 @@ export const loadRequestMessages = async ({
// 添加原始input作为文本 // 添加原始input作为文本
result.push({ type: 'text', text: input }); result.push({ type: 'text', text: input });
return result; return result;
} };
// Load image // Parse user content(text and img)
const parseUserContent = async (content: string | ChatCompletionContentPart[]) => { const parseUserContent = async (content: string | ChatCompletionContentPart[]) => {
if (typeof content === 'string') { if (typeof content === 'string') {
return parseStringWithImages(content); return loadImageToBase64(parseStringWithImages(content));
} }
const result = await Promise.all( const result = await Promise.all(
content.map(async (item) => { content.map(async (item) => {
if (item.type === 'text') return parseStringWithImages(item.text); if (item.type === 'text') return parseStringWithImages(item.text);
if (item.type === 'file_url') return; if (item.type === 'file_url') return; // LLM not support file_url
if (!item.image_url.url) return item; if (!item.image_url.url) return item;
// Remove url origin
const imgUrl = (() => {
if (origin && item.image_url.url.startsWith(origin)) {
return item.image_url.url.replace(origin, '');
}
return item.image_url.url;
})();
/* Load local image */
if (imgUrl.startsWith('/')) {
const response = await axios.get(imgUrl, {
baseURL: serverRequestBaseUrl,
responseType: 'arraybuffer'
});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const imageType =
getFileContentTypeFromHeader(response.headers['content-type']) ||
guessBase64ImageType(base64);
return {
...item,
image_url: {
...item.image_url,
url: `data:${imageType};base64,${base64}`
}
};
}
return item; return item;
}) })
); );
return result.flat().filter(Boolean); return loadImageToBase64(result.flat().filter(Boolean) as ChatCompletionContentPart[]);
}; };
// format GPT messages, concat text messages // format GPT messages, concat text messages
const clearInvalidMessages = (messages: ChatCompletionMessageParam[]) => { const clearInvalidMessages = (messages: ChatCompletionMessageParam[]) => {
return messages return messages
@@ -247,7 +264,7 @@ export const loadRequestMessages = async ({
}; };
if (messages.length === 0) { if (messages.length === 0) {
return Promise.reject('core.chat.error.Messages empty'); return Promise.reject(i18nT('common:core.chat.error.Messages empty'));
} }
// filter messages file // filter messages file
@@ -275,6 +292,7 @@ export const loadRequestMessages = async ({
content: await parseUserContent(item.content) content: await parseUserContent(item.content)
}; };
} else if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant) { } else if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant) {
// remove invalid field
return { return {
role: item.role, role: item.role,
content: item.content, content: item.content,

View File

@@ -19,6 +19,7 @@
"input_guide": "Input Guide", "input_guide": "Input Guide",
"input_guide_lexicon": "Lexicon", "input_guide_lexicon": "Lexicon",
"input_guide_tip": "You can set up some preset questions. When the user inputs a question, related questions from these presets will be suggested.", "input_guide_tip": "You can set up some preset questions. When the user inputs a question, related questions from these presets will be suggested.",
"input_placeholder_phone": "Please enter your question",
"insert_input_guide,_some_data_already_exists": "Duplicate data detected, automatically filtered, {{len}} items inserted", "insert_input_guide,_some_data_already_exists": "Duplicate data detected, automatically filtered, {{len}} items inserted",
"is_chatting": "Chatting in progress... please wait until it finishes", "is_chatting": "Chatting in progress... please wait until it finishes",
"items": "Items", "items": "Items",
@@ -36,4 +37,4 @@
"stream_output": "Stream Output", "stream_output": "Stream Output",
"view_citations": "View References", "view_citations": "View References",
"web_site_sync": "Web Site Sync" "web_site_sync": "Web Site Sync"
} }

View File

@@ -19,6 +19,7 @@
"input_guide": "输入引导", "input_guide": "输入引导",
"input_guide_lexicon": "词库", "input_guide_lexicon": "词库",
"input_guide_tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。", "input_guide_tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。",
"input_placeholder_phone": "输入问题",
"insert_input_guide,_some_data_already_exists": "有重复数据,已自动过滤,共插入 {{len}} 条数据", "insert_input_guide,_some_data_already_exists": "有重复数据,已自动过滤,共插入 {{len}} 条数据",
"is_chatting": "正在聊天中...请等待结束", "is_chatting": "正在聊天中...请等待结束",
"items": "条", "items": "条",
@@ -36,4 +37,4 @@
"stream_output": "流输出", "stream_output": "流输出",
"view_citations": "查看引用", "view_citations": "查看引用",
"web_site_sync": "Web站点同步" "web_site_sync": "Web站点同步"
} }

View File

@@ -449,7 +449,11 @@ const ChatInput = ({
border: 'none' border: 'none'
}} }}
placeholder={ placeholder={
isSpeaking ? t('common:core.chat.Speaking') : t('common:core.chat.Type a message') isSpeaking
? t('common:core.chat.Speaking')
: isPc
? t('common:core.chat.Type a message')
: t('chat:input_placeholder_phone')
} }
resize={'none'} resize={'none'}
rows={1} rows={1}

View File

@@ -174,7 +174,7 @@ const ChatItem = (props: Props) => {
// Check last group is interactive, Auto add a empty text node(animation) // Check last group is interactive, Auto add a empty text node(animation)
const lastGroup = groupedValues[groupedValues.length - 1]; const lastGroup = groupedValues[groupedValues.length - 1];
if (isChatting) { if (isChatting || groupedValues.length === 0) {
if ( if (
(lastGroup && (lastGroup &&
lastGroup[lastGroup.length - 1] && lastGroup[lastGroup.length - 1] &&

View File

@@ -72,7 +72,7 @@ async function handler(
...(websiteConfig && { websiteConfig }), ...(websiteConfig && { websiteConfig }),
...(status && { status }), ...(status && { status }),
...(intro !== undefined && { intro }), ...(intro !== undefined && { intro }),
...(externalReadUrl && { externalReadUrl }), ...(externalReadUrl !== undefined && { externalReadUrl }),
// move // move
...(updatedDefaultPermission !== undefined && { ...(updatedDefaultPermission !== undefined && {
defaultPermission: updatedDefaultPermission defaultPermission: updatedDefaultPermission

View File

@@ -266,7 +266,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
workflowStreamResponse: workflowResponseWrite workflowStreamResponse: workflowResponseWrite
}); });
} }
return Promise.reject('请升级工作流'); return Promise.reject('您的工作流版本过低,请重新发布一次');
})(); })();
// save chat // save chat

View File

@@ -67,7 +67,7 @@ const Header = () => {
future, future,
setPast setPast
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const { appType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();
const [isPublished, setIsPublished] = useState(false); const [isPublished, setIsPublished] = useState(false);
useDebounceEffect( useDebounceEffect(
@@ -138,11 +138,11 @@ const Header = () => {
pathname: '/app/list', pathname: '/app/list',
query: { query: {
parentId: appDetail.parentId, parentId: appDetail.parentId,
type: appType type: lastAppListRouteType
} }
}); });
} catch (error) {} } catch (error) {}
}, [appDetail._id, appDetail.parentId, router]); }, [appDetail._id, appDetail.parentId, lastAppListRouteType, router]);
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return (

View File

@@ -37,7 +37,7 @@ const Header = ({
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { appId, appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v); const { appId, appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const { appType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), { const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
manual: false, manual: false,
@@ -49,11 +49,11 @@ const Header = ({
pathname: '/app/list', pathname: '/app/list',
query: { query: {
parentId, parentId,
type: appType type: lastAppListRouteType
} }
}); });
}, },
[router, appType] [router, lastAppListRouteType]
); );
const isPublished = useMemo(() => { const isPublished = useMemo(() => {

View File

@@ -67,7 +67,8 @@ const Header = () => {
future, future,
setPast setPast
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const { appType } = useSystemStore();
const { lastAppListRouteType } = useSystemStore();
// Check if the workflow is published // Check if the workflow is published
const [isPublished, setIsPublished] = useState(false); const [isPublished, setIsPublished] = useState(false);
@@ -139,11 +140,11 @@ const Header = () => {
pathname: '/app/list', pathname: '/app/list',
query: { query: {
parentId: appDetail.parentId, parentId: appDetail.parentId,
type: appType type: lastAppListRouteType
} }
}); });
} catch (error) {} } catch (error) {}
}, [appDetail._id, appDetail.parentId, router]); }, [appDetail._id, appDetail.parentId, lastAppListRouteType, router]);
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return (

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import NodeCard from './render/NodeCard'; import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow'; import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
@@ -12,7 +12,8 @@ import {
NumberInput, NumberInput,
NumberInputField, NumberInputField,
NumberInputStepper, NumberInputStepper,
Switch Switch,
Textarea
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
@@ -33,7 +34,7 @@ import { getRefData } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils'; import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { useCreation } from 'ahooks'; import { useCreation, useMemoizedFn } from 'ahooks';
import { getEditorVariables } from '../../utils'; import { getEditorVariables } from '../../utils';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
@@ -45,6 +46,19 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const appDetail = useContextSelector(AppContext, (v) => v.appDetail); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const menuList = useRef([
{
renderType: FlowNodeInputTypeEnum.input,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon,
label: t('common:core.workflow.inputType.Manual input')
},
{
renderType: FlowNodeInputTypeEnum.reference,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon,
label: t('common:core.workflow.inputType.Reference')
}
]);
const variables = useCreation(() => { const variables = useCreation(() => {
return getEditorVariables({ return getEditorVariables({
nodeId, nodeId,
@@ -80,195 +94,182 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
[inputs, nodeId, onChangeNode] [inputs, nodeId, onChangeNode]
); );
const Render = useMemo(() => { const ValueRender = useMemoizedFn(
const menuList = [ ({ updateItem, index }: { updateItem: TUpdateListItem; index: number }) => {
{ const { valueType } = getRefData({
renderType: FlowNodeInputTypeEnum.input, variable: updateItem.variable,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon, nodeList,
label: t('common:core.workflow.inputType.Manual input') chatConfig: appDetail.chatConfig
}, });
{ const renderTypeData = menuList.current.find(
renderType: FlowNodeInputTypeEnum.reference, (item) => item.renderType === updateItem.renderType
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon, );
label: t('common:core.workflow.inputType.Reference')
}
];
return ( const handleUpdate = (newValue: ReferenceValueProps | string) => {
<> if (isReferenceValue(newValue)) {
{updateList.map((updateItem, index) => { onUpdateList(
const { valueType } = getRefData({ updateList.map((update, i) =>
variable: updateItem.variable, i === index ? { ...update, value: newValue as ReferenceValueProps } : update
nodeList, )
chatConfig: appDetail.chatConfig );
}); } else {
const renderTypeData = menuList.find((item) => item.renderType === updateItem.renderType); onUpdateList(
const handleUpdate = (newValue: ReferenceValueProps | string) => { updateList.map((update, i) =>
if (isReferenceValue(newValue)) { i === index ? { ...update, value: ['', newValue as string] } : update
onUpdateList( )
updateList.map((update, i) => );
i === index ? { ...update, value: newValue as ReferenceValueProps } : update }
) };
);
} else {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: ['', newValue as string] } : update
)
);
}
};
return ( return (
<Container key={index} mt={4} w={'full'} mx={0}> <Container key={index} mt={4} w={'full'} mx={0}>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Flex w={'60px'}>{t('common:core.workflow.variable')}</Flex> <Flex w={'60px'}>{t('common:core.workflow.variable')}</Flex>
<Reference <Reference
nodeId={nodeId} nodeId={nodeId}
variable={updateItem.variable} variable={updateItem.variable}
onSelect={(value) => { onSelect={(value) => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
valueType,
variable: value
};
}
return update;
})
);
}}
/>
<Box flex={1} />
{updateList.length > 1 && (
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
top={3}
right={3}
onClick={() => {
onUpdateList(updateList.filter((_, i) => i !== index));
}}
/>
)}
</Flex>
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag">
<Flex w={'60px'}>
<Box>{t('common:core.workflow.value')}</Box>
<MyTooltip
label={
menuList.current.find((item) => item.renderType === updateItem.renderType)?.label
}
>
<Button
size={'xs'}
bg={'white'}
borderRadius={'xs'}
mx={2}
color={'primary.600'}
onClick={() => {
onUpdateList( onUpdateList(
updateList.map((update, i) => { updateList.map((update, i) => {
if (i === index) { if (i === index) {
return { return {
...update, ...update,
value: ['', ''], value: ['', ''],
valueType, renderType:
variable: value updateItem.renderType === FlowNodeInputTypeEnum.input
? FlowNodeInputTypeEnum.reference
: FlowNodeInputTypeEnum.input
}; };
} }
return update; return update;
}) })
); );
}} }}
/> >
<Box flex={1} /> <MyIcon name={renderTypeData?.icon as any} w={'14px'} />
{updateList.length > 1 && ( </Button>
<MyIcon </MyTooltip>
className="delete" </Flex>
name={'delete'}
w={'14px'} {/* Render input components */}
color={'myGray.600'} {(() => {
cursor={'pointer'} if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
_hover={{ color: 'red.500' }} return (
position={'absolute'} <Reference
top={3} nodeId={nodeId}
right={3} variable={updateItem.value}
onClick={() => { valueType={valueType}
onUpdateList(updateList.filter((_, i) => i !== index)); onSelect={handleUpdate}
}}
/> />
)} );
</Flex> }
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag"> if (valueType === WorkflowIOValueTypeEnum.string) {
<Flex w={'60px'}> return (
<Box>{t('common:core.workflow.value')}</Box> <Box w={'300px'}>
<MyTooltip <PromptEditor
label={ value={updateItem.value?.[1] || ''}
menuList.find((item) => item.renderType === updateItem.renderType)?.label onChange={handleUpdate}
} showOpenModal={false}
> variableLabels={variables}
<Button h={100}
size={'xs'}
bg={'white'}
borderRadius={'xs'}
mx={2}
color={'primary.600'}
onClick={() => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
renderType:
updateItem.renderType === FlowNodeInputTypeEnum.input
? FlowNodeInputTypeEnum.reference
: FlowNodeInputTypeEnum.input
};
}
return update;
})
);
}}
>
<MyIcon name={renderTypeData?.icon as any} w={'14px'} />
</Button>
</MyTooltip>
</Flex>
{/* Render input components */}
{(() => {
if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
return (
<Reference
nodeId={nodeId}
variable={updateItem.value}
valueType={valueType}
onSelect={handleUpdate}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Box w={'300px'}>
<PromptEditor
value={updateItem.value?.[1] || ''}
onChange={handleUpdate}
showOpenModal={false}
variableLabels={variables}
h={100}
/>
</Box>
);
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput value={Number(updateItem.value?.[1]) || 0}>
<NumberInputField
bg="white"
onChange={(e) => handleUpdate(e.target.value)}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
defaultChecked={updateItem.value?.[1] === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
/>
);
}
return (
<JsonEditor
bg="white"
resize
w="300px"
value={String(updateItem.value?.[1] || '')}
onChange={(e) => {
handleUpdate(e);
}}
/> />
); </Box>
})()} );
</Flex> }
</Container> if (valueType === WorkflowIOValueTypeEnum.number) {
); return (
})} <NumberInput value={Number(updateItem.value?.[1]) || 0}>
</> <NumberInputField bg="white" onChange={(e) => handleUpdate(e.target.value)} />
); <NumberInputStepper>
}, [appDetail.chatConfig, nodeId, nodeList, onUpdateList, t, updateList, variables]); <NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
defaultChecked={updateItem.value?.[1] === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
/>
);
}
return (
<Box w={'300px'}>
<PromptEditor
value={updateItem.value?.[1] || ''}
onChange={handleUpdate}
showOpenModal={false}
variableLabels={variables}
h={100}
/>
</Box>
);
})()}
</Flex>
</Container>
);
}
);
return ( return (
<NodeCard selected={selected} maxW={'1000px'} {...data}> <NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}> <Box px={4} pb={4}>
{Render} <>
{updateList.map((updateItem, index) => (
<ValueRender key={index} updateItem={updateItem} index={index} />
))}
</>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}> <Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Button <Button
variant={'whiteBase'} variant={'whiteBase'}

View File

@@ -549,20 +549,23 @@ const WorkflowContextProvider = ({
); );
/* If the module is connected by a tool, the tool input and the normal input are separated */ /* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = useMemoizedFn((inputs: FlowNodeInputItemType[], nodeId: string) => { const splitToolInputs = useCallback(
const isTool = !!edges.find( (inputs: FlowNodeInputItemType[], nodeId: string) => {
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId const isTool = !!edges.find(
); (edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
);
return { return {
isTool, isTool,
toolInputs: inputs.filter((item) => isTool && item.toolDescription), toolInputs: inputs.filter((item) => isTool && item.toolDescription),
commonInputs: inputs.filter((item) => { commonInputs: inputs.filter((item) => {
if (!isTool) return true; if (!isTool) return true;
return !item.toolDescription; return !item.toolDescription;
}) })
}; };
}); },
[edges]
);
/* ui flow to store data */ /* ui flow to store data */
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => { const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useState } from 'react'; import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
@@ -14,6 +14,7 @@ import { AppUpdateParams } from '@/global/core/app/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useI18n } from '@/web/context/I18n'; import { useI18n } from '@/web/context/I18n';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal')); const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
type AppListContextType = { type AppListContextType = {
@@ -135,6 +136,11 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
); );
}, []); }, []);
const { setLastAppListRouteType } = useSystemStore();
useEffect(() => {
setLastAppListRouteType(type);
}, [setLastAppListRouteType, type]);
const contextValue: AppListContextType = { const contextValue: AppListContextType = {
parentId, parentId,
appType: type, appType: type,

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
Box, Box,
Flex, Flex,

View File

@@ -19,8 +19,12 @@ type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: stri
type State = { type State = {
initd: boolean; initd: boolean;
setInitd: () => void; setInitd: () => void;
lastRoute: string; lastRoute: string;
setLastRoute: (e: string) => void; setLastRoute: (e: string) => void;
lastAppListRouteType?: string;
setLastAppListRouteType: (e?: string) => void;
loginStore?: LoginStoreType; loginStore?: LoginStoreType;
setLoginStore: (e: LoginStoreType) => void; setLoginStore: (e: LoginStoreType) => void;
loading: boolean; loading: boolean;
@@ -67,6 +71,12 @@ export const useSystemStore = create<State>()(
state.lastRoute = e; state.lastRoute = e;
}); });
}, },
lastAppListRouteType: undefined,
setLastAppListRouteType(e) {
set((state) => {
state.lastAppListRouteType = e;
});
},
loginStore: undefined, loginStore: undefined,
setLoginStore(e) { setLoginStore(e) {
set((state) => { set((state) => {