mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-05 01:02:59 +08:00
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:
@@ -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) => ``
|
||||
);
|
||||
content = content.replace(
|
||||
/!\[([^\]]*)\]\((?!http|https|\/\/)([^)]+)\)/g,
|
||||
(match, alt, 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);
|
||||
Reference in New Issue
Block a user