V4.14.0 features (#5850)

* feat: migrate chat files to s3 (#5802)

* feat: migrate chat files to s3

* feat: add delete jobs for deleting s3 files

* chore: improvements

* fix: lockfile

* fix: imports

* feat: add ttl for those uploaded files but not send yet

* feat: init bullmq worker

* fix: s3 key

* perf: s3 internal url

* remove env

* fix: re-sign a new url

* fix: re-sign a new url

* perf: s3 code

---------

Co-authored-by: archer <545436317@qq.com>

* update pacakge

* feat: add more file type for uploading (#5807)

* fix: re-sign a new url

* wip: file selector

* feat: add more file type for uploading

* feat: migrate chat files to s3 (#5802)

* feat: migrate chat files to s3

* feat: add delete jobs for deleting s3 files

* chore: improvements

* fix: lockfile

* fix: imports

* feat: add ttl for those uploaded files but not send yet

* feat: init bullmq worker

* fix: s3 key

* perf: s3 internal url

* remove env

* fix: re-sign a new url

* fix: re-sign a new url

* perf: s3 code

---------

Co-authored-by: archer <545436317@qq.com>

* fix: limit minmax available file upload number

* perf: file select modal code

* fix: fileselect refresh

* fix: ts

---------

Co-authored-by: archer <545436317@qq.com>

* bugfix: chat page (#5809)

* fix: upload avatar

* fix: chat page username display issue and setting button visibility

* doc

* Markdown match base64 performance

* feat: improve global variables(time, file, dataset) (#5804)

* feat: improve global variables(time, file, dataset)

* feat: optimize code

* perf: time variables code

* fix: model, file

* fix: hide file upload

* fix: ts

* hide dataset select

---------

Co-authored-by: archer <545436317@qq.com>

* perf: insert training queue

* perf: s3 upload error i18n

* fix: share page s3

* fix: timeselector ui error

* var update node

* Timepicker ui

* feat: plugin support password

* fix: password disabled UX

* fix: button size

* fix: no model cache for chat page (#5820)

* rename function

* fix: workflow bug

* fix: interactive loop

* fix test

* perf: common textare no richtext

* move system plugin config (#5803) (#5813)

* move system plugin config (#5803)

* move system plugin config

* extract tag bar

* filter

* tool detail temp

* marketplace

* params

* fix

* type

* search

* tags render

* status

* ui

* code

* connect to backend (#5815)

* feat: marketplace apis & type definitions (#5817)

* chore: marketplace init

* chore: marketplace list api type

* chore: detail api

* marketplace & import

* feat: marketplace ui (#5826)

* temp

* marketplace

* import

* feat: detail return readme

* chore: cache data expire 10 mins

* chore: update docs

* feat: marketplace ui

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* feat: marketplace (#5830)

* temp

* marketplace

* chore: tool list tag filter

* chore: adjust

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* tool detail drawer

* remove tag filter

* fix

* fix

* fix build

* update pnpm-lock

* fix type

* perf code

* marketplace router

* fix build

* navbar icon

* fix ui

* fix init

* docs: marketplace/plugin (#5832)

* temp

* marketplace

* docs(plugin): system tool docs

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* default url

* feat: i18n/ docker build (#5833)

* chore: docker build

* feat: i18n selector

* fix

* fix

* fix: i18n parse

* fix: i18n parse

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>

* marketplace url

* update action

* market place code

* market place code

* title

* fix: nextconfig

* fix: copilot review

* Remove bypassable regex-based XSS sanitization from marketplace search (#5835)

* Initial plan

* Remove problematic regex-based XSS sanitization from search inputs

Co-authored-by: c121914yu <50446880+c121914yu@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: c121914yu <50446880+c121914yu@users.noreply.github.com>

* feat: tool tag openapi

* api check

* fix: tsc

* fix: ts

* fix: lock

* sdk version

* ts

* sdk version

* remove invalid tip

* perf: export data add timezone

* perf: admin plugin api move

* perf: tool code

* move tag code

* perf: marketplace and team plugin code

* remove workflow invalid request

* rename global tool code

* rename global tool code

* rename api

* fix some bugs (#5841)

* fix some bugs

* fix

* perf: Tag filter

* fix: ts

* fix: ts

---------

Co-authored-by: archer <545436317@qq.com>

* perf: Concat function

* fix: workflow snapshot push

* fix: ts type

* fix: login to config/*

* fix: ts

* fix: model avatar (#5848)

* fix: model avatar

* fix: ts

* fix: avatar migration to s3

* update lock

* fix: avatar redirect

---------

Co-authored-by: archer <545436317@qq.com>

* fix tool detail (#5847)

* fix tool detail

* init script

* fix build

* perf: plugin detail modal

* change tooltags to tags

* fix icon

---------

Co-authored-by: archer <545436317@qq.com>

* fix tag filter scroll (#5852)

* fix create app plugin & import info (#5853)

* tag size

* rename toolkit

* download url

* import plugin status (#5854)

* init doc

* fix: init shell

---------

Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Archer
2025-11-04 16:58:12 +08:00
committed by GitHub
parent fac170306e
commit a499d05a02
364 changed files with 15051 additions and 3514 deletions
@@ -0,0 +1,94 @@
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import type { SystemPluginToolTagType } from '@fastgpt/global/core/plugin/type';
import React, { useMemo } from 'react';
const ToolTagFilterBox = ({
tags,
selectedTagIds,
onTagSelect,
size = 'base'
}: {
tags: SystemPluginToolTagType[];
selectedTagIds: string[];
onTagSelect: (tagIds: string[]) => void;
size?: 'base' | 'sm';
}) => {
const { t, i18n } = useTranslation();
const toggleTag = (tagId: string) => {
if (selectedTagIds.includes(tagId)) {
onTagSelect(selectedTagIds.filter((id) => id !== tagId));
} else {
onTagSelect([...selectedTagIds, tagId]);
}
};
const tagBaseStyles = useMemo(() => {
const sizeStyles = {
base: {
px: 3,
py: 1.5,
fontSize: 'sm'
},
sm: {
px: 2,
py: 1,
fontSize: 'xs'
}
};
return {
...sizeStyles[size],
fontWeight: 'medium',
color: 'myGray.700',
border: '1px solid',
borderColor: 'myGray.200',
whiteSpace: 'nowrap',
flexShrink: 0,
cursor: 'pointer'
};
}, [size]);
return (
<Flex alignItems={'center'} userSelect={'none'}>
<Box
{...tagBaseStyles}
rounded={'sm'}
bg={selectedTagIds.length === 0 ? 'myGray.150' : 'transparent'}
onClick={() => onTagSelect([])}
>
{t('common:All')}
</Box>
<Box mx={2} h={'20px'} w={'1px'} bg={'myGray.200'} />
<Flex
gap={2}
flex={1}
overflowX="auto"
flexWrap="nowrap"
css={{
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}}
>
{tags.map((tag) => {
const isSelected = selectedTagIds.includes(tag.tagId);
return (
<Box
key={tag.tagId}
{...tagBaseStyles}
rounded={'full'}
bg={isSelected ? 'myGray.150 !important' : 'transparent'}
onClick={() => toggleTag(tag.tagId)}
>
{t(parseI18nString(tag.tagName, i18n.language))}
</Box>
);
})}
</Flex>
</Flex>
);
};
export default React.memo(ToolTagFilterBox);
@@ -0,0 +1,232 @@
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import Avatar from '../../../common/Avatar';
import MyBox from '../../../common/MyBox';
import React, { useMemo, useRef, useState, useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '../../../common/Icon';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type';
/*
3 种使用场景:
1. admin 视角插件市场:显示是否安装,无状态,显示安装/卸载
2. team 视角资源库:显示是否安装,状态文本,以及安装/卸载
3. 开放的插件市场:不显示任何状态,只显示下载按钮
*/
export type ToolCardItemType = {
id: string;
name: string;
description?: string;
icon?: string;
author?: string;
tags?: string[] | null;
downloadUrl?: string;
status?: number;
installed?: boolean;
};
const ToolCard = ({
item,
systemTitle,
isLoading,
mode,
onClickButton,
onClickCard
}: {
item: ToolCardItemType;
systemTitle?: string;
isLoading?: boolean;
mode: 'admin' | 'team' | 'marketplace';
onClickButton: (installed: boolean) => void;
onClickCard?: () => void;
}) => {
const { t, i18n } = useTranslation();
const tagsContainerRef = useRef<HTMLDivElement>(null);
const [visibleTagsCount, setVisibleTagsCount] = useState(item.tags?.length || 0);
useEffect(() => {
const calculate = () => {
const container = tagsContainerRef.current;
if (!container || !item.tags?.length) return;
const containerWidth = container.offsetWidth;
const tagElements = container.querySelectorAll('[data-tag-item]');
if (!containerWidth || !tagElements.length) return;
let totalWidth = 0;
let count = 0;
for (let i = 0; i < tagElements.length; i++) {
const width = totalWidth + (tagElements[i] as HTMLElement).offsetWidth + (i > 0 ? 4 : 0);
if (width + (i < tagElements.length - 1 ? 54 : 0) > containerWidth) break;
totalWidth = width;
count++;
}
setVisibleTagsCount(Math.max(1, count));
};
const timer = setTimeout(calculate, 0);
const observer = new ResizeObserver(calculate);
if (tagsContainerRef.current) observer.observe(tagsContainerRef.current);
return () => {
clearTimeout(timer);
observer.disconnect();
};
}, [item.tags]);
const statusMap = useMemo(() => {
if (mode === 'marketplace') return null;
const pluginStatusMap: Record<number, { label: string; color: string; icon?: string } | null> =
{
[PluginStatusEnum.Offline]: {
label: t('app:toolkit_status_offline'),
color: 'red.600'
},
[PluginStatusEnum.SoonOffline]: {
label: t('app:toolkit_status_soon_offline'),
color: 'yellow.600'
}
};
const installedStatusMap = item.installed
? {
label: t('app:toolkit_installed'),
color: 'myGray.500',
icon: 'common/check'
}
: null;
if (mode === 'admin') {
return installedStatusMap;
}
if (mode === 'team') {
if (item.status && pluginStatusMap[item.status]) {
return pluginStatusMap[item.status];
}
return installedStatusMap;
}
}, [item.installed, item.status]);
return (
<MyBox
key={item.id}
p={4}
pb={3}
border={'base'}
bg={'white'}
borderRadius={'10px'}
display={'flex'}
flexDirection={'column'}
cursor={onClickCard ? 'pointer' : 'default'}
onClick={onClickCard}
_hover={{
boxShadow: '0 4px 4px 0 rgba(19, 51, 107, 0.05), 0 0 1px 0 rgba(19, 51, 107, 0.08);',
'& .install-button': {
display: 'flex'
}
}}
>
<HStack>
<Avatar src={item.icon} borderRadius={'sm'} w={'1.5rem'} />
<Box color={'myGray.900'} fontWeight={'medium'}>
{parseI18nString(item.name, i18n.language)}
</Box>
{statusMap && (
<Flex fontSize={'12px'} fontWeight={'medium'} color={statusMap.color} gap={1}>
{statusMap.icon && <MyIcon name={statusMap.icon as any} w={4} />}
{statusMap.label}
</Flex>
)}
</HStack>
<Box
flex={['1 0 48px', '1 0 56px']}
mt={3}
pr={1}
textAlign={'justify'}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.500'}
>
<Box className={'textEllipsis2'}>
{parseI18nString(item.description || '', i18n.language) ||
t('app:templateMarket.no_intro')}
</Box>
</Box>
<Flex gap={1} overflow={'hidden'} ref={tagsContainerRef}>
{item.tags?.slice(0, visibleTagsCount).map((tag) => {
return (
<Box
key={tag}
px={2}
py={1}
border={'1px solid'}
borderRadius={'6px'}
borderColor={'myGray.200'}
fontSize={'11px'}
fontWeight={'medium'}
color={'myGray.700'}
flexShrink={0}
data-tag-item
>
{tag}
</Box>
);
})}
{item.tags && item.tags.length > visibleTagsCount && (
<Box
px={2}
py={1}
border={'1px solid'}
borderRadius={'6px'}
borderColor={'myGray.200'}
fontSize={'11px'}
fontWeight={'medium'}
color={'myGray.700'}
flexShrink={0}
>
+{item.tags.length - visibleTagsCount}
</Box>
)}
</Flex>
<Flex w={'full'} fontSize={'mini'} alignItems={'end'} justifyContent={'space-between'}>
<Box color={'myGray.500'} mt={3}>{`by ${item.author || systemTitle || 'FastGPT'}`}</Box>
{mode === 'marketplace' ? (
<Button
className="install-button"
size={'sm'}
variant={'primary'}
onClick={(e) => {
e.stopPropagation();
onClickButton(false);
}}
isLoading={isLoading}
{...(!isLoading ? { display: 'none' } : {})}
>
{t('common:Download')}
</Button>
) : (
<Button
className="install-button"
{...(!isLoading ? { display: 'none' } : {})}
size={'sm'}
variant={item.installed ? 'primaryOutline' : 'primary'}
onClick={(e) => {
e.stopPropagation();
onClickButton(!item.installed);
}}
isLoading={isLoading}
>
{item.installed ? t('app:toolkit_uninstall') : t('app:toolkit_install')}
</Button>
)}
</Flex>
</MyBox>
);
};
export default React.memo(ToolCard);
@@ -0,0 +1,458 @@
import React, { useMemo, useState, useEffect } from 'react';
import {
Box,
Button,
Drawer,
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerOverlay,
Flex,
VStack,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import Avatar from '../../../common/Avatar';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import MyIconButton from '../../../common/Icon/button';
import LightRowTabs from '../../../common/Tabs/LightRowTabs';
import type {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io';
import { type ToolCardItemType } from './ToolCard';
import MyBox from '../../../common/MyBox';
import Markdown from '../../../common/Markdown';
import type { ToolDetailType } from '@fastgpt/global/sdk/fastgpt-plugin';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import type { GetTeamToolDetailResponseType } from '@fastgpt/global/openapi/core/plugin/team/toolApi';
import type { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
type toolDetailType = ToolDetailType & {
versionList?: Array<{
value: string;
description?: string;
inputs?: Array<FlowNodeInputItemType>;
outputs?: Array<FlowNodeOutputItemType>;
}>;
courseUrl?: string;
readme?: string;
userGuide?: string;
currentCost?: number;
hasSystemSecret?: boolean;
secretInputConfig?: Array<{}>;
inputList?: Array<FlowNodeInputItemType>;
};
const ParamSection = ({
title,
params
}: {
title: string;
params: (FlowNodeInputItemType | FlowNodeOutputItemType)[];
}) => {
const { i18n } = useTranslation();
return (
<VStack
align="stretch"
p={4}
gap={0}
border="1px solid"
borderColor="myGray.200"
borderRadius="md"
bg="myGray.50"
>
<Flex alignItems="center" gap={2} mb={4}>
<Box w="4px" h="16px" bg="primary.600" borderRadius="2px" flexShrink={0} />
<Box fontSize="sm" color="myGray.900">
{title}
</Box>
</Flex>
{params.map((param, index) => {
const isInput = 'required' in param;
return (
<Box key={index}>
<Flex alignItems="center" gap={2} mb={1}>
{isInput && param.required && (
<Box as="span" color="red.500" fontSize="xs" fontWeight="medium" ml={-2} mr={-1}>
*
</Box>
)}
<Box fontWeight={500}>{parseI18nString(param.label || param.key, i18n.language)}</Box>
<Box
px={1}
borderRadius="4px"
fontSize={'11px'}
color="myGray.500"
bg={'myGray.100'}
border={'1px solid'}
borderColor={'myGray.200'}
>
{FlowValueTypeMap[param.valueType as WorkflowIOValueTypeEnum]?.label || 'String'}
</Box>
</Flex>
{param.description && (
<Box fontSize="sm" color="myGray.500" mt={1}>
{parseI18nString(param.description, i18n.language)}
</Box>
)}
{index !== params.length - 1 && <Box h={'1px'} w={'full'} bg={'myGray.200'} my={4} />}
</Box>
);
})}
</VStack>
);
};
const SubToolAccordionItem = ({ tool }: { tool: any }) => {
const { t, i18n } = useTranslation();
return (
<AccordionItem borderRadius="md" mb={2} border={'none'}>
<AccordionButton
px={2}
py={2}
_hover={{ bg: 'myGray.50' }}
borderRadius="md"
alignItems={'center'}
>
<Box flex={1} textAlign="left">
<Box fontSize="md" color="myGray.900">
{parseI18nString(tool.name, i18n.language)}
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
{tool.intro || parseI18nString(tool.description, i18n.language)}
</Box>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel px={2} pb={4} pt={0}>
{/* <Flex gap={1} fontSize={'12px'}>
<MyIcon name={'common/info'} color={'primary.600'} w={4} />
{!!tool?.currentCost ? (
<Flex gap={1}>
<Box>{t('app:toolkit_call_points_label')}</Box>
{tool?.currentCost}
</Flex>
) : (
t('app:toolkit_no_call_points')
)}
</Flex> */}
{tool.versionList && tool.versionList.length > 0 && (
<VStack align="stretch" spacing={3} mt={3}>
{tool.versionList[0]?.inputs && tool.versionList[0].inputs.length > 0 && (
<ParamSection title={t('app:toolkit_inputs')} params={tool.versionList[0].inputs} />
)}
{tool.versionList[0]?.outputs && tool.versionList[0].outputs.length > 0 && (
<ParamSection title={t('app:toolkit_outputs')} params={tool.versionList[0].outputs} />
)}
</VStack>
)}
</AccordionPanel>
</AccordionItem>
);
};
const ToolDetailDrawer = ({
onClose,
selectedTool,
onToggleInstall,
systemTitle,
onFetchDetail,
isLoading,
showPoint,
mode
}: {
onClose: () => void;
selectedTool: ToolCardItemType;
onToggleInstall: (installed: boolean) => void;
systemTitle?: string;
onFetchDetail?: (toolId: string) => Promise<GetTeamToolDetailResponseType>;
isLoading?: boolean;
showPoint: boolean;
mode: 'admin' | 'team' | 'marketplace';
}) => {
const { t, i18n } = useTranslation();
const [activeTab, setActiveTab] = useState<'guide' | 'params'>('params');
const [toolDetail, setToolDetail] = useState<
{ tools: Array<toolDetailType & { readme: string }>; downloadUrl: string } | undefined
>(undefined);
const [loading, setLoading] = useState(false);
const [readmeContent, setReadmeContent] = useState<string>('');
const [isInstalled, setIsInstalled] = useState(selectedTool.installed);
const isDownload = useMemo(() => {
return mode === 'marketplace';
}, [mode]);
useEffect(() => {
const fetchToolDetail = async () => {
if (onFetchDetail && selectedTool?.id) {
setLoading(true);
try {
const detail = await onFetchDetail(selectedTool.id);
setToolDetail(detail as any);
} finally {
setLoading(false);
}
}
};
fetchToolDetail();
}, []);
const isToolSet = useMemo(() => {
if (!toolDetail?.tools || !Array.isArray(toolDetail?.tools) || toolDetail?.tools.length === 0) {
return false;
}
const subTools = toolDetail?.tools.filter((subTool: any) => subTool.parentId);
return subTools.length > 0;
}, [toolDetail?.tools]);
const parentTool = useMemo(() => {
const parentTool = toolDetail?.tools.find((tool: toolDetailType) => !tool.parentId);
return {
...parentTool,
tags: selectedTool.tags
};
}, [selectedTool.tags, toolDetail?.tools]);
const subTools = useMemo(() => {
if (!isToolSet || !toolDetail?.tools) return [];
return toolDetail?.tools.filter((subTool: toolDetailType) => !!subTool.parentId);
}, [isToolSet, toolDetail?.tools]);
useEffect(() => {
const fetchReadme = async () => {
if (!toolDetail) return;
const readmeUrl = parentTool?.readme;
if (!readmeUrl) return;
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);
} catch (error) {
console.error('Failed to fetch README:', error);
setReadmeContent('');
}
};
fetchReadme();
}, [parentTool?.readme]);
return (
<Drawer isOpen={true} onClose={onClose} placement="right">
<DrawerOverlay />
<DrawerContent maxW="480px">
<DrawerHeader pt={6} pb={1}>
<Flex gap={1.5}>
<Avatar src={parentTool?.icon || ''} borderRadius={'md'} w={6} />
<Box fontSize={'16px'} fontWeight={500} color={'myGray.900'}>
{parseI18nString(parentTool?.name || '', i18n.language)}
</Box>
<Box flex={1} />
<MyIconButton icon={'common/closeLight'} onClick={onClose} />
</Flex>
</DrawerHeader>
<DrawerBody
position="relative"
sx={{
overflowY: 'overlay' as any,
'&::-webkit-scrollbar': {
width: '6px',
position: 'absolute'
},
'&::-webkit-scrollbar-track': {
background: 'transparent'
},
'&::-webkit-scrollbar-thumb': {
background: 'myGray.300',
borderRadius: '3px'
},
'&::-webkit-scrollbar-thumb:hover': {
background: 'myGray.400'
},
scrollbarWidth: 'thin',
scrollbarColor: 'var(--chakra-colors-myGray-300) transparent'
}}
>
<MyBox>
<Flex gap={2} flexWrap="wrap">
{parentTool?.tags?.map((tag: string) => (
<Box
key={tag}
px={2}
py={1}
border={'1px solid'}
borderRadius={'6px'}
borderColor={'myGray.200'}
fontSize={'10px'}
fontWeight={'medium'}
color={'myGray.700'}
>
{tag}
</Box>
))}
</Flex>
<Box fontSize={'12px'} color="myGray.500" mt={3}>
{parseI18nString(parentTool?.description || '', i18n.language)}
</Box>
<Box fontSize={'12px'} color="myGray.500" mt={3}>
{`by ${parentTool?.author || systemTitle || 'FastGPT'}`}
</Box>
<Flex mt={3}>
<Button
w="full"
variant={isInstalled ? 'primaryOutline' : 'primary'}
isLoading={isLoading || loading}
onClick={async () => {
onToggleInstall(!isInstalled);
if (mode === 'marketplace') return;
setIsInstalled(!isInstalled);
}}
>
{isDownload
? t('common:Download')
: isInstalled
? t('app:toolkit_uninstall')
: t('app:toolkit_install')}
</Button>
</Flex>
{showPoint && (
<Flex mt={4} gap={1.5} alignItems={'center'}>
<Box fontWeight={'medium'} fontSize={'14px'} color={'myGray.900'}>
{t('app:toolkit_call_points_label')}
</Box>
<Box fontSize={'12px'} color={'myGray.600'}>
{!!parentTool?.currentCost
? parentTool?.currentCost
: t('app:toolkit_no_call_points')}
</Box>
</Flex>
)}
<Flex mt={4} gap={1.5} alignItems={'center'}>
<Box fontWeight={'medium'} fontSize={'14px'} color={'myGray.900'}>
{t('app:toolkit_activation_label')}
</Box>
<Box fontSize={'12px'} color={'myGray.600'}>
{parentTool?.hasSystemSecret ||
(parentTool?.secretInputConfig && parentTool?.secretInputConfig.length > 0) ||
(parentTool?.inputList && parentTool?.inputList.length > 0)
? t('app:toolkit_activation_required')
: t('app:toolkit_activation_not_required')}
</Box>
</Flex>
<Box mt={4}>
<LightRowTabs
list={[
{
label: isToolSet
? t('app:toolkit_tool_list')
: t('app:toolkit_params_description'),
value: 'params'
},
...(parentTool?.courseUrl || parentTool?.readme || parentTool?.userGuide
? [{ label: t('app:toolkit_user_guide'), value: 'guide' }]
: [])
]}
value={activeTab}
onChange={(value) => {
if (value === 'guide' && parentTool?.courseUrl) {
window.open(parentTool?.courseUrl, '_blank');
} else {
setActiveTab(value as 'guide' | 'params');
}
}}
gap={4}
/>
<Box h={'1px'} w={'full'} bg={'myGray.200'} mt={'-5px'} mx={1} />
</Box>
<Box mt={4}>
{activeTab === 'guide' && (
<VStack align="stretch" spacing={4}>
{(readmeContent || parentTool?.userGuide) && (
<Box
px={4}
py={3}
border="1px solid"
borderColor="myGray.200"
borderRadius="md"
bg="myGray.50"
fontSize="sm"
color="myGray.900"
maxH="400px"
overflowY="auto"
>
<Markdown source={readmeContent || parentTool?.userGuide || ''} />
</Box>
)}
</VStack>
)}
{activeTab === 'params' && (
<VStack align="stretch" spacing={4}>
{isToolSet && subTools.length > 0 && (
<Accordion
allowMultiple
{...(subTools.length === 1 ? { defaultIndex: [0] } : {})}
>
{subTools.map((subTool: ToolDetailType) => (
<SubToolAccordionItem key={subTool.toolId} tool={subTool} />
))}
</Accordion>
)}
{!isToolSet && (
<>
{parentTool?.versionList?.[0]?.inputs &&
parentTool?.versionList?.[0]?.inputs.length > 0 && (
<ParamSection
title={t('app:toolkit_inputs')}
params={parentTool?.versionList?.[0]?.inputs}
/>
)}
{parentTool?.versionList?.[0]?.outputs &&
parentTool?.versionList?.[0]?.outputs.length > 0 && (
<ParamSection
title={t('app:toolkit_outputs')}
params={parentTool?.versionList?.[0]?.outputs}
/>
)}
</>
)}
</VStack>
)}
</Box>
</MyBox>
</DrawerBody>
</DrawerContent>
</Drawer>
);
};
export default React.memo(ToolDetailDrawer);