Commit 9bb58c98 by Finley Ge Committed by GitHub

chore: render tool readmes in v2 modals & fix multiselect schema in agent (#7082)

* Render tool readmes in v2 modals

* fix(agent): accept system tool descriptions as parameters
parent de43a189
......@@ -169,7 +169,7 @@ export const jsonSchema2NodeInput = ({
schemaType === 'http'
? value['x-tool-description']
: schemaType === 'systemTool'
? value['toolDescription']
? value['toolDescription'] || value.description
: value.description || key,
required: jsonSchema?.required?.includes(key),
...getNodeInputRenderTypeFromSchemaInputType(value)
......
......@@ -123,6 +123,9 @@ export const ToolDataSchema = z.object({
courseUrl: z.string().optional().meta({
description: '工具教程地址'
}),
readmeUrl: z.string().optional().meta({
description: '工具 README 地址'
}),
name: z.string().optional().meta({
description: '工具展示名称'
}),
......@@ -171,7 +174,8 @@ export const FlowNodeCommonTypeSchema = z.object({
currentCost: NumSchema.optional(),
systemKeyCost: NumSchema.optional(),
hasTokenFee: BoolSchema.optional(),
hasSystemSecret: BoolSchema.optional()
hasSystemSecret: BoolSchema.optional(),
readmeUrl: z.string().optional()
});
export type FlowNodeCommonType = z.infer<typeof FlowNodeCommonTypeSchema>;
......@@ -199,6 +203,7 @@ export const FlowNodeTemplateTypeSchema = FlowNodeCommonTypeSchema.extend({
diagram: z.string().optional(),
courseUrl: z.string().optional(),
readmeUrl: z.string().optional(),
userGuide: z.string().optional(),
tags: z.array(z.string()).nullish(),
......
......@@ -216,6 +216,44 @@ describe('jsonSchema2NodeInput', () => {
]);
});
it('should keep standard description as tool description for system tool schema', () => {
const jsonSchema: JSONSchemaInputType = {
type: 'object',
properties: {
sources: {
type: 'array',
items: {
type: 'string',
enum: ['36kr', 'zhihu', 'weibo']
},
title: '热榜来源',
description: '选择热榜来源网站(可多选)'
}
},
required: ['sources']
};
const result = jsonSchema2NodeInput({ jsonSchema, schemaType: 'systemTool' });
expect(result).toEqual([
{
key: 'sources',
label: '热榜来源',
valueType: WorkflowIOValueTypeEnum.arrayString,
description: '选择热榜来源网站(可多选)',
toolDescription: '选择热榜来源网站(可多选)',
required: true,
value: [],
renderTypeList: ['multipleSelect', 'reference'],
list: [
{ label: '36kr', value: '36kr' },
{ label: 'zhihu', value: 'zhihu' },
{ label: 'weibo', value: 'weibo' }
]
}
]);
});
it('should stringify enum options to match node input schema', () => {
const jsonSchema: JSONSchemaInputType = {
type: 'object',
......
......@@ -63,6 +63,7 @@ export async function getToolPreviewNode({
intro: toolDetail.intro,
toolDescription: toolDetail.toolDescription,
courseUrl: toolDetail.courseUrl,
readmeUrl: toolDetail.readmeUrl,
userGuide: toolDetail.userGuide ?? undefined,
showStatus: true,
isTool: true,
......
......@@ -154,7 +154,8 @@ export async function rewriteAppWorkflowToDetail({
status: preview.status,
diagram: preview.diagram,
userGuide: preview.userGuide,
courseUrl: preview.courseUrl
courseUrl: preview.courseUrl,
readmeUrl: preview.readmeUrl
};
node.versionLabel = preview.versionLabel;
node.isLatestVersion = preview.isLatestVersion;
......
......@@ -420,12 +420,32 @@ export const getAgentRuntimeTools = async ({
const jsonSchema = param.valueType
? valueTypeJsonSchemaMap[param.valueType] || toolValueTypeList[0].jsonSchema
: toolValueTypeList[0].jsonSchema;
properties[param.key] = {
const enumValues =
param.list?.map((item) => item.value).filter(Boolean) ||
param.enum?.split('\n').filter(Boolean);
const schema = {
...jsonSchema,
description: param.toolDescription || '',
enum: param.enum?.split('\n').filter(Boolean) || undefined
description: param.toolDescription || ''
};
if (!enumValues?.length) {
properties[param.key] = schema;
return;
}
properties[param.key] =
schema.type === 'array'
? {
...schema,
items: {
...(schema.items && typeof schema.items === 'object' ? schema.items : {}),
enum: enumValues
}
}
: {
...schema,
enum: enumValues
};
});
return {
......
......@@ -3,10 +3,12 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getAgentRuntimeTools } from '@fastgpt/service/core/workflow/dispatch/ai/agent/sub/tool/utils';
import type { NodeToolConfigType } from '@fastgpt/global/core/workflow/type/node';
import { jsonSchema2NodeInput } from '@fastgpt/global/core/app/jsonschema';
const { authAppByTmbIdMock, getAppVersionByIdMock } = vi.hoisted(() => ({
const { authAppByTmbIdMock, getAppVersionByIdMock, getSystemToolDetailMock } = vi.hoisted(() => ({
authAppByTmbIdMock: vi.fn(),
getAppVersionByIdMock: vi.fn()
getAppVersionByIdMock: vi.fn(),
getSystemToolDetailMock: vi.fn()
}));
vi.mock('@fastgpt/service/support/permission/app/auth', () => ({
......@@ -20,7 +22,7 @@ vi.mock('@fastgpt/service/core/app/version/controller', () => ({
vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({
SystemToolRepo: {
getInstance: vi.fn(() => ({
getSystemToolDetail: vi.fn()
getSystemToolDetail: getSystemToolDetailMock
}))
}
}));
......@@ -127,6 +129,7 @@ const createToolsetApp = ({
describe('getAgentRuntimeTools schema loading', () => {
beforeEach(() => {
vi.clearAllMocks();
getSystemToolDetailMock.mockReset();
authAppByTmbIdMock.mockImplementation(async ({ appId }: { appId: string }) => {
const app = appMap[appId];
......@@ -244,4 +247,67 @@ describe('getAgentRuntimeTools schema loading', () => {
expect(tools[0].requestSchema.function.parameters).not.toEqual(httpInputSchema);
expect(tools[0].toolConfig?.httpTool?.toolId).toBe('http-http_app/create');
});
it('loads system tool params from standard JSON schema description', async () => {
getSystemToolDetailMock.mockResolvedValue({
id: 'systemTool-gpjj5s',
name: '热榜工具',
avatar: 'hot-list.png',
intro: '获取热榜信息,支持36氪、知乎、微博、掘金、头条等多个平台',
toolDescription: '获取热榜信息',
status: 'active',
source: 'system',
isToolSet: false,
hasSystemSecret: false,
systemSecretStatus: 'none',
currentCost: 0,
systemKeyCost: 0,
hasTokenFee: false,
tags: [],
author: '',
version: '1.0.0',
isLatestVersion: true,
outputs: [],
inputs: jsonSchema2NodeInput({
schemaType: 'systemTool',
jsonSchema: {
type: 'object',
properties: {
platform: {
type: 'array',
title: '热榜平台',
description: '选择热榜来源网站(可多选)',
items: {
type: 'string',
enum: ['36kr', 'zhihu', 'weibo', 'juejin', 'toutiao']
}
}
},
required: ['platform']
}
})
});
const tools = await getAgentRuntimeTools({
tmbId: 'tmb_1',
tools: [{ id: 'systemTool-gpjj5s', config: {} }]
});
expect(tools).toHaveLength(1);
expect(tools[0].requestSchema.function.name).toBe('gpjj5s');
expect(tools[0].requestSchema.function.parameters).toEqual({
type: 'object',
properties: {
platform: {
type: 'array',
items: {
type: 'string',
enum: ['36kr', 'zhihu', 'weibo', 'juejin', 'toutiao']
},
description: '选择热榜来源网站(可多选)'
}
},
required: ['platform']
});
});
});
/**
* 读取 README 并修正相对图片地址,避免远端 markdown 图片在弹窗中失效。
*/
export const fetchRemoteMarkdown = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch markdown: ${response.status}`);
}
const baseUrl = url.substring(0, url.lastIndexOf('/') + 1);
const content = await response.text();
return content
.replace(/!\[([^\]]*)\]\(\.\/([^)]+)\)/g, (match, alt, path) => `![${alt}](${baseUrl}${path})`)
.replace(
/!\[([^\]]*)\]\((?!http|https|\/\/)([^)]+)\)/g,
(match, alt, path) => `![${alt}](${baseUrl}${path})`
);
};
/**
* Export table data to CSV format
* @param tableElement - HTML table element to export
* @param filename - Name of the exported file (without extension)
......
import React, { useState, useCallback, useEffect } from 'react';
import React, { useState, useCallback } from 'react';
import {
Box,
Button,
......@@ -19,10 +19,10 @@ import MyIconButton from '../../../common/Icon/button';
import LightRowTabs from '../../../common/Tabs/LightRowTabs';
import { type ToolCardItemType } from './ToolCard';
import MyBox from '../../../common/MyBox';
import Markdown from '../../../common/Markdown';
import { useTableMultipleSelect } from '../../../../hooks/useTableMultipleSelect';
import {
ParamSection,
ReadmeBox,
SubToolAccordionItem,
useToolDetail,
drawerScrollbarStyles,
......@@ -76,15 +76,13 @@ const BatchUpdateDrawer: React.FC<BatchUpdateDrawerProps> = ({
autoFetch: viewMode === 'detail'
});
// Reset view mode when drawer closes
useEffect(() => {
if (!isOpen) {
setViewMode('list');
setSelectedToolForDetail(null);
setActiveTab('params');
setSelectedItems([]);
}
}, [isOpen, setSelectedItems]);
const handleClose = useCallback(() => {
setViewMode('list');
setSelectedToolForDetail(null);
setActiveTab('params');
setSelectedItems([]);
onClose();
}, [onClose, setSelectedItems]);
const handleViewDetail = useCallback((tool: ToolCardItemType) => {
setSelectedToolForDetail(tool);
......@@ -111,7 +109,7 @@ const BatchUpdateDrawer: React.FC<BatchUpdateDrawerProps> = ({
}, [selectedToolForDetail, onBatchUpdate, handleBack]);
return (
<Drawer isOpen={isOpen} onClose={onClose} placement="right">
<Drawer isOpen={isOpen} onClose={handleClose} placement="right">
<DrawerOverlay />
<DrawerContent maxW="480px" borderLeftRadius="md">
<DrawerHeader pt={6} pb={1}>
......@@ -121,7 +119,7 @@ const BatchUpdateDrawer: React.FC<BatchUpdateDrawerProps> = ({
{t('app:toolkit_updatable_plugins')}
</Box>
<Box flex={1} />
<MyIconButton icon={'common/closeLight'} onClick={onClose} />
<MyIconButton icon={'common/closeLight'} onClick={handleClose} />
</Flex>
) : (
<Flex gap={1.5}>
......@@ -270,7 +268,12 @@ const BatchUpdateDrawer: React.FC<BatchUpdateDrawerProps> = ({
]}
value={activeTab}
onChange={(value) => {
if (value === 'guide' && parentTool?.courseUrl) {
if (
value === 'guide' &&
parentTool?.courseUrl &&
!parentTool?.readme &&
!parentTool?.userGuide
) {
window.open(parentTool?.courseUrl, '_blank');
} else {
setActiveTab(value as 'guide' | 'params');
......@@ -284,21 +287,11 @@ const BatchUpdateDrawer: React.FC<BatchUpdateDrawerProps> = ({
<Box mt={4}>
{activeTab === 'guide' && (
<VStack align="stretch" spacing={4} flex="1" minH="0">
{(readmeContent || parentTool?.userGuide) && (
<Box
px={4}
py={3}
border="1px solid"
borderColor="myGray.200"
borderRadius="md"
bg="myGray.50"
fontSize="sm"
color="myGray.900"
flex="1"
overflowY="auto"
>
<Markdown source={readmeContent || parentTool?.userGuide || ''} />
</Box>
{(parentTool?.readme || readmeContent || parentTool?.userGuide) && (
<ReadmeBox
source={readmeContent || parentTool?.userGuide || ''}
courseUrl={parentTool?.courseUrl}
/>
)}
</VStack>
)}
......
import React from 'react';
import { Box, Flex, Link } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '../../../../common/Icon';
import Markdown from '../../../../common/Markdown';
const ReadmeBox = ({
source,
courseUrl,
maxH
}: {
source: string;
courseUrl?: string;
maxH?: string | number;
}) => {
const { t } = useTranslation();
return (
<Box
px={4}
py={3}
border="1px solid"
borderColor="myGray.200"
borderRadius="md"
bg="myGray.50"
fontSize="sm"
color="myGray.900"
flex="1"
overflowY="auto"
maxH={maxH}
>
{courseUrl && (
<Flex
as={Link}
href={courseUrl}
isExternal
alignItems="center"
gap={1.5}
mb={3}
color="primary.600"
fontSize="sm"
textDecoration="none"
_hover={{ textDecoration: 'underline' }}
>
<MyIcon name="common/link" w={4} />
<Box>{t('app:toolkit_tutorial_link')}</Box>
</Flex>
)}
<Markdown source={source} />
</Box>
);
};
export default React.memo(ReadmeBox);
......@@ -10,6 +10,7 @@ import {
jsonSchema2NodeOutput,
jsonSchema2SecretInput
} from '@fastgpt/global/core/app/jsonschema';
import { fetchRemoteMarkdown } from '../../../../common/Markdown/utils';
export type UseToolDetailProps = {
toolId?: string;
......@@ -149,23 +150,7 @@ export const useToolDetail = ({
}
try {
const response = await fetch(readmeUrl);
if (!response.ok) {
throw new Error(`Failed to fetch README: ${response.status}`);
}
let content = await response.text();
const baseUrl = readmeUrl.substring(0, readmeUrl.lastIndexOf('/') + 1);
content = content.replace(
/!\[([^\]]*)\]\(\.\/([^)]+)\)/g,
(match, alt, path) => `![${alt}](${baseUrl}${path})`
);
content = content.replace(
/!\[([^\]]*)\]\((?!http|https|\/\/)([^)]+)\)/g,
(match, alt, path) => `![${alt}](${baseUrl}${path})`
);
setReadmeContent(content);
setReadmeContent(await fetchRemoteMarkdown(readmeUrl));
} catch (error) {
console.error('Failed to fetch README:', error);
setReadmeContent('');
......
......@@ -2,3 +2,4 @@ export * from './types';
export * from './components';
export * from './hooks';
export * from './styles';
export { default as ReadmeBox } from './ReadmeBox';
......@@ -20,10 +20,10 @@ import MyMenu from '../../../common/MyMenu';
import LightRowTabs from '../../../common/Tabs/LightRowTabs';
import { type ToolCardItemType } from './ToolCard';
import MyBox from '../../../common/MyBox';
import Markdown from '../../../common/Markdown';
import { useRequest } from '../../../../hooks/useRequest';
import {
ParamSection,
ReadmeBox,
SubToolAccordionItem,
useToolDetail,
drawerScrollbarStyles,
......@@ -88,21 +88,17 @@ const ToolDetailDrawer = ({
}
}, [selectedTool.id]);
useEffect(() => {
if (!selectedVersion && toolVersions[0]?.version) {
setSelectedVersion(toolVersions[0].version);
}
}, [selectedVersion, toolVersions]);
const activeVersion = selectedVersion || toolVersions[0]?.version;
// Use tool detail hook
const { parentTool, isToolSet, subTools, readmeContent, loadingDetail } = useToolDetail({
toolId: selectedTool.id,
version: selectedVersion,
version: activeVersion,
tags: selectedTool.tags || undefined,
onFetchDetail
});
const currentVersion = selectedVersion || parentTool?.version || selectedTool.version;
const currentVersion = activeVersion || parentTool?.version || selectedTool.version;
return (
<Drawer isOpen={true} onClose={onClose} placement="right">
......@@ -266,7 +262,12 @@ const ToolDetailDrawer = ({
]}
value={activeTab}
onChange={(value) => {
if (value === 'guide' && parentTool?.courseUrl) {
if (
value === 'guide' &&
parentTool?.courseUrl &&
!parentTool?.readme &&
!parentTool?.userGuide
) {
window.open(parentTool?.courseUrl, '_blank');
} else {
setActiveTab(value as 'guide' | 'params');
......@@ -280,21 +281,11 @@ const ToolDetailDrawer = ({
<Box mt={4}>
{activeTab === 'guide' && (
<VStack align="stretch" spacing={4} flex="1" minH="0">
{(readmeContent || parentTool?.userGuide) && (
<Box
px={4}
py={3}
border="1px solid"
borderColor="myGray.200"
borderRadius="md"
bg="myGray.50"
fontSize="sm"
color="myGray.900"
flex="1"
overflowY="auto"
>
<Markdown source={readmeContent || parentTool?.userGuide || ''} />
</Box>
{(parentTool?.readme || readmeContent || parentTool?.userGuide) && (
<ReadmeBox
source={readmeContent || parentTool?.userGuide || ''}
courseUrl={parentTool?.courseUrl}
/>
)}
</VStack>
)}
......
......@@ -494,6 +494,7 @@
"toolkit_updatable_plugins": "Updatable Plugins",
"toolkit_update_failed": "Update failed",
"toolkit_user_guide": "User Guide",
"toolkit_tutorial_link": "Open tutorial link",
"toolkit_version_description": "Version Description",
"toolkit_version_info": "Version Information",
"tools": "tool",
......
......@@ -494,6 +494,7 @@
"toolkit_updatable_plugins": "可更新的插件",
"toolkit_update_failed": "更新失败",
"toolkit_user_guide": "使用说明",
"toolkit_tutorial_link": "查看教程链接",
"toolkit_version_description": "版本描述",
"toolkit_version_info": "版本信息",
"tools": "工具",
......
......@@ -482,6 +482,7 @@
"toolkit_updatable_plugins": "可更新的外掛程式",
"toolkit_update_failed": "更新失敗",
"toolkit_user_guide": "使用說明",
"toolkit_tutorial_link": "查看教學連結",
"toolkit_version_description": "版本描述",
"toolkit_version_info": "版本資訊",
"tools": "工具",
......
Subproject commit 5151345e0b0c883f3b812194430598dfe3113e65
Subproject commit 35458b567a3b719987bdd4e9a18dc747a8b34495
import { Box, ModalBody, useDisclosure } from '@chakra-ui/react';
import Markdown from '@/components/Markdown';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/v2/common/MyModal';
import { getDocPath } from '@/web/common/system/doc';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { fetchRemoteMarkdown } from '@fastgpt/web/components/common/Markdown/utils';
import ReadmeBox from '@fastgpt/web/components/core/plugin/tool/ToolDetail/ReadmeBox';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
const UseGuideModal = ({
children,
title,
iconSrc,
text,
link
link,
readmeUrl
}: {
children: ({ onClick }: { onClick: () => void }) => React.ReactNode;
title?: string;
iconSrc?: string;
text?: string;
link?: string;
readmeUrl?: string;
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const {
data: readmeContent = '',
loading: loadingReadme,
run: fetchReadme
} = useRequest(fetchRemoteMarkdown, {
manual: true,
errorToast: ''
});
const courseUrl = useMemo(() => (link ? getDocPath(link) : undefined), [link]);
const guideContent = readmeContent || text;
const titleContent = useMemo(() => {
if (!iconSrc) return title;
return (
<Flex alignItems={'center'} gap={3}>
<Avatar src={iconSrc} w={'20px'} borderRadius={'sm'} />
<Box>{title}</Box>
</Flex>
);
}, [iconSrc, title]);
const onClick = useCallback(() => {
if (link) {
return window.open(getDocPath(link), '_blank');
if (readmeUrl) {
fetchReadme(readmeUrl);
return onOpen();
}
if (text) {
return onOpen();
}
}, [link, text, onOpen]);
if (courseUrl) {
return window.open(courseUrl, '_blank');
}
}, [courseUrl, fetchReadme, readmeUrl, text, onOpen]);
return (
<>
{children({ onClick })}
{isOpen && (
<MyModal isOpen iconSrc={iconSrc} title={title} onClose={onClose} minW={'600px'}>
<ModalBody>
<Box border={'base'} borderRadius={'10px'} p={4} minH={'500px'}>
<Markdown source={text} />
</Box>
</ModalBody>
<MyModal
isOpen
isCentered
size={'lg'}
title={titleContent}
onClose={onClose}
h={['90vh', '80vh']}
maxH={['90vh', '700px']}
bodyStyles={{
px: [5, 8],
pt: [4, 6],
pb: [5, 8],
minH: 0
}}
>
<MyBox isLoading={loadingReadme} minH={0} flex={1} display={'flex'}>
<ReadmeBox source={guideContent || ''} courseUrl={readmeUrl ? courseUrl : undefined} />
</MyBox>
</MyModal>
)}
</>
......
......@@ -175,6 +175,8 @@ export const appWorkflow2Form = ({
templateType: FlowNodeTemplateTypeEnum.other,
pluginData: node.pluginData,
courseUrl: node.pluginData?.courseUrl,
readmeUrl: node.pluginData?.readmeUrl,
userGuide: node.pluginData?.userGuide,
toolConfig: node.toolConfig,
hasSystemSecret: node.hasSystemSecret,
systemKeyCost: node.systemKeyCost,
......@@ -609,6 +611,9 @@ export function form2AppWorkflow(
},
toolConfig: tool.toolConfig,
pluginData: tool.pluginData,
courseUrl: tool.courseUrl,
readmeUrl: tool.readmeUrl,
userGuide: tool.userGuide,
inputs: tool.inputs.map((input) => {
// Special key value
if (input.key === NodeInputKeyEnum.forbidStream) {
......
import { Button, Flex, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { childAppSystemKey } from '../FormComponent/ToolSelector/ToolSelectModal';
......@@ -59,12 +58,13 @@ const ConfigToolModal = ({
<HStack mb={4} spacing={1} fontSize={'sm'}>
<MyIcon name={'common/info'} color={'primary.600'} w={'1.25rem'} />
<Box flex={1}>{t('app:tool_input_param_tip')}</Box>
{!!(configTool?.courseUrl || configTool?.userGuide) && (
{!!(configTool?.courseUrl || configTool?.readmeUrl || configTool?.userGuide) && (
<UseGuideModal
title={configTool?.name}
iconSrc={configTool?.avatar}
text={configTool?.userGuide}
link={configTool?.courseUrl}
readmeUrl={configTool?.readmeUrl}
>
{({ onClick }) => (
<Box cursor={'pointer'} color={'primary.500'} onClick={onClick}>
......@@ -135,6 +135,7 @@ const ConfigToolModal = ({
hasSystemSecret={configTool?.hasSystemSecret}
secretCost={configTool?.systemKeyCost}
courseUrl={configTool?.courseUrl}
readmeUrl={configTool?.readmeUrl}
parentId={configTool?.pluginId}
onClose={setFalseSecretModal}
onSubmit={(data) => {
......
......@@ -65,6 +65,7 @@ const ToolConfig = ({ nodeId, inputs }: { nodeId?: string; inputs?: FlowNodeInpu
hasSystemSecret={node?.hasSystemSecret}
secretCost={node?.systemKeyCost}
courseUrl={node?.courseUrl}
readmeUrl={node?.readmeUrl}
parentId={node?.pluginId}
onClose={setFalse}
onSubmit={onSubmit}
......
......@@ -25,7 +25,7 @@ export const useAgentSkillManager = ({
);
const selectedTools: SelectedToolItemType[] = useMemo(
() => (Array.isArray(toolsInput?.value) ? toolsInput!.value : []),
[toolsInput?.value]
[toolsInput]
);
const fileLinkInput = useMemo(() => inputs.find((i) => i.key === 'fileLink'), [inputs]);
......
......@@ -262,13 +262,27 @@ const NodeCard = (props: Props) => {
{
onSuccess(res) {
if (!res) return;
// Execute forcibly updates the courseUrl field
onChangeNode({
nodeId,
type: 'attr',
key: 'courseUrl',
value: res?.courseUrl
});
// 教程元信息由工具详情实时回写,兼容已保存的旧节点。
onChangeNode([
{
nodeId,
type: 'attr',
key: 'courseUrl',
value: res?.courseUrl
},
{
nodeId,
type: 'attr',
key: 'readmeUrl',
value: res?.readmeUrl
},
{
nodeId,
type: 'attr',
key: 'userGuide',
value: res?.userGuide
}
]);
},
manual: false
}
......@@ -367,6 +381,7 @@ const NodeCard = (props: Props) => {
<NodeActionButtons
nodeTemplate={nodeTemplate}
courseUrl={node?.courseUrl}
readmeUrl={node?.readmeUrl}
rtDoms={rtDoms}
/>
......@@ -392,6 +407,7 @@ const NodeCard = (props: Props) => {
nodeId={nodeId}
isFolder={node?.isFolder}
courseUrl={node?.courseUrl}
readmeUrl={node?.readmeUrl}
hasSystemSecret={node?.hasSystemSecret}
pluginId={node?.pluginId}
systemKeyCost={node?.systemKeyCost}
......@@ -778,7 +794,8 @@ const MenuRender = React.memo(function MenuRender({
currentCost: node.data.currentCost,
systemKeyCost: node.data.systemKeyCost,
hasTokenFee: node.data.hasTokenFee,
hasSystemSecret: node.data.hasSystemSecret
hasSystemSecret: node.data.hasSystemSecret,
readmeUrl: node.data.readmeUrl
};
return [
......@@ -926,10 +943,12 @@ const NodeActionButtons = React.memo<{
name?: string;
avatar?: string;
courseUrl?: string;
readmeUrl?: string;
};
courseUrl?: string;
readmeUrl?: string;
rtDoms?: React.ReactNode[];
}>(({ nodeTemplate, courseUrl, rtDoms }) => {
}>(({ nodeTemplate, courseUrl, readmeUrl, rtDoms }) => {
const { t } = useTranslation();
const buttons = useMemo(() => {
......@@ -950,14 +969,18 @@ const NodeActionButtons = React.memo<{
);
}
if (courseUrl || nodeTemplate?.userGuide) {
const guideReadmeUrl = nodeTemplate?.readmeUrl || readmeUrl;
const guideCourseUrl = nodeTemplate?.courseUrl || courseUrl;
if (guideCourseUrl || guideReadmeUrl || nodeTemplate?.userGuide) {
result.push(
<UseGuideModal
key="userGuide"
title={nodeTemplate?.name}
iconSrc={nodeTemplate?.avatar}
text={nodeTemplate?.userGuide}
link={nodeTemplate?.courseUrl || courseUrl}
link={guideCourseUrl}
readmeUrl={guideReadmeUrl}
>
{({ onClick }) => (
<MyTooltip label={t('workflow:Node.Open_Node_Course')}>
......@@ -973,7 +996,7 @@ const NodeActionButtons = React.memo<{
}
return result;
}, [nodeTemplate, courseUrl, rtDoms, t]);
}, [nodeTemplate, courseUrl, readmeUrl, rtDoms, t]);
if (buttons.length === 0) {
return null;
......@@ -1042,6 +1065,7 @@ const NodeSecret = React.memo(function NodeSecret({
nodeId,
isFolder,
courseUrl,
readmeUrl,
hasSystemSecret,
pluginId,
systemKeyCost,
......@@ -1050,6 +1074,7 @@ const NodeSecret = React.memo(function NodeSecret({
nodeId: string;
isFolder?: boolean;
courseUrl?: string;
readmeUrl?: string;
hasSystemSecret?: boolean;
pluginId?: string;
systemKeyCost?: number;
......@@ -1098,6 +1123,7 @@ const NodeSecret = React.memo(function NodeSecret({
onCloseToolParamConfigModal();
}}
courseUrl={courseUrl}
readmeUrl={readmeUrl}
inputConfig={inputConfig}
hasSystemSecret={hasSystemSecret}
parentId={pluginId}
......
......@@ -26,6 +26,7 @@ import { getAppToolTemplates } from '@/web/core/app/api/tool';
import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { InputTypeEnum } from '@/components/core/app/formRender/constant';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
export type ToolParamsFormType = {
type: SystemToolSecretInputTypeEnum;
......@@ -39,6 +40,7 @@ const SecretInputModal = ({
isFolder,
inputConfig,
courseUrl,
readmeUrl,
onClose,
onSubmit
}: {
......@@ -48,6 +50,7 @@ const SecretInputModal = ({
hasSystemSecret?: boolean;
secretCost?: number;
courseUrl?: string;
readmeUrl?: string;
onClose: () => void;
onSubmit: (data: ToolParamsFormType) => void;
}) => {
......@@ -176,36 +179,46 @@ const SecretInputModal = ({
]
: []),
{
title: courseUrl ? (
<HStack
spacing={2}
color={'myGray.900'}
fontWeight={'500'}
whiteSpace={'nowrap'}
fontSize={'sm'}
lineHeight={1}
>
<Box>{t('app:manual_secret')}</Box>
title:
courseUrl || readmeUrl ? (
<HStack
spacing={1}
color={'primary.600'}
justifyContent={'flex-end'}
_hover={{
textDecoration: 'underline',
cursor: 'point'
}}
onClick={(e) => {
e.stopPropagation();
window.open(courseUrl, '_blank');
}}
spacing={2}
color={'myGray.900'}
fontWeight={'500'}
whiteSpace={'nowrap'}
fontSize={'sm'}
lineHeight={1}
>
<MyIcon name={'book'} w={'14px'} />
<Box fontSize={'sm'}>{t('app:secret_get_course')}</Box>
<Box>{t('app:manual_secret')}</Box>
<UseGuideModal
title={t('app:manual_secret')}
iconSrc="key"
link={courseUrl}
readmeUrl={readmeUrl}
>
{({ onClick }) => (
<HStack
spacing={1}
color={'primary.600'}
justifyContent={'flex-end'}
_hover={{
textDecoration: 'underline',
cursor: 'pointer'
}}
onClick={(e) => {
e.stopPropagation();
onClick();
}}
>
<MyIcon name={'book'} w={'14px'} />
<Box fontSize={'sm'}>{t('app:secret_get_course')}</Box>
</HStack>
)}
</UseGuideModal>
</HStack>
</HStack>
) : (
t('app:manual_secret')
),
) : (
t('app:manual_secret')
),
desc: t('app:tool_active_manual_config_desc'),
value: SystemToolSecretInputTypeEnum.manual,
children:
......
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