Commit b4a5a76d by siigure Committed by GitHub

feat: tooltip (#7087)

* feat: tooltip

* fix: 优化 tooltip 展示逻辑并修复 header UI 布局

* fix: 优化名称 tooltip 溢出展示

* submodule

---------

Co-authored-by: archer <545436317@qq.com>
parent d38bf227
......@@ -10,3 +10,4 @@ description: 'FastGPT V4.15.0-beta5 Release Notes'
## ⚙️ Improvements
1. HTML output now automatically switches to preview mode after generation, reducing the need to open the preview manually.
2. Improved long-name display for apps, datasets, files, and folders: names are truncated when they exceed the available width, and the full name is shown when hovering over the name.
......@@ -10,3 +10,4 @@ description: 'FastGPT V4.15.0-beta5 更新说明'
## ⚙️ 优化
1. HTML 输出后自动切换为预览,减少手动打开预览的操作。
2. 优化应用、知识库、文件和文件夹等长名称展示:超出宽度时自动省略,并在 hover 名称时展示完整内容。
......@@ -130,39 +130,11 @@
"content/guide/workspace/team/team_roles_permissions.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/workspace/team/team_roles_permissions.mdx": "2026-05-07T15:06:40+08:00",
"content/openapi/app.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/app.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/dataset.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/dataset.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/index.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/index.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/intro.en.mdx": "2026-05-29T21:03:14+08:00",
"content/openapi/intro.mdx": "2026-05-29T21:03:14+08:00",
"content/openapi/share.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.mdx": "2026-04-26T21:08:47+08:00",
"content/plugin/index.en.mdx": "2026-06-04T16:10:15+08:00",
"content/plugin/index.mdx": "2026-06-04T16:10:15+08:00",
"content/plugin/intro.en.mdx": "2026-06-09T16:03:58+08:00",
"content/plugin/intro.mdx": "2026-06-09T16:03:58+08:00",
"content/plugin/model-presets.en.mdx": "2026-06-04T16:10:15+08:00",
"content/plugin/model-presets.mdx": "2026-06-04T16:10:15+08:00",
"content/plugin/system-tool-development.en.mdx": "2026-06-09T16:03:58+08:00",
"content/plugin/system-tool-development.mdx": "2026-06-09T16:03:58+08:00",
"content/self-host/config/env.en.mdx": "2026-06-10T19:02:59+08:00",
"content/self-host/config/env.mdx": "2026-06-10T19:02:59+08:00",
"content/self-host/config/json.en.mdx": "2026-05-25T11:21:30+08:00",
"content/self-host/config/json.mdx": "2026-05-25T11:21:30+08:00",
"content/self-host/config/model/intro.en.mdx": "2026-06-04T16:10:15+08:00",
"content/self-host/config/model/intro.mdx": "2026-06-04T16:10:15+08:00",
"content/self-host/config/model/minimax.en.mdx": "2026-06-03T10:40:17+08:00",
"content/self-host/config/model/minimax.mdx": "2026-06-03T10:40:17+08:00",
"content/self-host/config/model/siliconCloud.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/model/siliconCloud.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/object-storage.en.mdx": "2026-05-21T11:24:48+08:00",
"content/self-host/config/object-storage.mdx": "2026-05-21T11:24:48+08:00",
"content/self-host/config/signoz.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/signoz.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/custom-models/bge-rerank.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/custom-models/bge-rerank.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/custom-models/chatglm2-m3e.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -193,8 +165,8 @@
"content/self-host/migration/docker_db.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/migration/docker_mongo.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/migration/docker_mongo.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/troubleshooting/attention.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/troubleshooting/attention.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/troubleshooting/attention.en.mdx": "2026-06-12T12:19:25+08:00",
"content/self-host/troubleshooting/attention.mdx": "2026-06-12T12:19:25+08:00",
"content/self-host/troubleshooting/faq.en.mdx": "2026-05-12T18:16:10+08:00",
"content/self-host/troubleshooting/faq.mdx": "2026-05-12T18:16:10+08:00",
"content/self-host/troubleshooting/methods.en.mdx": "2026-04-26T21:08:47+08:00",
......
import React from 'react';
import { Tooltip, type TooltipProps } from '@chakra-ui/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Tooltip, type TooltipProps, useMergeRefs } from '@chakra-ui/react';
interface Props extends TooltipProps {}
const defaultTooltipProps = {
className: 'chakra-tooltip',
bg: 'white',
arrowShadowColor: 'rgba(0,0,0,0.05)',
hasArrow: true,
arrowSize: 12,
offset: [-15, 15] as [number, number],
color: 'myGray.800',
px: 4,
py: 2,
borderRadius: '8px',
whiteSpace: 'pre-wrap',
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)'
} satisfies Omit<TooltipProps, 'children'>;
interface Props extends TooltipProps {
/**
* 仅当触发元素横向内容溢出时展示 Tooltip,用于替代外部按字数估算的判断。
*/
showOnlyWhenOverflow?: boolean;
}
/**
* 为“仅溢出时展示”模式注入测量 ref,并保留原子元素上的 ref 与 hover/focus 事件。
* 普通 Tooltip 不走这段逻辑,避免全局 tooltip 都承担 DOM 测量成本。
*/
const useOverflowTooltipTrigger = ({
children,
enabled
}: {
children: React.ReactNode;
enabled: boolean;
}) => {
const triggerRef = useRef<HTMLElement | null>(null);
const [isOverflow, setIsOverflow] = useState(false);
const checkOverflow = useCallback(() => {
if (!enabled) {
setIsOverflow(false);
return;
}
const target = triggerRef.current;
if (!target) {
setIsOverflow(false);
return;
}
setIsOverflow(
target.scrollWidth > target.clientWidth || target.scrollHeight > target.clientHeight
);
}, [enabled]);
useEffect(() => {
if (!enabled) return;
const frame = window.requestAnimationFrame(checkOverflow);
const target = triggerRef.current;
if (!target) {
return () => window.cancelAnimationFrame(frame);
}
const observer =
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(checkOverflow);
observer?.observe(target);
window.addEventListener('resize', checkOverflow);
return () => {
window.cancelAnimationFrame(frame);
observer?.disconnect();
window.removeEventListener('resize', checkOverflow);
};
}, [checkOverflow, children, enabled]);
const setTriggerRef = useCallback(
(node: HTMLElement | null) => {
triggerRef.current = node;
checkOverflow();
},
[checkOverflow]
);
const triggerElement = React.isValidElement(children) ? children : null;
const childOriginRef = triggerElement
? ((triggerElement as any).ref as React.Ref<HTMLElement> | undefined)
: undefined;
const triggerElementProps = triggerElement?.props as
| {
onMouseEnter?: React.MouseEventHandler;
onFocus?: React.FocusEventHandler;
}
| undefined;
const mergedRef = useMergeRefs(childOriginRef, setTriggerRef);
if (!enabled) {
return {
isOverflow: false,
triggerChildren: children
};
}
return {
isOverflow,
triggerChildren: triggerElement ? (
React.cloneElement(triggerElement as React.ReactElement<any>, {
ref: mergedRef,
onMouseEnter: (event: React.MouseEvent) => {
checkOverflow();
triggerElementProps?.onMouseEnter?.(event);
},
onFocus: (event: React.FocusEvent) => {
checkOverflow();
triggerElementProps?.onFocus?.(event);
}
})
) : (
<span
ref={setTriggerRef}
style={{ display: 'inline-block', maxWidth: '100%' }}
onMouseEnter={checkOverflow}
onFocus={checkOverflow}
>
{children}
</span>
)
};
};
const MyTooltip = ({
children,
shouldWrapChildren = true,
showOnlyWhenOverflow = false,
isDisabled,
...props
}: Props) => {
const { isOverflow, triggerChildren } = useOverflowTooltipTrigger({
children,
enabled: showOnlyWhenOverflow
});
const finalChildren = showOnlyWhenOverflow ? triggerChildren : children;
const finalShouldWrapChildren = showOnlyWhenOverflow ? false : shouldWrapChildren;
const finalIsDisabled = isDisabled || (showOnlyWhenOverflow && !isOverflow);
const MyTooltip = ({ children, shouldWrapChildren = true, ...props }: Props) => {
return (
<Tooltip
className="chakra-tooltip"
bg={'white'}
arrowShadowColor={'rgba(0,0,0,0.05)'}
hasArrow
arrowSize={12}
offset={[-15, 15]}
color={'myGray.800'}
px={4}
py={2}
borderRadius={'8px'}
whiteSpace={'pre-wrap'}
boxShadow={'1px 1px 10px rgba(0,0,0,0.2)'}
shouldWrapChildren={shouldWrapChildren}
{...defaultTooltipProps}
shouldWrapChildren={finalShouldWrapChildren}
isDisabled={finalIsDisabled}
{...props}
>
{children}
{finalChildren}
</Tooltip>
);
};
......
Subproject commit 0f49c306a09ba34ea427643dd81d7939cd80985f
Subproject commit cd209c713a0263acb08492738c2c4265d8dc37b9
......@@ -110,9 +110,6 @@ const FolderPath = (props: {
}
};
const shouldTruncate = !isLast && item.parentName.length > 10;
const displayName = shouldTruncate ? `${item.parentName.slice(0, 10)}...` : item.parentName;
const pathBox = (
<Box
fontSize={['xs', fontSize || 'sm']}
......@@ -133,13 +130,15 @@ const FolderPath = (props: {
})}
{...(isLast && !forbidLastClick && clickStyles)}
>
{displayName}
{item.parentName}
</Box>
);
return (
<Flex key={item.parentId || index} alignItems={'center'}>
{shouldTruncate ? <MyTooltip label={item.parentName}>{pathBox}</MyTooltip> : pathBox}
<MyTooltip label={item.parentName} showOnlyWhenOverflow>
{pathBox}
</MyTooltip>
{!isLast && <MyIcon name={'common/line'} color={'myGray.500'} mx={1} width={'5px'} />}
</Flex>
);
......
......@@ -29,6 +29,7 @@ import FolderPath from '@/components/common/folder/Path';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import QuickCreateDatasetModal from '@/pageComponents/app/detail/components/QuickCreateDatasetModal';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
// Dataset selection modal component
export const DatasetSelectModal = ({
......@@ -306,14 +307,29 @@ export const DatasetSelectModal = ({
</Box>
{/* Avatar */}
<Avatar src={item.avatar} w={7} h={7} borderRadius="sm" ml={3} mr={2.5} />
<Avatar
src={item.avatar}
w={7}
h={7}
borderRadius="sm"
ml={3}
mr={2.5}
flexShrink={0}
/>
{/* Name and type */}
<Box flex={1} minW={0}>
<Box fontSize="sm" color={'myGray.900'} lineHeight={1}>
{item.name}
</Box>
<Box fontSize="xs" color="myGray.500">
<MyTooltip label={item.name} showOnlyWhenOverflow>
<Box
fontSize="sm"
color={'myGray.900'}
lineHeight={1.2}
className="textEllipsis"
>
{item.name}
</Box>
</MyTooltip>
<Box fontSize="xs" color="myGray.500" className="textEllipsis">
{item.type === DatasetTypeEnum.folder ? (
<>{t('common:Folder')}</>
) : (
......@@ -408,9 +424,20 @@ export const DatasetSelectModal = ({
cursor="pointer"
alignItems="center"
>
<Avatar src={item.avatar} w={6} h={6} borderRadius="sm" mr={3} />
<Box flex={1} minW={0}>
<Box fontSize="sm">{item.name}</Box>
<Avatar
src={item.avatar}
w={6}
h={6}
borderRadius="sm"
mr={3}
flexShrink={0}
/>
<Box flex={1} minW={0} mr={2}>
<MyTooltip label={item.name} showOnlyWhenOverflow>
<Box fontSize="sm" className="textEllipsis" lineHeight={1.2}>
{item.name}
</Box>
</MyTooltip>
</Box>
<IconButton
aria-label="Remove"
......@@ -418,6 +445,7 @@ export const DatasetSelectModal = ({
size="xs"
variant="ghost"
color="black"
flexShrink={0}
_hover={{ bg: 'myGray.200' }}
onClick={() =>
setSelectedDatasets((prev) =>
......
......@@ -51,36 +51,35 @@ const RawSourceBox = ({
teamId,
teamToken
});
const displaySourceName = sourceName || t('common:unknow_source');
return (
<MyTooltip
label={canPreview ? t('file:click_to_view_raw_source') : ''}
shouldWrapChildren={false}
<Box
color={'myGray.900'}
fontWeight={'medium'}
display={'inline-flex'}
whiteSpace={'nowrap'}
{...(canPreview
? {
cursor: 'pointer',
textDecoration: 'underline',
onClick: read
}
: {})}
{...props}
>
<Box
color={'myGray.900'}
fontWeight={'medium'}
display={'inline-flex'}
whiteSpace={'nowrap'}
{...(canPreview
? {
cursor: 'pointer',
textDecoration: 'underline',
onClick: read
}
: {})}
{...props}
>
<MyIcon name={icon as any} w={['1rem', '1.25rem']} mr={2} />
<MyIcon name={icon as any} w={['1rem', '1.25rem']} mr={2} flexShrink={0} />
<MyTooltip label={displaySourceName} showOnlyWhenOverflow>
<Box
maxW={['200px', '300px']}
className={props.className ?? 'textEllipsis'}
wordBreak={'break-all'}
minW={0}
>
{sourceName || t('common:unknow_source')}
{displaySourceName}
</Box>
</Box>
</MyTooltip>
</MyTooltip>
</Box>
);
};
......
......@@ -27,6 +27,7 @@ import type { SimpleAppSnapshotType } from './useSnapshots';
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
import type { Form2WorkflowFnType } from './type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const AppCard = ({
appForm,
......@@ -91,16 +92,18 @@ const AppCard = ({
<Flex alignItems={'center'} justifyContent={'space-between'} mb={5}>
<Flex alignItems={'center'} flex={1} minW={0}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} h={'28px'} />
<Box
ml={3}
fontWeight={'bold'}
fontSize={'lg'}
color={'myGray.900'}
flex={1}
noOfLines={1}
>
{appDetail.name}
</Box>
<MyTooltip label={appDetail.name} showOnlyWhenOverflow>
<Box
ml={3}
fontWeight={'bold'}
fontSize={'lg'}
color={'myGray.900'}
flex={1}
noOfLines={1}
>
{appDetail.name}
</Box>
</MyTooltip>
</Flex>
{/* Right Action Icons */}
......
......@@ -132,7 +132,7 @@ const ExportConfigPopover = ({
<MyPopover
placement={'right-start'}
offset={[0, 0]}
hasArrow={false}
hasArrow={true}
trigger={'hover'}
flip={false}
zIndex={2000}
......
......@@ -12,6 +12,7 @@ import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { WorkflowUtilsContext } from './context/workflowUtilsContext';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
const ExportConfigPopover = dynamic(
......@@ -165,11 +166,15 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
const Render = useMemo(() => {
return (
<HStack flex={1} justifyContent={'space-between'}>
<HStack>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
<Box>
<HStack spacing={1}>
<Box color={'myGray.900'}>{appDetail.name}</Box>
<HStack minW={0}>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} flexShrink={0} />
<Box minW={0}>
<HStack spacing={1} minW={0}>
<MyTooltip label={appDetail.name} showOnlyWhenOverflow>
<Box color={'myGray.900'} maxW={['45vw', '280px']} className="textEllipsis">
{appDetail.name}
</Box>
</MyTooltip>
</HStack>
{showSaveStatus && (
<Flex alignItems={'center'} fontSize={'mini'} lineHeight={1}>
......@@ -199,6 +204,7 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
icon={<MyIcon name={'common/select'} w={'18px'} color={'myGray.500'} />}
w={'34px'}
h={'34px'}
flexShrink={0}
bg={'white'}
border={'1px solid'}
borderColor={'myGray.250'}
......
......@@ -48,16 +48,18 @@ const QuoteItem = ({
>
<Flex gap={2} alignItems={'center'} pb={'8px'}>
<Box alignItems={'center'} fontSize={'10px'} fontWeight={500} display={'inline-flex'}>
<Flex gap={1}>
<Flex gap={1} minW={0}>
<MyIcon name={icon as any} flexShrink={0} w={'12px'} />
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
flex={'1 0 0'}
color={'myGray.900'}
>
{sourceName}
</Box>
<MyTooltip label={sourceName} showOnlyWhenOverflow>
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
flex={'1 0 0'}
color={'myGray.900'}
>
{sourceName}
</Box>
</MyTooltip>
</Flex>
</Box>
</Flex>
......
......@@ -15,6 +15,7 @@ import {
import { getQuoteDataList } from '@/web/core/chat/record/api';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
type MobileQuoteTab = 'detail' | 'source';
......@@ -177,9 +178,11 @@ const QuoteReader = ({
onClick={() => openCollectionQuote(item)}
>
<MyIcon name={item.icon as any} flexShrink={0} w={'16px'} />
<Box className={'textEllipsis'} minW={0}>
{item.sourceName}
</Box>
<MyTooltip label={item.sourceName} showOnlyWhenOverflow>
<Box className={'textEllipsis'} minW={0}>
{item.sourceName}
</Box>
</MyTooltip>
</Flex>
))}
</Flex>
......
......@@ -165,263 +165,253 @@ const List = () => {
const isTool = ToolTypeList.includes(app.type);
const isFolder = AppFolderTypeList.includes(app.type);
return (
<MyTooltip
<MyBox
key={app._id}
label={
app.type === AppTypeEnum.folder
? t('common:open_folder')
: app.permission.hasWritePer || app.permission.hasReadChatLogPer
? t('app:edit_app')
: t('app:go_to_chat')
}
data-virtual-item=""
py={4}
px={5}
cursor={'pointer'}
border={'base'}
bg={'white'}
borderRadius={'10px'}
position={'relative'}
display={'flex'}
flexDirection={'column'}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5',
'& .more': {
display: 'flex'
},
'& .time': {
display: ['flex', 'none']
}
}}
onClick={() => {
if (AppFolderTypeList.includes(app.type)) {
setSearchKey('');
router.push({
query: {
...router.query,
parentId: app._id
}
});
} else if (app.permission.hasWritePer || app.permission.hasReadChatLogPer) {
router.push(`/app/detail?appId=${app._id}`);
} else {
window.open(
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
);
}
}}
{...getBoxProps({
dataId: app._id,
isFolder: app.type === AppTypeEnum.folder || app.type === AppTypeEnum.toolFolder
})}
>
<MyBox
data-virtual-item=""
py={4}
px={5}
cursor={'pointer'}
border={'base'}
bg={'white'}
borderRadius={'10px'}
position={'relative'}
display={'flex'}
flexDirection={'column'}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5',
'& .more': {
display: 'flex'
},
'& .time': {
display: ['flex', 'none']
}
}}
onClick={() => {
if (AppFolderTypeList.includes(app.type)) {
setSearchKey('');
router.push({
query: {
...router.query,
parentId: app._id
}
});
} else if (app.permission.hasWritePer || app.permission.hasReadChatLogPer) {
router.push(`/app/detail?appId=${app._id}`);
} else {
window.open(
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
);
}
}}
{...getBoxProps({
dataId: app._id,
isFolder: app.type === AppTypeEnum.folder || app.type === AppTypeEnum.toolFolder
})}
>
<Grid templateColumns="auto 1fr auto" alignItems="center" width="100%" gap={2}>
<Avatar src={app.avatar} borderRadius={'sm'} w={'1.5rem'} />
<Box color={'myGray.900'} fontWeight={'medium'} minWidth={0} overflow="hidden">
<Grid templateColumns="auto 1fr auto" alignItems="center" width="100%" gap={2}>
<Avatar src={app.avatar} borderRadius={'sm'} w={'1.5rem'} />
<Box color={'myGray.900'} fontWeight={'medium'} minWidth={0} overflow="hidden">
<MyTooltip label={app.name} showOnlyWhenOverflow>
<Box className={'textEllipsis'}>{app.name}</Box>
</Box>
<Box justifySelf="end" mr={-5}>
<AppTypeTag type={app.type} />
</Box>
</Grid>
<Box
flex={'1 0 56px'}
mt={3}
textAlign={'justify'}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.500'}
>
<Box className={'textEllipsis2'} whiteSpace={'pre-wrap'} lineHeight={1.3}>
{app.intro || t('common:no_intro')}
</Box>
</MyTooltip>
</Box>
<HStack h={'24px'} fontSize={'mini'} color={'myGray.500'} w="full">
<HStack flex={'1 0 0'}>
<UserBox
sourceMember={app.sourceMember}
fontSize="xs"
avatarSize="1rem"
spacing={0.5}
/>
<PermissionIconText
private={app.private}
color={'myGray.500'}
iconColor={'myGray.400'}
w={'0.875rem'}
/>
</HStack>
<HStack>
{isPc && (
<HStack spacing={0.5} className="time">
<MyIcon name={'history'} w={'0.85rem'} color={'myGray.400'} />
<Box color={'myGray.500'}>
{t(formatTimeToChatTime(app.updateTime) as any).replace('#', ':')}
</Box>
</HStack>
)}
{(AppFolderTypeList.includes(app.type)
? app.permission.hasManagePer
: app.permission.hasWritePer || app.permission.hasReadChatLogPer) && (
<Box className="more" display={['', 'none']}>
<MyMenu
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
aria-label={''}
/>
}
menuList={[
...([
AppTypeEnum.simple,
AppTypeEnum.workflow,
AppTypeEnum.chatAgent
].includes(app.type)
? [
{
children: [
{
icon: 'core/chat/chatLight',
type: 'grayBg' as MenuItemType,
label: t('app:go_to_chat'),
onClick: () => {
window.open(
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
);
}
<Box justifySelf="end" mr={-5}>
<AppTypeTag type={app.type} />
</Box>
</Grid>
<Box
flex={'1 0 56px'}
mt={3}
textAlign={'justify'}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.500'}
>
<Box className={'textEllipsis2'} whiteSpace={'pre-wrap'} lineHeight={1.3}>
{app.intro || t('common:no_intro')}
</Box>
</Box>
<HStack h={'24px'} fontSize={'mini'} color={'myGray.500'} w="full">
<HStack flex={'1 0 0'}>
<UserBox
sourceMember={app.sourceMember}
fontSize="xs"
avatarSize="1rem"
spacing={0.5}
/>
<PermissionIconText
private={app.private}
color={'myGray.500'}
iconColor={'myGray.400'}
w={'0.875rem'}
/>
</HStack>
<HStack>
{isPc && (
<HStack spacing={0.5} className="time">
<MyIcon name={'history'} w={'0.85rem'} color={'myGray.400'} />
<Box color={'myGray.500'}>
{t(formatTimeToChatTime(app.updateTime) as any).replace('#', ':')}
</Box>
</HStack>
)}
{(AppFolderTypeList.includes(app.type)
? app.permission.hasManagePer
: app.permission.hasWritePer || app.permission.hasReadChatLogPer) && (
<Box className="more" display={['', 'none']}>
<MyMenu
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
aria-label={''}
/>
}
menuList={[
...([AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.chatAgent].includes(
app.type
)
? [
{
children: [
{
icon: 'core/chat/chatLight',
type: 'grayBg' as MenuItemType,
label: t('app:go_to_chat'),
onClick: () => {
window.open(
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
);
}
]
}
]
: []),
...([AppTypeEnum.workflowTool].includes(app.type)
? [
{
children: [
{
icon: 'core/chat/chatLight',
type: 'grayBg' as MenuItemType,
label: t('app:go_to_run'),
onClick: () => {
window.open(
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
);
}
}
]
}
]
: []),
...([AppTypeEnum.workflowTool].includes(app.type)
? [
{
children: [
{
icon: 'core/chat/chatLight',
type: 'grayBg' as MenuItemType,
label: t('app:go_to_run'),
onClick: () => {
window.open(
`/chat?appId=${app._id}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`,
'_blank'
);
}
]
}
]
: []),
...(app.permission.hasManagePer
? [
{
children: [
{
icon: 'edit',
type: 'grayBg' as MenuItemType,
label: t('common:dataset.Edit Info'),
onClick: () => {
if (app.type === AppTypeEnum.httpPlugin) {
toast({
title: t('app:type.Http plugin_deprecated'),
status: 'warning'
});
}
setEditedApp({
id: app._id,
avatar: app.avatar,
name: app.name,
intro: app.intro
}
]
}
]
: []),
...(app.permission.hasManagePer
? [
{
children: [
{
icon: 'edit',
type: 'grayBg' as MenuItemType,
label: t('common:dataset.Edit Info'),
onClick: () => {
if (app.type === AppTypeEnum.httpPlugin) {
toast({
title: t('app:type.Http plugin_deprecated'),
status: 'warning'
});
}
},
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission).hasManagePer
? []
: [
{
icon: 'common/file/move',
type: 'grayBg' as MenuItemType,
label: t('common:move_to'),
onClick: () => setMoveAppId(app._id)
}
]),
...(app.permission.hasManagePer
? [
{
icon: 'key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppId(app._id)
}
]
: [])
]
}
]
: []),
...(!app.permission?.hasWritePer ||
app.type === AppTypeEnum.mcpToolSet ||
app.type === AppTypeEnum.folder ||
app.type === AppTypeEnum.httpToolSet ||
app.type === AppTypeEnum.httpPlugin
? []
: [
{
children: [
{
icon: 'copy',
type: 'grayBg' as MenuItemType,
label: t('app:copy_one_app'),
onClick: () =>
openConfirmCopy({
onConfirm: () => onclickCopy({ appId: app._id })
})()
}
]
}
]),
...(app.permission.isOwner
? [
{
children: [
{
type: 'danger' as const,
icon: 'delete',
label: t('common:Delete'),
onClick: () =>
openConfirmDelete({
customContent: (() => {
if (isFolder) return t('app:confirm_delete_folder_tip');
if (isAgent) return t('app:confirm_del_app_tip');
if (isTool) return t('app:confirm_del_tool_tip');
return t('app:confirm_del_app_tip');
})(),
onConfirm: () => onclickDelApp(app._id),
confirmButtonVariant: 'dangerFill',
inputConfirmText: app.name
})()
setEditedApp({
id: app._id,
avatar: app.avatar,
name: app.name,
intro: app.intro
});
}
]
}
]
: [])
]}
/>
</Box>
)}
</HStack>
},
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission).hasManagePer
? []
: [
{
icon: 'common/file/move',
type: 'grayBg' as MenuItemType,
label: t('common:move_to'),
onClick: () => setMoveAppId(app._id)
}
]),
...(app.permission.hasManagePer
? [
{
icon: 'key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppId(app._id)
}
]
: [])
]
}
]
: []),
...(!app.permission?.hasWritePer ||
app.type === AppTypeEnum.mcpToolSet ||
app.type === AppTypeEnum.folder ||
app.type === AppTypeEnum.httpToolSet ||
app.type === AppTypeEnum.httpPlugin
? []
: [
{
children: [
{
icon: 'copy',
type: 'grayBg' as MenuItemType,
label: t('app:copy_one_app'),
onClick: () =>
openConfirmCopy({
onConfirm: () => onclickCopy({ appId: app._id })
})()
}
]
}
]),
...(app.permission.isOwner
? [
{
children: [
{
type: 'danger' as const,
icon: 'delete',
label: t('common:Delete'),
onClick: () =>
openConfirmDelete({
customContent: (() => {
if (isFolder) return t('app:confirm_delete_folder_tip');
if (isAgent) return t('app:confirm_del_app_tip');
if (isTool) return t('app:confirm_del_tool_tip');
return t('app:confirm_del_app_tip');
})(),
onConfirm: () => onclickDelApp(app._id),
confirmButtonVariant: 'dangerFill',
inputConfirmText: app.name
})()
}
]
}
]
: [])
]}
/>
</Box>
)}
</HStack>
</MyBox>
</MyTooltip>
</HStack>
</MyBox>
);
};
......
......@@ -198,7 +198,7 @@ const CollectionCard = () => {
parentId: targetId
});
getData(pageNum);
} catch (error) {}
} catch {}
}
});
......@@ -261,18 +261,29 @@ const CollectionCard = () => {
}}
>
<Td minW={'150px'} maxW={['200px', '300px']} draggable py={2}>
<HStack>
<HStack minW={0}>
<HStack onClick={(e) => e.stopPropagation()}>
<Checkbox
isChecked={isSelected(collection)}
onChange={(e) => toggleSelect(collection)}
onChange={() => toggleSelect(collection)}
/>
</HStack>
<Box>
<Flex alignItems={'center'}>
<MyIcon name={collection.icon as any} w={'1.25rem'} mr={2} />
<MyTooltip label={t('common:click_drag_tip')} shouldWrapChildren={false}>
<Box color={'myGray.900'} fontWeight={'500'} className="textEllipsis">
<Box minW={0} flex={1}>
<Flex alignItems={'center'} minW={0}>
<MyIcon
name={collection.icon as any}
w={'1.25rem'}
mr={2}
flexShrink={0}
/>
<MyTooltip label={collection.name} showOnlyWhenOverflow>
<Box
color={'myGray.900'}
fontWeight={'500'}
className="textEllipsis"
minW={0}
flex={'0 1 auto'}
>
{collection.name}
</Box>
</MyTooltip>
......
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex, IconButton, useTheme, Progress } from '@chakra-ui/react';
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import { useContextSelector } from 'use-context-selector';
......@@ -8,6 +8,7 @@ import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContex
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import FolderPath from '@/components/common/folder/Path';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
export enum TabEnum {
dataCard = 'dataCard',
......@@ -23,10 +24,7 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
const router = useRouter();
const query = router.query;
const { isPc } = useSystem();
const { datasetDetail, rebuildingCount, paths } = useContextSelector(
DatasetPageContext,
(v) => v
);
const { datasetDetail, paths } = useContextSelector(DatasetPageContext, (v) => v);
const tabList = [
{
......@@ -92,9 +90,17 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
aria-label={''}
_hover={'none'}
/>
<Box fontWeight={500} color={'myGray.600'} fontSize={'sm'}>
{datasetDetail.name}
</Box>
<MyTooltip label={datasetDetail.name} showOnlyWhenOverflow>
<Box
fontWeight={500}
color={'myGray.600'}
fontSize={'sm'}
maxW={['45vw', '280px']}
className="textEllipsis"
>
{datasetDetail.name}
</Box>
</MyTooltip>
</Flex>
</>
) : (
......
......@@ -149,249 +149,239 @@ function List() {
const vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar;
return (
<MyTooltip
<MyBox
key={dataset._id}
label={
<Flex flexDirection={'column'} alignItems={'center'}>
<Box fontSize={'xs'} color={'myGray.500'}>
{dataset.type === DatasetTypeEnum.folder
? t('common:open_folder')
: t('common:folder.open_dataset')}
</Box>
</Flex>
}
data-virtual-item=""
display={'flex'}
flexDirection={'column'}
lineHeight={1.5}
h="100%"
pt={5}
pb={3}
px={5}
cursor={'pointer'}
borderWidth={1.5}
border={'base'}
boxShadow={'2'}
bg={'white'}
borderRadius={'lg'}
position={'relative'}
minH={'150px'}
{...getBoxProps({
dataId: dataset._id,
isFolder: dataset.type === DatasetTypeEnum.folder
})}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5',
'& .delete': {
display: 'block'
},
'& .more': {
display: 'flex'
},
'& .time': {
display: ['flex', 'none']
}
}}
onClick={() => {
if (dataset.type === DatasetTypeEnum.folder) {
setSearchKey('');
router.push({
pathname: '/dataset/list',
query: {
parentId: dataset._id
}
});
} else {
router.push({
pathname: '/dataset/detail',
query: {
datasetId: dataset._id
}
});
}
}}
>
<MyBox
data-virtual-item=""
display={'flex'}
flexDirection={'column'}
lineHeight={1.5}
h="100%"
pt={5}
pb={3}
px={5}
cursor={'pointer'}
borderWidth={1.5}
border={'base'}
boxShadow={'2'}
bg={'white'}
borderRadius={'lg'}
position={'relative'}
minH={'150px'}
{...getBoxProps({
dataId: dataset._id,
isFolder: dataset.type === DatasetTypeEnum.folder
})}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5',
'& .delete': {
display: 'block'
},
'& .more': {
display: 'flex'
},
'& .time': {
display: ['flex', 'none']
}
}}
onClick={() => {
if (dataset.type === DatasetTypeEnum.folder) {
setSearchKey('');
router.push({
pathname: '/dataset/list',
query: {
parentId: dataset._id
}
});
} else {
router.push({
pathname: '/dataset/detail',
query: {
datasetId: dataset._id
}
});
}
}}
>
<Flex w="100%">
<Avatar src={dataset.avatar} borderRadius={6} w={'28px'} flexShrink={0} />
<Box width="0" flex="1" className="textEllipsis" color={'myGray.900'} ml={2}>
{dataset.name}
</Box>
{dataset.type !== DatasetTypeEnum.folder && (
<Box flexShrink={0} mr={-5}>
<SideTag
type={dataset.type}
py={0.5}
px={2}
borderLeftRadius={'sm'}
borderRightRadius={0}
/>
</Box>
)}
</Flex>
<Box
flex={1}
className={'textEllipsis3'}
whiteSpace={'pre-wrap'}
py={3}
fontSize={'xs'}
color={'myGray.500'}
>
{dataset.intro ||
(dataset.type === DatasetTypeEnum.folder
? t('common:core.dataset.Folder placeholder')
: t('common:core.dataset.Intro Placeholder'))}
<Flex w="100%">
<Avatar src={dataset.avatar} borderRadius={6} w={'28px'} flexShrink={0} />
<Box width="0" flex="1" className="textEllipsis" color={'myGray.900'} ml={2}>
<MyTooltip label={dataset.name} showOnlyWhenOverflow>
<Box className="textEllipsis">{dataset.name}</Box>
</MyTooltip>
</Box>
<Flex
h={'24px'}
alignItems={'center'}
justifyContent={'space-between'}
fontSize={'sm'}
fontWeight={500}
color={'myGray.500'}
>
<HStack spacing={3.5}>
<UserBox
sourceMember={dataset.sourceMember}
fontSize="xs"
avatarSize="1rem"
spacing={0.5}
{dataset.type !== DatasetTypeEnum.folder && (
<Box flexShrink={0} mr={-5}>
<SideTag
type={dataset.type}
py={0.5}
px={2}
borderLeftRadius={'sm'}
borderRightRadius={0}
/>
<PermissionIconText
flexShrink={0}
private={dataset.private}
iconColor="myGray.400"
color={'myGray.500'}
/>
</HStack>
</Box>
)}
</Flex>
<HStack>
{isPc && dataset.type !== DatasetTypeEnum.folder && (
<HStack spacing={1} className="time">
<Avatar src={vectorModelAvatar} w={'0.85rem'} />
<Box color={'myGray.500'} fontSize={'mini'}>
{dataset.vectorModel.name}
</Box>
</HStack>
)}
{(dataset.type === DatasetTypeEnum.folder
? dataset.permission.hasManagePer
: dataset.permission.hasWritePer) && (
<Box
className="more"
display={['', 'none']}
borderRadius={'md'}
_hover={{
'& .icon': {
bg: 'myGray.100'
}
}}
onClick={(e) => {
e.stopPropagation();
}}
>
<MyMenu
Button={
<Box w={'22px'} h={'22px'}>
<MyIcon
className="icon"
name={'more'}
h={'16px'}
w={'16px'}
px={1}
py={1}
borderRadius={'md'}
cursor={'pointer'}
/>
</Box>
}
menuList={[
{
children: [
{
icon: 'edit',
label: t('common:dataset.Edit Info'),
onClick: () =>
setEditedDataset({
id: dataset._id,
name: dataset.name,
intro: dataset.intro,
avatar: dataset.avatar
})
},
...((parentDataset ? parentDataset : dataset)?.permission.hasManagePer
? [
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => {
setMoveDatasetId(dataset._id);
}
<Box
flex={1}
className={'textEllipsis3'}
whiteSpace={'pre-wrap'}
py={3}
fontSize={'xs'}
color={'myGray.500'}
>
{dataset.intro ||
(dataset.type === DatasetTypeEnum.folder
? t('common:core.dataset.Folder placeholder')
: t('common:core.dataset.Intro Placeholder'))}
</Box>
<Flex
h={'24px'}
alignItems={'center'}
justifyContent={'space-between'}
fontSize={'sm'}
fontWeight={500}
color={'myGray.500'}
>
<HStack spacing={3.5}>
<UserBox
sourceMember={dataset.sourceMember}
fontSize="xs"
avatarSize="1rem"
spacing={0.5}
/>
<PermissionIconText
flexShrink={0}
private={dataset.private}
iconColor="myGray.400"
color={'myGray.500'}
/>
</HStack>
<HStack>
{isPc && dataset.type !== DatasetTypeEnum.folder && (
<HStack spacing={1} className="time">
<Avatar src={vectorModelAvatar} w={'0.85rem'} />
<Box color={'myGray.500'} fontSize={'mini'}>
{dataset.vectorModel.name}
</Box>
</HStack>
)}
{(dataset.type === DatasetTypeEnum.folder
? dataset.permission.hasManagePer
: dataset.permission.hasWritePer) && (
<Box
className="more"
display={['', 'none']}
borderRadius={'md'}
_hover={{
'& .icon': {
bg: 'myGray.100'
}
}}
onClick={(e) => {
e.stopPropagation();
}}
>
<MyMenu
Button={
<Box w={'22px'} h={'22px'}>
<MyIcon
className="icon"
name={'more'}
h={'16px'}
w={'16px'}
px={1}
py={1}
borderRadius={'md'}
cursor={'pointer'}
/>
</Box>
}
menuList={[
{
children: [
{
icon: 'edit',
label: t('common:dataset.Edit Info'),
onClick: () =>
setEditedDataset({
id: dataset._id,
name: dataset.name,
intro: dataset.intro,
avatar: dataset.avatar
})
},
...((parentDataset ? parentDataset : dataset)?.permission.hasManagePer
? [
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => {
setMoveDatasetId(dataset._id);
}
]
: []),
...(dataset.permission.hasManagePer
? [
{
icon: 'key',
label: t('common:permission.Permission'),
onClick: () => setEditPerDatasetId(dataset._id)
}
]
: []),
...(dataset.permission.hasManagePer
? [
{
icon: 'key',
label: t('common:permission.Permission'),
onClick: () => setEditPerDatasetId(dataset._id)
}
]
: [])
]
},
...(dataset.type != DatasetTypeEnum.folder
? [
{
children: [
{
icon: 'export',
label: t('common:Export'),
onClick: () => {
exportDataset(dataset);
}
]
: [])
}
]
}
]
},
...(dataset.type != DatasetTypeEnum.folder
? [
{
children: [
{
icon: 'export',
label: t('common:Export'),
onClick: () => {
exportDataset(dataset);
}
}
]
}
]
: []),
...(dataset.permission.hasManagePer
? [
{
children: [
{
icon: 'delete',
label: t('common:Delete'),
type: 'danger' as const,
onClick: () =>
openConfirm({
onConfirm: () =>
onDelDataset(dataset._id).then(() => {
refetchPaths();
loadMyDatasets();
}),
customContent: DeleteTipsMap.current[dataset.type],
inputConfirmText: dataset.name
})()
}
]
}
]
: [])
]}
/>
</Box>
)}
</HStack>
</Flex>
</MyBox>
</MyTooltip>
: []),
...(dataset.permission.hasManagePer
? [
{
children: [
{
icon: 'delete',
label: t('common:Delete'),
type: 'danger' as const,
onClick: () =>
openConfirm({
onConfirm: () =>
onDelDataset(dataset._id).then(() => {
refetchPaths();
loadMyDatasets();
}),
customContent: DeleteTipsMap.current[dataset.type],
inputConfirmText: dataset.name
})()
}
]
}
]
: [])
]}
/>
</Box>
)}
</HStack>
</Flex>
</MyBox>
);
};
......
......@@ -17,6 +17,7 @@ import { useContextSelector } from 'use-context-selector';
import { DatasetPageContext } from '../context/datasetPageContext';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const SelectCollections = ({
datasetId,
......@@ -187,14 +188,16 @@ const SelectCollections = ({
result = [...selectedDatasetCollectionIds, item._id];
}
setSelectedDatasetCollectionIds(result);
onChange && onChange({ parentId, collectionIds: result });
onChange?.({ parentId, collectionIds: result });
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<MyIcon name={item.icon as any} w={'18px'} />
<Box ml={3} fontSize={'sm'} className="textEllipsis">
{item.name}
<Flex alignItems={'center'} h={'38px'} minW={0}>
<MyIcon name={item.icon as any} w={'18px'} flexShrink={0} />
<Box ml={3} fontSize={'sm'} minW={0} flex={1}>
<MyTooltip label={item.name} showOnlyWhenOverflow>
<Box className="textEllipsis">{item.name}</Box>
</MyTooltip>
</Box>
</Flex>
</Card>
......
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