mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
♻️ refactor: migrate to new DropdownMenuV2 and showContextMenu API (#11079)
* ♻️ refactor: migrate to new DropdownMenuV2 and showContextMenu API - Replace Dropdown with DropdownMenuV2 for action menus - Use showContextMenu for context menu handling instead of Dropdown wrapper - Update @lobehub/ui to preview version with new context menu API - Add styles for popup-open state in NavItem component * ♻️ refactor: migrate to new DropdownMenuV2 and showContextMenu API * chore: Update @lobehub/ui dependency to version ^4.6.3. Signed-off-by: Innei <tukon479@gmail.com> * ♻️ refactor: migrate to new DropdownMenuV2 and showContextMenu API - Remove deprecated ContextMenu component - Migrate all context menu usages to DropdownMenuV2 and showContextMenu API - Update multiple Action components across Conversation features - Update ResourceManager toolbar components - Clean up related styles 🤖 Generated with [Claude Code](https://claude.com/claude-code) * feat: Update `@lobehub/ui` dependency, simplify `ActionIconGroup` menu prop, and ensure action group visibility when popups are open. Signed-off-by: Innei <tukon479@gmail.com> * fix: Add null check for context menu items, include debug log, and update `@lobehub/ui` dependency. Signed-off-by: Innei <tukon479@gmail.com> * ♻️ refactor: migrate TopicSelector to new DropdownMenuV2 API Migrate from antd/Dropdown to @lobehub/ui DropdownMenu component with checkbox items pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
e3f0f46436
commit
04cfc0e9e0
59 changed files with 1170 additions and 1437 deletions
|
|
@ -207,7 +207,7 @@
|
|||
"@lobehub/icons": "^4.0.2",
|
||||
"@lobehub/market-sdk": "^0.25.1",
|
||||
"@lobehub/tts": "^4.0.2",
|
||||
"@lobehub/ui": "^4.6.2",
|
||||
"@lobehub/ui": "^4.8.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@neondatabase/serverless": "^1.0.2",
|
||||
"@next/third-parties": "^16.1.1",
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ActionIcon, Dropdown, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { ActionIcon, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { MessageSquareDashed, Star } from 'lucide-react';
|
||||
import { Suspense, memo, useCallback } from 'react';
|
||||
|
|
@ -92,34 +92,28 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
|||
|
||||
return (
|
||||
<Flexbox style={{ position: 'relative' }}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !threadId && !isInAgentSubRoute}
|
||||
disabled={editing}
|
||||
icon={
|
||||
<ActionIcon
|
||||
color={fav ? cssVar.colorWarning : undefined}
|
||||
fill={fav ? cssVar.colorWarning : 'transparent'}
|
||||
icon={Star}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
favoriteTopic(id, !fav);
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
}
|
||||
loading={isLoading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
title={title}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !threadId && !isInAgentSubRoute}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
icon={
|
||||
<ActionIcon
|
||||
color={fav ? cssVar.colorWarning : undefined}
|
||||
fill={fav ? cssVar.colorWarning : 'transparent'}
|
||||
icon={Star}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
favoriteTopic(id, !fav);
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
}
|
||||
loading={isLoading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
title={title}
|
||||
/>
|
||||
<Editing id={id} title={title} toggleEditing={toggleEditing} />
|
||||
{active && (
|
||||
<Suspense
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { ExternalLink, LucideCopy, PencilLine, Trash, Wand2 } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
|
@ -17,7 +17,7 @@ interface TopicItemDropdownMenuProps {
|
|||
export const useTopicItemDropdownMenu = ({
|
||||
id,
|
||||
toggleEditing,
|
||||
}: TopicItemDropdownMenuProps): MenuProps['items'] => {
|
||||
}: TopicItemDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['topic', 'common']);
|
||||
const { modal } = App.useApp();
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export const useTopicItemDropdownMenu = ({
|
|||
s.removeTopic,
|
||||
]);
|
||||
|
||||
return useMemo(() => {
|
||||
return useCallback(() => {
|
||||
if (!id) return [];
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Dropdown, Icon } from '@lobehub/ui';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { TreeDownRightIcon } from '@lobehub/ui/icons';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
|
@ -46,23 +46,15 @@ const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !isInAgentSubRoute}
|
||||
disabled={editing}
|
||||
icon={
|
||||
<Icon color={cssVar.colorTextDescription} icon={TreeDownRightIcon} size={'small'} />
|
||||
}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !isInAgentSubRoute}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
icon={<Icon color={cssVar.colorTextDescription} icon={TreeDownRightIcon} size={'small'} />}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
/>
|
||||
<Editing id={id} title={title} toggleEditing={toggleEditing} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { PencilLine, Trash } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
|
@ -14,13 +14,13 @@ interface ThreadItemDropdownMenuProps {
|
|||
export const useThreadItemDropdownMenu = ({
|
||||
id,
|
||||
toggleEditing,
|
||||
}: ThreadItemDropdownMenuProps): MenuProps['items'] => {
|
||||
}: ThreadItemDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['thread', 'common']);
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const [removeThread] = useChatStore((s) => [s.removeThread]);
|
||||
|
||||
return useMemo(() => {
|
||||
return useCallback(() => {
|
||||
return [
|
||||
{
|
||||
icon: <Icon icon={PencilLine} />,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { App, Dropdown } from 'antd';
|
||||
import { Button, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
|
|
@ -17,10 +17,17 @@ import { useHomeStore } from '@/store/home';
|
|||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
button: css`
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
buttonGroup: css`
|
||||
width: 100%;
|
||||
`,
|
||||
menuButton: css`
|
||||
padding-inline: 8px;
|
||||
border-start-start-radius: 0 !important;
|
||||
border-end-start-radius: 0 !important;
|
||||
`,
|
||||
primaryButton: css`
|
||||
border-start-end-radius: 0 !important;
|
||||
border-end-end-radius: 0 !important;
|
||||
`,
|
||||
}));
|
||||
|
||||
|
|
@ -127,28 +134,41 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'addAgent',
|
||||
label: t('assistants.addAgent'),
|
||||
onClick: handleAddAgent,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown.Button
|
||||
className={styles.button}
|
||||
icon={<Icon icon={ChevronDownIcon} />}
|
||||
loading={isLoading}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'addAgent',
|
||||
label: t('assistants.addAgent'),
|
||||
onClick: handleAddAgent,
|
||||
},
|
||||
],
|
||||
}}
|
||||
onClick={handleAddAgentAndConverse}
|
||||
overlayStyle={{ minWidth: 267 }}
|
||||
size={'large'}
|
||||
style={{ flex: 1, width: 'unset' }}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('assistants.addAgentAndConverse')}
|
||||
</Dropdown.Button>
|
||||
<Flexbox className={styles.buttonGroup} gap={0} horizontal>
|
||||
<Button
|
||||
block
|
||||
className={styles.primaryButton}
|
||||
loading={isLoading}
|
||||
onClick={handleAddAgentAndConverse}
|
||||
size={'large'}
|
||||
style={{ flex: 1, width: 'unset' }}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('assistants.addAgentAndConverse')}
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
items={menuItems}
|
||||
popupProps={{ style: { minWidth: 267 } }}
|
||||
triggerProps={{ disabled: isLoading }}
|
||||
>
|
||||
<Button
|
||||
className={styles.menuButton}
|
||||
disabled={isLoading}
|
||||
icon={<Icon icon={ChevronDownIcon} />}
|
||||
size={'large'}
|
||||
type={'primary'}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
Tag as AntTag,
|
||||
Avatar,
|
||||
Block,
|
||||
DropdownMenu,
|
||||
Flexbox,
|
||||
Icon,
|
||||
Tag,
|
||||
|
|
@ -11,7 +12,7 @@ import {
|
|||
Tooltip,
|
||||
TooltipGroup,
|
||||
} from '@lobehub/ui';
|
||||
import { App, Dropdown } from 'antd';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import {
|
||||
AlertTriangle,
|
||||
|
|
@ -258,14 +259,14 @@ const UserAgentCard = memo<UserAgentCardProps>(
|
|||
width={'100%'}
|
||||
>
|
||||
{isOwner && (
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||||
<DropdownMenu items={menuItems}>
|
||||
<div
|
||||
className={cx('more-button', styles.moreButton)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Icon icon={MoreVerticalIcon} size={16} style={{ cursor: 'pointer' }} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<Flexbox
|
||||
align={'flex-start'}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ActionIcon, Dropdown, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { ActionIcon, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { MessageSquareDashed, Star } from 'lucide-react';
|
||||
import { Suspense, memo, useCallback } from 'react';
|
||||
|
|
@ -92,34 +92,28 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
|||
|
||||
return (
|
||||
<Flexbox style={{ position: 'relative' }}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !threadId && !isInAgentSubRoute}
|
||||
disabled={editing}
|
||||
icon={
|
||||
<ActionIcon
|
||||
color={fav ? cssVar.colorWarning : undefined}
|
||||
fill={fav ? cssVar.colorWarning : 'transparent'}
|
||||
icon={Star}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
favoriteTopic(id, !fav);
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
}
|
||||
loading={isLoading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
title={title}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !threadId && !isInAgentSubRoute}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
icon={
|
||||
<ActionIcon
|
||||
color={fav ? cssVar.colorWarning : undefined}
|
||||
fill={fav ? cssVar.colorWarning : 'transparent'}
|
||||
icon={Star}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
favoriteTopic(id, !fav);
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
}
|
||||
loading={isLoading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
title={title}
|
||||
/>
|
||||
<Editing id={id} title={title} toggleEditing={toggleEditing} />
|
||||
{active && (
|
||||
<Suspense
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { ExternalLink, LucideCopy, PencilLine, Trash, Wand2 } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
|
@ -17,7 +17,7 @@ interface TopicItemDropdownMenuProps {
|
|||
export const useTopicItemDropdownMenu = ({
|
||||
id,
|
||||
toggleEditing,
|
||||
}: TopicItemDropdownMenuProps): MenuProps['items'] => {
|
||||
}: TopicItemDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['topic', 'common']);
|
||||
const { modal } = App.useApp();
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export const useTopicItemDropdownMenu = ({
|
|||
s.removeTopic,
|
||||
]);
|
||||
|
||||
return useMemo(() => {
|
||||
return useCallback(() => {
|
||||
if (!id) return [];
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Dropdown, Icon } from '@lobehub/ui';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { TreeDownRightIcon } from '@lobehub/ui/icons';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
|
@ -46,23 +46,15 @@ const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !isInAgentSubRoute}
|
||||
disabled={editing}
|
||||
icon={
|
||||
<Icon color={cssVar.colorTextDescription} icon={TreeDownRightIcon} size={'small'} />
|
||||
}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !isInAgentSubRoute}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
icon={<Icon color={cssVar.colorTextDescription} icon={TreeDownRightIcon} size={'small'} />}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
/>
|
||||
<Editing id={id} title={title} toggleEditing={toggleEditing} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { PencilLine, Trash } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
|
@ -14,13 +14,13 @@ interface ThreadItemDropdownMenuProps {
|
|||
export const useThreadItemDropdownMenu = ({
|
||||
id,
|
||||
toggleEditing,
|
||||
}: ThreadItemDropdownMenuProps): MenuProps['items'] => {
|
||||
}: ThreadItemDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['thread', 'common']);
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const [removeThread] = useChatStore((s) => [s.removeThread]);
|
||||
|
||||
return useMemo(() => {
|
||||
return useCallback(() => {
|
||||
return [
|
||||
{
|
||||
icon: <Icon icon={PencilLine} />,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { ActionIcon, Tag } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { ItemType } from 'antd/es/menu/interface';
|
||||
import { ActionIcon, DropdownMenu, type DropdownMenuCheckboxItem, Tag } from '@lobehub/ui';
|
||||
import { Clock3Icon, PlusIcon } from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -32,15 +30,23 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
|||
[topics, activeTopicId],
|
||||
);
|
||||
|
||||
const items = useMemo<ItemType[]>(
|
||||
const items = useMemo<DropdownMenuCheckboxItem[]>(
|
||||
() =>
|
||||
(topics || []).map((topic) => ({
|
||||
checked: topic.id === activeTopicId,
|
||||
closeOnClick: true,
|
||||
key: topic.id,
|
||||
label: topic.title,
|
||||
onClick: () => switchTopic(topic.id),
|
||||
onCheckedChange: (checked) => {
|
||||
if (checked) {
|
||||
switchTopic(topic.id);
|
||||
}
|
||||
},
|
||||
type: 'checkbox',
|
||||
})),
|
||||
[topics, t, switchTopic],
|
||||
[topics, switchTopic, activeTopicId],
|
||||
);
|
||||
const isEmpty = !topics || topics.length === 0;
|
||||
|
||||
return (
|
||||
<NavHeader
|
||||
|
|
@ -53,22 +59,14 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
|||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('actions.addNewTopic')}
|
||||
/>
|
||||
<Dropdown
|
||||
disabled={!topics || topics.length === 0}
|
||||
menu={{
|
||||
items,
|
||||
selectedKeys: activeTopicId ? [activeTopicId] : [],
|
||||
}}
|
||||
overlayStyle={{
|
||||
maxHeight: 600,
|
||||
minWidth: 200,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
popupProps={{ style: { maxHeight: 600, minWidth: 200, overflowY: 'auto' } }}
|
||||
triggerProps={{ disabled: isEmpty }}
|
||||
>
|
||||
<ActionIcon disabled={!topics || topics.length === 0} icon={Clock3Icon} />
|
||||
</Dropdown>
|
||||
<ActionIcon disabled={isEmpty} icon={Clock3Icon} />
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
showTogglePanelButton={false}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GROUP_CHAT_URL } from '@lobechat/const';
|
||||
import type { SidebarAgentItem } from '@lobechat/types';
|
||||
import { ActionIcon, Dropdown, Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { Loader2, PinIcon } from 'lucide-react';
|
||||
import { type CSSProperties, type DragEvent, memo, useCallback, useMemo } from 'react';
|
||||
|
|
@ -85,7 +85,7 @@ const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
|
|||
return <GroupAvatar avatars={(avatar as any) || []} size={22} />;
|
||||
}, [isUpdating, avatar]);
|
||||
|
||||
const dropdownMenu: MenuProps['items'] = useDropdownMenu({
|
||||
const dropdownMenu = useDropdownMenu({
|
||||
group: undefined,
|
||||
id,
|
||||
openCreateGroupModal: () => {}, // Groups don't need this
|
||||
|
|
@ -97,29 +97,23 @@ const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<Link aria-label={id} to={groupUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
className={className}
|
||||
disabled={editing || isUpdating}
|
||||
draggable={!editing && !isUpdating}
|
||||
extra={pinIcon}
|
||||
icon={avatarIcon}
|
||||
key={id}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
style={style}
|
||||
title={displayTitle}
|
||||
/>
|
||||
</Link>
|
||||
</Dropdown>
|
||||
<Link aria-label={id} to={groupUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
className={className}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing || isUpdating}
|
||||
draggable={!editing && !isUpdating}
|
||||
extra={pinIcon}
|
||||
icon={avatarIcon}
|
||||
key={id}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
style={style}
|
||||
title={displayTitle}
|
||||
/>
|
||||
</Link>
|
||||
<Editing id={id} title={displayTitle} toggleEditing={toggleEditing} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { SESSION_CHAT_URL } from '@lobechat/const';
|
||||
import type { SidebarAgentItem } from '@lobechat/types';
|
||||
import { ActionIcon, Dropdown, Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { Loader2, PinIcon } from 'lucide-react';
|
||||
import { type CSSProperties, type DragEvent, memo, useCallback, useMemo } from 'react';
|
||||
|
|
@ -96,7 +96,7 @@ const AgentItem = memo<AgentItemProps>(({ item, style, className }) => {
|
|||
return <Avatar avatar={typeof avatar === 'string' ? avatar : undefined} />;
|
||||
}, [isUpdating, avatar]);
|
||||
|
||||
const dropdownMenu: MenuProps['items'] = useDropdownMenu({
|
||||
const dropdownMenu = useDropdownMenu({
|
||||
group: undefined, // TODO: pass group from parent if needed
|
||||
id,
|
||||
openCreateGroupModal: handleOpenCreateGroupModal,
|
||||
|
|
@ -108,30 +108,25 @@ const AgentItem = memo<AgentItemProps>(({ item, style, className }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<Link aria-label={id} to={agentUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
className={className}
|
||||
disabled={editing || isUpdating}
|
||||
draggable={!editing && !isUpdating}
|
||||
extra={pinIcon}
|
||||
icon={avatarIcon}
|
||||
key={id}
|
||||
loading={isLoading}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
style={style}
|
||||
title={displayTitle}
|
||||
/>
|
||||
</Link>
|
||||
</Dropdown>
|
||||
<Link aria-label={id} to={agentUrl}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
className={className}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing || isUpdating}
|
||||
draggable={!editing && !isUpdating}
|
||||
extra={pinIcon}
|
||||
icon={avatarIcon}
|
||||
key={id}
|
||||
loading={isLoading}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
style={style}
|
||||
title={displayTitle}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Editing
|
||||
avatar={typeof avatar === 'string' ? avatar : undefined}
|
||||
id={id}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { type MenuProps } from '@lobehub/ui';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useSessionItemMenuItems } from '../../../../hooks';
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ export const useDropdownMenu = ({
|
|||
pinned,
|
||||
sessionType,
|
||||
toggleEditing,
|
||||
}: ActionProps): MenuProps['items'] => {
|
||||
}: ActionProps): (() => MenuProps['items']) => {
|
||||
const {
|
||||
pinMenuItem,
|
||||
renameMenuItem,
|
||||
|
|
@ -31,7 +31,7 @@ export const useDropdownMenu = ({
|
|||
deleteMenuItem,
|
||||
} = useSessionItemMenuItems();
|
||||
|
||||
return useMemo(
|
||||
return useCallback(
|
||||
() =>
|
||||
[
|
||||
pinMenuItem(id, pinned, parentType),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionsProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionsProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Dropdown } from '@lobehub/ui';
|
||||
import { BoxIcon } from 'lucide-react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
|
|
@ -39,20 +38,14 @@ const ProjectItem = memo<ProjectItemProps>(({ id, name }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
disabled={editing || isUpdating}
|
||||
icon={BoxIcon}
|
||||
loading={isLoading || isUpdating}
|
||||
title={name}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing || isUpdating}
|
||||
icon={BoxIcon}
|
||||
loading={isLoading || isUpdating}
|
||||
title={name}
|
||||
/>
|
||||
<Editing id={id} name={name} toggleEditing={toggleEditing} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { PencilLine, Trash } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
|
@ -14,12 +14,12 @@ interface ProjectItemDropdownMenuProps {
|
|||
export const useProjectItemDropdownMenu = ({
|
||||
id,
|
||||
toggleEditing,
|
||||
}: ProjectItemDropdownMenuProps): MenuProps['items'] => {
|
||||
}: ProjectItemDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['home', 'common']);
|
||||
const [removeKnowledgeBase] = useKnowledgeBaseStore((s) => [s.removeKnowledgeBase]);
|
||||
const { modal } = App.useApp();
|
||||
|
||||
return useMemo<MenuProps['items']>(
|
||||
return useCallback(
|
||||
() => [
|
||||
{
|
||||
icon: <Icon icon={PencilLine} />,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ActionIcon, Flexbox } from '@lobehub/ui';
|
||||
import { ActionIcon, DropdownMenu, Flexbox } from '@lobehub/ui';
|
||||
import { CreateBotIcon } from '@lobehub/ui/icons';
|
||||
import { Dropdown } from 'antd';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
|
|
@ -45,7 +44,7 @@ const AddButton = memo(() => {
|
|||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={tChat('newAgent')}
|
||||
/>
|
||||
<Dropdown menu={{ items: dropdownItems || [] }}>
|
||||
<DropdownMenu items={dropdownItems}>
|
||||
<ActionIcon
|
||||
color={cssVar.colorTextQuaternary}
|
||||
icon={ChevronDownIcon}
|
||||
|
|
@ -54,7 +53,7 @@ const AddButton = memo(() => {
|
|||
width: 16,
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Avatar, type MenuProps } from '@lobehub/ui';
|
||||
import { Dropdown } from '@lobehub/ui';
|
||||
import { Avatar } from '@lobehub/ui';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -56,7 +55,7 @@ const PageListItem = memo<DocumentItemProps>(({ pageId, className }) => {
|
|||
return FileTextIcon;
|
||||
}, [emoji]);
|
||||
|
||||
const dropdownMenu: MenuProps['items'] = useDropdownMenu({
|
||||
const dropdownMenu = useDropdownMenu({
|
||||
documentContent: document?.content || undefined,
|
||||
pageId,
|
||||
toggleEditing,
|
||||
|
|
@ -64,23 +63,17 @@ const PageListItem = memo<DocumentItemProps>(({ pageId, className }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active}
|
||||
className={className}
|
||||
disabled={editing}
|
||||
icon={icon}
|
||||
key={pageId}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active}
|
||||
className={className}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
icon={icon}
|
||||
key={pageId}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
/>
|
||||
<Editing
|
||||
currentEmoji={emoji}
|
||||
documentId={pageId}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { Copy, CopyPlus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useFileStore } from '@/store/file';
|
||||
|
|
@ -16,7 +16,7 @@ export const useDropdownMenu = ({
|
|||
documentContent,
|
||||
pageId,
|
||||
toggleEditing,
|
||||
}: ActionProps): MenuProps['items'] => {
|
||||
}: ActionProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['common', 'file']);
|
||||
const { message, modal } = App.useApp();
|
||||
const removeDocument = useFileStore((s) => s.removeDocument);
|
||||
|
|
@ -59,7 +59,7 @@ export const useDropdownMenu = ({
|
|||
}
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
return useCallback(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { Dropdown } from '@lobehub/ui';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { Loader2Icon } from 'lucide-react';
|
||||
import React, { type CSSProperties, memo, useCallback, useMemo } from 'react';
|
||||
|
|
@ -66,33 +65,27 @@ const KnowledgeBaseItem = memo<KnowledgeBaseItemProps>(({ id, name, active, styl
|
|||
return <RepoIcon size={18} />;
|
||||
}, [isLoading]);
|
||||
|
||||
const dropdownMenu: MenuProps['items'] = useDropdownMenu({
|
||||
const dropdownMenu = useDropdownMenu({
|
||||
id,
|
||||
toggleEditing,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
}}
|
||||
trigger={['contextMenu']}
|
||||
>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active}
|
||||
className={className}
|
||||
disabled={editing}
|
||||
icon={icon}
|
||||
key={id}
|
||||
loading={isLoading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={style}
|
||||
title={name}
|
||||
/>
|
||||
</Dropdown>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active}
|
||||
className={className}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
icon={icon}
|
||||
key={id}
|
||||
loading={isLoading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={style}
|
||||
title={name}
|
||||
/>
|
||||
<Editing id={id} name={name} toggleEditing={toggleEditing} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { PencilLine, Trash } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
|
@ -11,7 +11,7 @@ interface ActionProps {
|
|||
toggleEditing: (visible?: boolean) => void;
|
||||
}
|
||||
|
||||
export const useDropdownMenu = ({ id, toggleEditing }: ActionProps): MenuProps['items'] => {
|
||||
export const useDropdownMenu = ({ id, toggleEditing }: ActionProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['file', 'common']);
|
||||
const { modal } = App.useApp();
|
||||
const removeKnowledgeBase = useKnowledgeBaseStore((s) => s.removeKnowledgeBase);
|
||||
|
|
@ -29,7 +29,7 @@ export const useDropdownMenu = ({ id, toggleEditing }: ActionProps): MenuProps['
|
|||
});
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
return useCallback(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { Center, Flexbox, Skeleton, Text } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Center, type DropdownItem, DropdownMenu, Flexbox, Skeleton, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { ChevronsUpDown } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
|
@ -81,7 +79,7 @@ const Head = memo<{ id: string }>(({ id }) => {
|
|||
[navigate, setMode],
|
||||
);
|
||||
|
||||
const menuItems: MenuProps['items'] = useMemo(() => {
|
||||
const menuItems = useMemo<DropdownItem[]>(() => {
|
||||
if (!libraries) return [];
|
||||
|
||||
return libraries.map((library) => ({
|
||||
|
|
@ -120,14 +118,14 @@ const Head = memo<{ id: string }>(({ id }) => {
|
|||
</Flexbox>
|
||||
)}
|
||||
{name && (
|
||||
<Dropdown menu={{ items: menuItems }} placement="bottomRight" trigger={['click']}>
|
||||
<DropdownMenu items={menuItems} placement="bottomRight">
|
||||
<ChevronsUpDown
|
||||
className={styles.icon}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size={16}
|
||||
style={{ cursor: 'pointer', flex: 'none' }}
|
||||
/>
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { ActionIcon, Flexbox } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import { ActionIcon, DropdownMenu, type DropdownMenuCheckboxItem, Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import type { ItemType } from 'antd/es/menu/interface';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { Clock3Icon, PlusIcon } from 'lucide-react';
|
||||
|
|
@ -53,7 +51,7 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
|||
[topics, activeTopicId],
|
||||
);
|
||||
|
||||
const items = useMemo<ItemType[]>(
|
||||
const items = useMemo<DropdownMenuCheckboxItem[]>(
|
||||
() =>
|
||||
(topics || []).map((topic) => {
|
||||
const displayTime =
|
||||
|
|
@ -62,6 +60,8 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
|||
: dayjs(topic.updatedAt).format('YYYY-MM-DD');
|
||||
|
||||
return {
|
||||
checked: topic.id === activeTopicId,
|
||||
closeOnClick: true,
|
||||
key: topic.id,
|
||||
label: (
|
||||
<Flexbox align="center" gap={4} horizontal justify="space-between" width="100%">
|
||||
|
|
@ -69,11 +69,17 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
|||
<span className={styles.time}>{displayTime}</span>
|
||||
</Flexbox>
|
||||
),
|
||||
onClick: () => switchTopic(topic.id),
|
||||
onCheckedChange: (checked) => {
|
||||
if (checked) {
|
||||
switchTopic(topic.id);
|
||||
}
|
||||
},
|
||||
type: 'checkbox',
|
||||
};
|
||||
}),
|
||||
[topics, switchTopic, styles],
|
||||
[topics, switchTopic, styles, activeTopicId],
|
||||
);
|
||||
const isEmpty = !topics || topics.length === 0;
|
||||
|
||||
return (
|
||||
<NavHeader
|
||||
|
|
@ -88,19 +94,14 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
|||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('actions.addNewTopic')}
|
||||
/>
|
||||
<Dropdown
|
||||
disabled={!topics || topics.length === 0}
|
||||
menu={{
|
||||
items,
|
||||
selectedKeys: activeTopicId ? [activeTopicId] : [],
|
||||
style: { maxHeight: 400, overflowY: 'auto' },
|
||||
}}
|
||||
overlayStyle={{ minWidth: 280 }}
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
popupProps={{ style: { maxHeight: 400, minWidth: 280, overflowY: 'auto' } }}
|
||||
triggerProps={{ disabled: isEmpty }}
|
||||
>
|
||||
<ActionIcon disabled={!topics || topics.length === 0} icon={Clock3Icon} />
|
||||
</Dropdown>
|
||||
<ActionIcon disabled={isEmpty} icon={Clock3Icon} />
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
showTogglePanelButton={false}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ export const styles = createStaticStyles(({ css, cssVar }) => {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
&:has([data-popup-open]) {
|
||||
div[role='menubar'] {
|
||||
pointer-events: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
time,
|
||||
div[role='menubar'] {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ export const ErrorActionsBar = memo<ErrorActionsBarProps>(({ actions, onActionCl
|
|||
return (
|
||||
<ActionIconGroup
|
||||
items={[regenerate, del]}
|
||||
menu={{
|
||||
items: [edit, copy, divider, del],
|
||||
}}
|
||||
menu={[edit, copy, divider, del]}
|
||||
onActionClick={onActionClick}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { type UIChatMessage } from '@lobechat/types';
|
||||
import { ActionIconGroup } from '@lobehub/ui';
|
||||
import { ActionIconGroup, createRawModal } from '@lobehub/ui';
|
||||
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import ShareMessageModal from '../../../components/ShareMessageModal';
|
||||
import { messageStateSelectors, useConversationStore } from '../../../store';
|
||||
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
|
||||
import {
|
||||
Provider,
|
||||
createStore,
|
||||
messageStateSelectors,
|
||||
useConversationStore,
|
||||
useConversationStoreApi,
|
||||
} from '../../../store';
|
||||
import type {
|
||||
MessageActionItem,
|
||||
MessageActionItemOrDivider,
|
||||
|
|
@ -57,7 +63,30 @@ interface AssistantActionsBarProps {
|
|||
export const AssistantActionsBar = memo<AssistantActionsBarProps>(
|
||||
({ actionsConfig, id, data, index }) => {
|
||||
const { error, tools } = data;
|
||||
const [showShareModal, setShareModal] = useState(false);
|
||||
const store = useConversationStoreApi();
|
||||
|
||||
const handleOpenShareModal = useCallback(() => {
|
||||
createRawModal(
|
||||
(props: ShareModalProps) => (
|
||||
<Provider
|
||||
createStore={() => {
|
||||
const state = store.getState();
|
||||
return createStore({
|
||||
context: state.context,
|
||||
hooks: state.hooks,
|
||||
skipFetch: state.skipFetch,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ShareMessageModal {...props} />
|
||||
</Provider>
|
||||
),
|
||||
{
|
||||
message: data,
|
||||
},
|
||||
{ onCloseKey: 'onCancel', openKey: 'open' },
|
||||
);
|
||||
}, [data, store]);
|
||||
|
||||
const isCollapsed = useConversationStore(messageStateSelectors.isMessageCollapsed(id));
|
||||
|
||||
|
|
@ -65,7 +94,7 @@ export const AssistantActionsBar = memo<AssistantActionsBarProps>(
|
|||
data,
|
||||
id,
|
||||
index,
|
||||
onOpenShareModal: () => setShareModal(true),
|
||||
onOpenShareModal: handleOpenShareModal,
|
||||
});
|
||||
|
||||
const hasTools = !!tools;
|
||||
|
|
@ -174,17 +203,9 @@ export const AssistantActionsBar = memo<AssistantActionsBarProps>(
|
|||
[allActions],
|
||||
);
|
||||
|
||||
const shareOnCancel = useCallback(() => {
|
||||
setShareModal(false);
|
||||
}, []);
|
||||
if (error) return <ErrorActionsBar actions={defaultActions} onActionClick={handleAction} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIconGroup items={items} menu={{ items: menu }} onActionClick={handleAction} />
|
||||
<ShareMessageModal message={data} onCancel={shareOnCancel} open={showShareModal} />
|
||||
</>
|
||||
);
|
||||
return <ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />;
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { type AssistantContentBlock, type UIChatMessage } from '@lobechat/types';
|
||||
import { ActionIconGroup } from '@lobehub/ui';
|
||||
import { ActionIconGroup, createRawModal } from '@lobehub/ui';
|
||||
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import ShareMessageModal from '../../../components/ShareMessageModal';
|
||||
import { messageStateSelectors, useConversationStore } from '../../../store';
|
||||
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
|
||||
import {
|
||||
Provider,
|
||||
createStore,
|
||||
messageStateSelectors,
|
||||
useConversationStore,
|
||||
useConversationStoreApi,
|
||||
} from '../../../store';
|
||||
import type {
|
||||
MessageActionItem,
|
||||
MessageActionItemOrDivider,
|
||||
|
|
@ -59,7 +65,29 @@ interface GroupActionsProps {
|
|||
*/
|
||||
const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, contentBlock }) => {
|
||||
const { tools } = data;
|
||||
const [showShareModal, setShareModal] = useState(false);
|
||||
const store = useConversationStoreApi();
|
||||
const handleOpenShareModal = useCallback(() => {
|
||||
createRawModal(
|
||||
(props: ShareModalProps) => (
|
||||
<Provider
|
||||
createStore={() => {
|
||||
const state = store.getState();
|
||||
return createStore({
|
||||
context: state.context,
|
||||
hooks: state.hooks,
|
||||
skipFetch: state.skipFetch,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ShareMessageModal {...props} />
|
||||
</Provider>
|
||||
),
|
||||
{
|
||||
message: data,
|
||||
},
|
||||
{ onCloseKey: 'onCancel', openKey: 'open' },
|
||||
);
|
||||
}, [data, store]);
|
||||
|
||||
const isCollapsed = useConversationStore(messageStateSelectors.isMessageCollapsed(id));
|
||||
|
||||
|
|
@ -67,7 +95,7 @@ const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, conten
|
|||
contentBlock,
|
||||
data,
|
||||
id,
|
||||
onOpenShareModal: () => setShareModal(true),
|
||||
onOpenShareModal: handleOpenShareModal,
|
||||
});
|
||||
|
||||
const hasTools = !!tools;
|
||||
|
|
@ -130,16 +158,7 @@ const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, conten
|
|||
[allActions],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIconGroup items={items} menu={{ items: menu }} onActionClick={handleAction} />
|
||||
<ShareMessageModal
|
||||
message={data}
|
||||
onCancel={() => setShareModal(false)}
|
||||
open={showShareModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />;
|
||||
});
|
||||
|
||||
WithContentId.displayName = 'GroupActionsWithContentId';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { type AssistantContentBlock, type UIChatMessage } from '@lobechat/types';
|
||||
import { ActionIconGroup } from '@lobehub/ui';
|
||||
import { ActionIconGroup, createRawModal } from '@lobehub/ui';
|
||||
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import ShareMessageModal from '../../../components/ShareMessageModal';
|
||||
import { messageStateSelectors, useConversationStore } from '../../../store';
|
||||
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
|
||||
import {
|
||||
Provider,
|
||||
createStore,
|
||||
messageStateSelectors,
|
||||
useConversationStore,
|
||||
useConversationStoreApi,
|
||||
} from '../../../store';
|
||||
import type {
|
||||
MessageActionItem,
|
||||
MessageActionItemOrDivider,
|
||||
|
|
@ -58,7 +64,29 @@ interface GroupActionsProps {
|
|||
* Actions bar for group messages with content (has assistant message content)
|
||||
*/
|
||||
const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, contentBlock }) => {
|
||||
const [showShareModal, setShareModal] = useState(false);
|
||||
const store = useConversationStoreApi();
|
||||
const handleOpenShareModal = useCallback(() => {
|
||||
createRawModal(
|
||||
(props: ShareModalProps) => (
|
||||
<Provider
|
||||
createStore={() => {
|
||||
const state = store.getState();
|
||||
return createStore({
|
||||
context: state.context,
|
||||
hooks: state.hooks,
|
||||
skipFetch: state.skipFetch,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ShareMessageModal {...props} />
|
||||
</Provider>
|
||||
),
|
||||
{
|
||||
message: data,
|
||||
},
|
||||
{ onCloseKey: 'onCancel', openKey: 'open' },
|
||||
);
|
||||
}, [data, store]);
|
||||
|
||||
const isCollapsed = useConversationStore(messageStateSelectors.isMessageCollapsed(id));
|
||||
|
||||
|
|
@ -66,7 +94,7 @@ const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, conten
|
|||
contentBlock,
|
||||
data,
|
||||
id,
|
||||
onOpenShareModal: () => setShareModal(true),
|
||||
onOpenShareModal: handleOpenShareModal,
|
||||
});
|
||||
|
||||
// Get collapse/expand action based on current state
|
||||
|
|
@ -122,16 +150,7 @@ const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, conten
|
|||
[allActions],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIconGroup items={items} menu={{ items: menu }} onActionClick={handleAction} />
|
||||
<ShareMessageModal
|
||||
message={data}
|
||||
onCancel={() => setShareModal(false)}
|
||||
open={showShareModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />;
|
||||
});
|
||||
|
||||
WithContentId.displayName = 'GroupActionsWithContentId';
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ export const ErrorActionsBar = memo<ErrorActionsBarProps>(({ actions, onActionCl
|
|||
return (
|
||||
<ActionIconGroup
|
||||
items={[regenerate, del]}
|
||||
menu={{
|
||||
items: [edit, copy, divider, del],
|
||||
}}
|
||||
menu={[edit, copy, divider, del]}
|
||||
onActionClick={onActionClick}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { type UIChatMessage } from '@lobechat/types';
|
||||
import { ActionIconGroup } from '@lobehub/ui';
|
||||
import { ActionIconGroup, createRawModal } from '@lobehub/ui';
|
||||
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import ShareMessageModal from '../../../components/ShareMessageModal';
|
||||
import { useEventCallback } from '@/hooks/useEventCallback';
|
||||
|
||||
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
|
||||
import { Provider, createStore, useConversationStoreApi } from '../../../store';
|
||||
import type {
|
||||
MessageActionItem,
|
||||
MessageActionItemOrDivider,
|
||||
|
|
@ -56,13 +59,35 @@ interface AssistantActionsBarProps {
|
|||
export const AssistantActionsBar = memo<AssistantActionsBarProps>(
|
||||
({ actionsConfig, id, data, index }) => {
|
||||
const { error, tools } = data;
|
||||
const [showShareModal, setShareModal] = useState(false);
|
||||
const store = useConversationStoreApi();
|
||||
const handleOpenShareModal = useEventCallback(() => {
|
||||
createRawModal(
|
||||
(props: ShareModalProps) => (
|
||||
<Provider
|
||||
createStore={() => {
|
||||
const state = store.getState();
|
||||
return createStore({
|
||||
context: state.context,
|
||||
hooks: state.hooks,
|
||||
skipFetch: state.skipFetch,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ShareMessageModal {...props} />
|
||||
</Provider>
|
||||
),
|
||||
{
|
||||
message: data,
|
||||
},
|
||||
{ onCloseKey: 'onCancel', openKey: 'open' },
|
||||
);
|
||||
});
|
||||
|
||||
const defaultActions = useAssistantActions({
|
||||
data,
|
||||
id,
|
||||
index,
|
||||
onOpenShareModal: () => setShareModal(true),
|
||||
onOpenShareModal: handleOpenShareModal,
|
||||
});
|
||||
|
||||
const hasTools = !!tools;
|
||||
|
|
@ -159,16 +184,7 @@ export const AssistantActionsBar = memo<AssistantActionsBarProps>(
|
|||
|
||||
if (error) return <ErrorActionsBar actions={defaultActions} onActionClick={handleAction} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIconGroup items={items} menu={{ items: menu }} onActionClick={handleAction} />
|
||||
<ShareMessageModal
|
||||
message={data}
|
||||
onCancel={() => setShareModal(false)}
|
||||
open={showShareModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />;
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export const UserActionsBar = memo<UserActionsProps>(({ actionsConfig, id, data
|
|||
[allActions],
|
||||
);
|
||||
|
||||
return <ActionIconGroup items={items} menu={{ items: menu }} onActionClick={handleAction} />;
|
||||
return <ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />;
|
||||
});
|
||||
|
||||
UserActionsBar.displayName = 'UserActionsBar';
|
||||
|
|
|
|||
|
|
@ -4,29 +4,13 @@ import { isDesktop } from '@lobechat/const';
|
|||
import { Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import {
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
Suspense,
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type { VListHandle } from 'virtua';
|
||||
import { type MouseEvent, type ReactNode, Suspense, memo, useCallback } from 'react';
|
||||
|
||||
import BubblesLoading from '@/components/BubblesLoading';
|
||||
|
||||
import ContextMenu from '../components/ContextMenu';
|
||||
import History from '../components/History';
|
||||
import { useChatItemContextMenu } from '../hooks/useChatItemContextMenu';
|
||||
import {
|
||||
dataSelectors,
|
||||
messageStateSelectors,
|
||||
useConversationStore,
|
||||
virtuaListSelectors,
|
||||
} from '../store';
|
||||
import { dataSelectors, messageStateSelectors, useConversationStore } from '../store';
|
||||
import AgentCouncilMessage from './AgentCouncil';
|
||||
import AssistantMessage from './Assistant';
|
||||
import AssistantGroupMessage from './AssistantGroup';
|
||||
|
|
@ -73,13 +57,10 @@ const MessageItem = memo<MessageItemProps>(
|
|||
index,
|
||||
isLatestItem,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const topic = useConversationStore((s) => s.context.topicId);
|
||||
|
||||
// Get message and actionsBar from ConversationStore
|
||||
// Get message from ConversationStore
|
||||
const message = useConversationStore(dataSelectors.getDisplayMessageById(id), isEqual);
|
||||
const actionsBar = useConversationStore((s) => s.actionsBar);
|
||||
const role = message?.role;
|
||||
|
||||
const [editing, isMessageCreating] = useConversationStore((s) => [
|
||||
|
|
@ -87,24 +68,13 @@ const MessageItem = memo<MessageItemProps>(
|
|||
messageStateSelectors.isMessageCreating(id)(s),
|
||||
]);
|
||||
|
||||
const {
|
||||
containerRef: contextMenuContainerRef,
|
||||
contextMenuState,
|
||||
handleContextMenu,
|
||||
hideContextMenu,
|
||||
} = useChatItemContextMenu({
|
||||
const { handleContextMenu } = useChatItemContextMenu({
|
||||
editing,
|
||||
onActionClick: () => {},
|
||||
id,
|
||||
inPortalThread,
|
||||
topic,
|
||||
});
|
||||
|
||||
const setContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
containerRef.current = node;
|
||||
contextMenuContainerRef.current = node;
|
||||
},
|
||||
[contextMenuContainerRef],
|
||||
);
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
async (event: MouseEvent<HTMLDivElement>) => {
|
||||
if (!role || (role !== 'user' && role !== 'assistant' && role !== 'assistantGroup')) return;
|
||||
|
|
@ -129,15 +99,6 @@ const MessageItem = memo<MessageItemProps>(
|
|||
},
|
||||
[handleContextMenu, id, role, message],
|
||||
);
|
||||
// Get virtuaScrollMethods from ConversationStore
|
||||
const virtuaScrollMethods = useConversationStore(virtuaListSelectors.virtuaScrollMethods);
|
||||
|
||||
// Create a ref-like object for ContextMenu compatibility
|
||||
// VirtuaScrollMethods is a subset of VListHandle with the methods ContextMenu needs
|
||||
const virtuaRef = useMemo<RefObject<VListHandle | null>>(
|
||||
() => ({ current: virtuaScrollMethods as VListHandle | null }),
|
||||
[virtuaScrollMethods],
|
||||
);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
switch (role) {
|
||||
|
|
@ -202,7 +163,7 @@ const MessageItem = memo<MessageItemProps>(
|
|||
}
|
||||
|
||||
return null;
|
||||
}, [role, disableEditing, id, index, isLatestItem, actionsBar]);
|
||||
}, [role, disableEditing, id, index, isLatestItem]);
|
||||
|
||||
if (!role) return;
|
||||
|
||||
|
|
@ -213,22 +174,10 @@ const MessageItem = memo<MessageItemProps>(
|
|||
className={cx(styles.message, className, isMessageCreating && styles.loading)}
|
||||
data-index={index}
|
||||
onContextMenu={onContextMenu}
|
||||
ref={setContainerRef}
|
||||
>
|
||||
<Suspense fallback={<BubblesLoading />}>{renderContent()}</Suspense>
|
||||
{endRender}
|
||||
</Flexbox>
|
||||
<ContextMenu
|
||||
id={id}
|
||||
inPortalThread={inPortalThread}
|
||||
index={index}
|
||||
onClose={hideContextMenu}
|
||||
position={contextMenuState.position}
|
||||
selectedText={contextMenuState.selectedText}
|
||||
topic={topic}
|
||||
virtuaRef={virtuaRef}
|
||||
visible={contextMenuState.visible}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,418 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { type UIChatMessage } from '@lobechat/types';
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
||||
import { Dropdown, type MenuProps } from 'antd';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import {
|
||||
type ComponentType,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { VListHandle } from 'virtua';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
|
||||
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
||||
import {
|
||||
dataSelectors,
|
||||
messageStateSelectors,
|
||||
useConversationStore,
|
||||
useConversationStoreApi,
|
||||
} from '../store';
|
||||
import ShareMessageModal from './ShareMessageModal';
|
||||
|
||||
interface ActionMenuItem extends ActionIconGroupItemType {
|
||||
children?: { key: string; label: ReactNode }[];
|
||||
disable?: boolean;
|
||||
popupClassName?: string;
|
||||
}
|
||||
|
||||
type MenuItem = ActionMenuItem | { type: 'divider' };
|
||||
type ContextMenuEvent = ActionIconGroupEvent & { selectedText?: string };
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
contextMenu: css`
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
min-width: 160px;
|
||||
|
||||
.ant-dropdown-menu {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, black 15%, transparent);
|
||||
}
|
||||
`,
|
||||
trigger: css`
|
||||
pointer-events: none;
|
||||
|
||||
position: fixed;
|
||||
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
||||
opacity: 0;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface ContextMenuProps {
|
||||
id: string;
|
||||
inPortalThread: boolean;
|
||||
index: number;
|
||||
onClose: () => void;
|
||||
position: { x: number; y: number };
|
||||
selectedText?: string;
|
||||
topic?: string | null;
|
||||
virtuaRef?: RefObject<VListHandle | null> | null;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const ContextMenu = memo<ContextMenuProps>(
|
||||
({ visible, position, selectedText, id, index, inPortalThread, topic, virtuaRef, onClose }) => {
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation('common');
|
||||
const [shareMessage, setShareMessage] = useState<UIChatMessage | null>(null);
|
||||
const [isShareModalOpen, setShareModalOpen] = useState(false);
|
||||
|
||||
const storeApi = useConversationStoreApi();
|
||||
|
||||
const [role, error, isCollapsed, hasThread, isRegenerating] = useConversationStore((s) => {
|
||||
const item = dataSelectors.getDisplayMessageById(id)(s);
|
||||
return [
|
||||
item?.role,
|
||||
item?.error,
|
||||
messageStateSelectors.isMessageCollapsed(id)(s),
|
||||
messageStateSelectors.hasThreadBySourceMsgId(id)(s),
|
||||
messageStateSelectors.isMessageRegenerating(id)(s),
|
||||
];
|
||||
}, isEqual);
|
||||
|
||||
const isThreadMode = useConversationStore(messageStateSelectors.isThreadMode);
|
||||
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
||||
const actionsBar = useChatListActionsBar({ hasThread, isRegenerating });
|
||||
const inThread = isThreadMode || inPortalThread;
|
||||
|
||||
const [
|
||||
toggleMessageEditing,
|
||||
deleteMessage,
|
||||
regenerateUserMessage,
|
||||
regenerateAssistantMessage,
|
||||
translateMessage,
|
||||
ttsMessage,
|
||||
delAndRegenerateMessage,
|
||||
copyMessage,
|
||||
openThreadCreator,
|
||||
resendThreadMessage,
|
||||
delAndResendThreadMessage,
|
||||
toggleMessageCollapsed,
|
||||
] = useConversationStore((s) => [
|
||||
s.toggleMessageEditing,
|
||||
s.deleteMessage,
|
||||
s.regenerateUserMessage,
|
||||
s.regenerateAssistantMessage,
|
||||
s.translateMessage,
|
||||
s.ttsMessage,
|
||||
s.delAndRegenerateMessage,
|
||||
s.copyMessage,
|
||||
s.openThreadCreator,
|
||||
s.resendThreadMessage,
|
||||
s.delAndResendThreadMessage,
|
||||
s.toggleMessageCollapsed,
|
||||
]);
|
||||
|
||||
const getMessage = useCallback(
|
||||
() => dataSelectors.getDisplayMessageById(id)(storeApi.getState()),
|
||||
[id, storeApi],
|
||||
);
|
||||
|
||||
const menuItems = useMemo<MenuItem[]>(() => {
|
||||
if (!role) return [];
|
||||
|
||||
const {
|
||||
branching,
|
||||
collapse,
|
||||
copy,
|
||||
del,
|
||||
delAndRegenerate,
|
||||
divider,
|
||||
edit,
|
||||
expand,
|
||||
regenerate,
|
||||
share,
|
||||
translate,
|
||||
tts,
|
||||
} = actionsBar;
|
||||
|
||||
if (role === 'assistant') {
|
||||
if (error) {
|
||||
return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
const collapseAction = isCollapsed ? expand : collapse;
|
||||
const list: MenuItem[] = [edit, copy, collapseAction];
|
||||
|
||||
if (!inThread && !isGroupSession) list.push(branching);
|
||||
|
||||
list.push(
|
||||
divider,
|
||||
tts,
|
||||
translate,
|
||||
divider,
|
||||
share,
|
||||
divider,
|
||||
regenerate,
|
||||
delAndRegenerate,
|
||||
del,
|
||||
);
|
||||
|
||||
return list.filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
if (role === 'assistantGroup') {
|
||||
if (error) {
|
||||
return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
const collapseAction = isCollapsed ? expand : collapse;
|
||||
const list: MenuItem[] = [
|
||||
edit,
|
||||
copy,
|
||||
collapseAction,
|
||||
divider,
|
||||
share,
|
||||
divider,
|
||||
regenerate,
|
||||
del,
|
||||
];
|
||||
|
||||
return list.filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
const list: MenuItem[] = [edit, copy];
|
||||
|
||||
if (!inThread) list.push(branching);
|
||||
|
||||
list.push(divider, tts, translate, divider, regenerate, del);
|
||||
|
||||
return list.filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [actionsBar, error, inThread, isCollapsed, isGroupSession, role]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
const item = getMessage();
|
||||
if (!item || item.role !== 'assistant') return;
|
||||
|
||||
setShareMessage(item);
|
||||
setShareModalOpen(true);
|
||||
}, [getMessage]);
|
||||
|
||||
const handleShareClose = useCallback(() => {
|
||||
setShareModalOpen(false);
|
||||
setShareMessage(null);
|
||||
}, []);
|
||||
|
||||
const handleAction = useCallback(
|
||||
async (action: ContextMenuEvent) => {
|
||||
const item = getMessage();
|
||||
if (!item) return;
|
||||
|
||||
switch (action.key) {
|
||||
case 'edit': {
|
||||
toggleMessageEditing(id, true);
|
||||
break;
|
||||
}
|
||||
case 'copy': {
|
||||
await copyMessage(id, item.content);
|
||||
message.success(t('copySuccess'));
|
||||
break;
|
||||
}
|
||||
case 'expand':
|
||||
case 'collapse': {
|
||||
toggleMessageCollapsed(id);
|
||||
break;
|
||||
}
|
||||
case 'branching': {
|
||||
if (!topic) {
|
||||
message.warning(t('branchingRequiresSavedTopic'));
|
||||
break;
|
||||
}
|
||||
openThreadCreator(id);
|
||||
break;
|
||||
}
|
||||
case 'del': {
|
||||
deleteMessage(id);
|
||||
break;
|
||||
}
|
||||
case 'regenerate': {
|
||||
if (inPortalThread) {
|
||||
resendThreadMessage(id);
|
||||
} else if (role === 'assistant') {
|
||||
regenerateAssistantMessage(id);
|
||||
} else {
|
||||
regenerateUserMessage(id);
|
||||
}
|
||||
|
||||
if (item.error) deleteMessage(id);
|
||||
break;
|
||||
}
|
||||
case 'delAndRegenerate': {
|
||||
if (inPortalThread) {
|
||||
delAndResendThreadMessage(id);
|
||||
} else {
|
||||
delAndRegenerateMessage(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tts': {
|
||||
ttsMessage(id);
|
||||
break;
|
||||
}
|
||||
case 'share': {
|
||||
handleShare();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.keyPath?.at(-1) === 'translate') {
|
||||
const lang = action.keyPath[0];
|
||||
translateMessage(id, lang);
|
||||
}
|
||||
},
|
||||
[
|
||||
copyMessage,
|
||||
deleteMessage,
|
||||
delAndRegenerateMessage,
|
||||
delAndResendThreadMessage,
|
||||
getMessage,
|
||||
handleShare,
|
||||
id,
|
||||
index,
|
||||
inPortalThread,
|
||||
message,
|
||||
openThreadCreator,
|
||||
regenerateAssistantMessage,
|
||||
regenerateUserMessage,
|
||||
resendThreadMessage,
|
||||
role,
|
||||
t,
|
||||
toggleMessageCollapsed,
|
||||
toggleMessageEditing,
|
||||
topic,
|
||||
translateMessage,
|
||||
ttsMessage,
|
||||
virtuaRef,
|
||||
],
|
||||
);
|
||||
|
||||
const renderIcon = useCallback((iconComponent: ActionIconGroupItemType['icon']) => {
|
||||
if (!iconComponent) return null;
|
||||
|
||||
if (isValidElement(iconComponent)) {
|
||||
return <ActionIcon icon={iconComponent} size={'small'} />;
|
||||
}
|
||||
|
||||
const IconComponent = iconComponent as ComponentType<{ size?: number }>;
|
||||
|
||||
return <ActionIcon icon={<IconComponent size={16} />} size={'small'} />;
|
||||
}, []);
|
||||
|
||||
const dropdownMenuItems = useMemo(() => {
|
||||
return (menuItems ?? []).filter(Boolean).map((item) => {
|
||||
if ('type' in item && item.type === 'divider') return { type: 'divider' as const };
|
||||
|
||||
const actionItem = item as ActionMenuItem;
|
||||
const children = actionItem.children?.map((child) => ({
|
||||
key: child.key,
|
||||
label: child.label,
|
||||
}));
|
||||
const disabled =
|
||||
actionItem.disabled ??
|
||||
(typeof actionItem.disable === 'boolean' ? actionItem.disable : undefined);
|
||||
|
||||
return {
|
||||
children,
|
||||
danger: actionItem.danger,
|
||||
disabled,
|
||||
icon: renderIcon(actionItem.icon),
|
||||
key: actionItem.key,
|
||||
label: actionItem.label,
|
||||
popupClassName: actionItem.popupClassName,
|
||||
};
|
||||
});
|
||||
}, [menuItems, renderIcon]);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(info: Parameters<NonNullable<MenuProps['onClick']>>[0]) => {
|
||||
const event = {
|
||||
...info,
|
||||
selectedText,
|
||||
} as ContextMenuEvent;
|
||||
|
||||
handleAction(event);
|
||||
onClose();
|
||||
},
|
||||
[handleAction, onClose, selectedText],
|
||||
);
|
||||
|
||||
if (!visible || menuItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<>
|
||||
<div
|
||||
className={styles.trigger}
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownMenuItems,
|
||||
onClick: handleMenuClick,
|
||||
}}
|
||||
open={visible}
|
||||
placement="bottomLeft"
|
||||
trigger={[]}
|
||||
>
|
||||
<div
|
||||
className={styles.contextMenu}
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</>,
|
||||
document.body,
|
||||
)}
|
||||
{shareMessage && (
|
||||
<ShareMessageModal
|
||||
message={shareMessage}
|
||||
onCancel={handleShareClose}
|
||||
open={isShareModalOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContextMenu.displayName = 'ContextMenu';
|
||||
|
||||
export default ContextMenu;
|
||||
|
|
@ -15,7 +15,7 @@ enum Tab {
|
|||
Text = 'text',
|
||||
}
|
||||
|
||||
interface ShareModalProps {
|
||||
export interface ShareModalProps {
|
||||
message: UIChatMessage;
|
||||
onCancel: () => void;
|
||||
open: boolean;
|
||||
|
|
|
|||
|
|
@ -1,49 +1,338 @@
|
|||
import { type ActionIconGroupEvent } from '@lobehub/ui';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
type ActionIconGroupEvent,
|
||||
type ActionIconGroupItemType,
|
||||
type DropdownItem,
|
||||
type GenericItemType,
|
||||
createRawModal,
|
||||
showContextMenu,
|
||||
} from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MSG_CONTENT_CLASSNAME } from '@/features/Conversation/ChatItem/components/MessageContent';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
||||
|
||||
interface ContextMenuState {
|
||||
position: { x: number; y: number };
|
||||
selectedText?: string;
|
||||
visible: boolean;
|
||||
import ShareMessageModal from '../components/ShareMessageModal';
|
||||
import {
|
||||
dataSelectors,
|
||||
messageStateSelectors,
|
||||
useConversationStore,
|
||||
useConversationStoreApi,
|
||||
} from '../store';
|
||||
import { useChatListActionsBar } from './useChatListActionsBar';
|
||||
|
||||
interface ActionMenuItem extends ActionIconGroupItemType {
|
||||
children?: { key: string; label: ReactNode }[];
|
||||
disable?: boolean;
|
||||
popupClassName?: string;
|
||||
}
|
||||
|
||||
type MenuItem = ActionMenuItem | { type: 'divider' };
|
||||
type ContextMenuEvent = ActionIconGroupEvent & { selectedText?: string };
|
||||
|
||||
interface UseChatItemContextMenuProps {
|
||||
editing?: boolean;
|
||||
id: string;
|
||||
onActionClick: (action: ActionIconGroupEvent) => void;
|
||||
inPortalThread: boolean;
|
||||
topic?: string | null;
|
||||
}
|
||||
|
||||
export const useChatItemContextMenu = ({
|
||||
onActionClick,
|
||||
editing,
|
||||
}: Omit<UseChatItemContextMenuProps, 'id'>) => {
|
||||
id,
|
||||
inPortalThread,
|
||||
topic,
|
||||
}: UseChatItemContextMenuProps) => {
|
||||
const contextMenuMode = useUserStore(userGeneralSettingsSelectors.contextMenuMode);
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const [contextMenuState, setContextMenuState] = useState<ContextMenuState>({
|
||||
position: { x: 0, y: 0 },
|
||||
visible: false,
|
||||
});
|
||||
const selectedTextRef = useRef<string | undefined>(undefined);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const storeApi = useConversationStoreApi();
|
||||
|
||||
const [role, error, isCollapsed, hasThread, isRegenerating] = useConversationStore((s) => {
|
||||
const item = dataSelectors.getDisplayMessageById(id)(s);
|
||||
return [
|
||||
item?.role,
|
||||
item?.error,
|
||||
messageStateSelectors.isMessageCollapsed(id)(s),
|
||||
messageStateSelectors.hasThreadBySourceMsgId(id)(s),
|
||||
messageStateSelectors.isMessageRegenerating(id)(s),
|
||||
];
|
||||
}, isEqual);
|
||||
|
||||
const isThreadMode = useConversationStore(messageStateSelectors.isThreadMode);
|
||||
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
||||
const actionsBar = useChatListActionsBar({ hasThread, isRegenerating });
|
||||
const inThread = isThreadMode || inPortalThread;
|
||||
|
||||
const [
|
||||
toggleMessageEditing,
|
||||
deleteMessage,
|
||||
regenerateUserMessage,
|
||||
regenerateAssistantMessage,
|
||||
translateMessage,
|
||||
ttsMessage,
|
||||
delAndRegenerateMessage,
|
||||
copyMessage,
|
||||
openThreadCreator,
|
||||
resendThreadMessage,
|
||||
delAndResendThreadMessage,
|
||||
toggleMessageCollapsed,
|
||||
] = useConversationStore((s) => [
|
||||
s.toggleMessageEditing,
|
||||
s.deleteMessage,
|
||||
s.regenerateUserMessage,
|
||||
s.regenerateAssistantMessage,
|
||||
s.translateMessage,
|
||||
s.ttsMessage,
|
||||
s.delAndRegenerateMessage,
|
||||
s.copyMessage,
|
||||
s.openThreadCreator,
|
||||
s.resendThreadMessage,
|
||||
s.delAndResendThreadMessage,
|
||||
s.toggleMessageCollapsed,
|
||||
]);
|
||||
|
||||
const getMessage = useCallback(
|
||||
() => dataSelectors.getDisplayMessageById(id)(storeApi.getState()),
|
||||
[id, storeApi],
|
||||
);
|
||||
|
||||
const menuItems = useMemo<MenuItem[]>(() => {
|
||||
if (!role) return [];
|
||||
|
||||
const {
|
||||
branching,
|
||||
collapse,
|
||||
copy,
|
||||
del,
|
||||
delAndRegenerate,
|
||||
divider,
|
||||
edit,
|
||||
expand,
|
||||
regenerate,
|
||||
share,
|
||||
translate,
|
||||
tts,
|
||||
} = actionsBar;
|
||||
|
||||
if (role === 'assistant') {
|
||||
if (error) {
|
||||
return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
const collapseAction = isCollapsed ? expand : collapse;
|
||||
const list: MenuItem[] = [edit, copy, collapseAction];
|
||||
|
||||
if (!inThread && !isGroupSession) list.push(branching);
|
||||
|
||||
list.push(
|
||||
divider,
|
||||
tts,
|
||||
translate,
|
||||
divider,
|
||||
share,
|
||||
divider,
|
||||
regenerate,
|
||||
delAndRegenerate,
|
||||
del,
|
||||
);
|
||||
|
||||
return list.filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
if (role === 'assistantGroup') {
|
||||
if (error) {
|
||||
return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
const collapseAction = isCollapsed ? expand : collapse;
|
||||
const list: MenuItem[] = [
|
||||
edit,
|
||||
copy,
|
||||
collapseAction,
|
||||
divider,
|
||||
share,
|
||||
divider,
|
||||
regenerate,
|
||||
del,
|
||||
];
|
||||
|
||||
return list.filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
const list: MenuItem[] = [edit, copy];
|
||||
|
||||
if (!inThread) list.push(branching);
|
||||
|
||||
list.push(divider, tts, translate, divider, regenerate, del);
|
||||
|
||||
return list.filter(Boolean) as MenuItem[];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [actionsBar, error, inThread, isCollapsed, isGroupSession, role]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
const item = getMessage();
|
||||
if (!item || item.role !== 'assistant') return;
|
||||
|
||||
createRawModal(
|
||||
ShareMessageModal,
|
||||
{
|
||||
message: item,
|
||||
},
|
||||
{ onCloseKey: 'onCancel', openKey: 'open' },
|
||||
);
|
||||
}, [getMessage]);
|
||||
|
||||
const handleAction = useCallback(
|
||||
async (action: ContextMenuEvent) => {
|
||||
const item = getMessage();
|
||||
if (!item) return;
|
||||
|
||||
switch (action.key) {
|
||||
case 'edit': {
|
||||
toggleMessageEditing(id, true);
|
||||
break;
|
||||
}
|
||||
case 'copy': {
|
||||
await copyMessage(id, item.content);
|
||||
message.success(t('copySuccess'));
|
||||
break;
|
||||
}
|
||||
case 'expand':
|
||||
case 'collapse': {
|
||||
toggleMessageCollapsed(id);
|
||||
break;
|
||||
}
|
||||
case 'branching': {
|
||||
if (!topic) {
|
||||
message.warning(t('branchingRequiresSavedTopic'));
|
||||
break;
|
||||
}
|
||||
openThreadCreator(id);
|
||||
break;
|
||||
}
|
||||
case 'del': {
|
||||
deleteMessage(id);
|
||||
break;
|
||||
}
|
||||
case 'regenerate': {
|
||||
if (inPortalThread) {
|
||||
resendThreadMessage(id);
|
||||
} else if (role === 'assistant') {
|
||||
regenerateAssistantMessage(id);
|
||||
} else {
|
||||
regenerateUserMessage(id);
|
||||
}
|
||||
|
||||
if (item.error) deleteMessage(id);
|
||||
break;
|
||||
}
|
||||
case 'delAndRegenerate': {
|
||||
if (inPortalThread) {
|
||||
delAndResendThreadMessage(id);
|
||||
} else {
|
||||
delAndRegenerateMessage(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tts': {
|
||||
ttsMessage(id);
|
||||
break;
|
||||
}
|
||||
case 'share': {
|
||||
handleShare();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.keyPath?.[0] === 'translate') {
|
||||
const lang = action.keyPath.at(-1);
|
||||
if (lang) translateMessage(id, lang);
|
||||
}
|
||||
},
|
||||
[
|
||||
copyMessage,
|
||||
deleteMessage,
|
||||
delAndRegenerateMessage,
|
||||
delAndResendThreadMessage,
|
||||
getMessage,
|
||||
handleShare,
|
||||
id,
|
||||
inPortalThread,
|
||||
message,
|
||||
openThreadCreator,
|
||||
regenerateAssistantMessage,
|
||||
regenerateUserMessage,
|
||||
resendThreadMessage,
|
||||
role,
|
||||
t,
|
||||
toggleMessageCollapsed,
|
||||
toggleMessageEditing,
|
||||
topic,
|
||||
translateMessage,
|
||||
ttsMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(info: ActionIconGroupEvent) => {
|
||||
handleAction({
|
||||
...info,
|
||||
selectedText: selectedTextRef.current,
|
||||
} as ContextMenuEvent);
|
||||
},
|
||||
[handleAction],
|
||||
);
|
||||
|
||||
const contextMenuItems = useMemo<GenericItemType[]>(() => {
|
||||
if (!menuItems) return [];
|
||||
return menuItems.filter(Boolean).map((item) => {
|
||||
if ('type' in item && item.type === 'divider') return { type: 'divider' as const };
|
||||
|
||||
const actionItem = item as ActionMenuItem;
|
||||
const children = actionItem.children?.map((child) => ({
|
||||
key: child.key,
|
||||
label: child.label,
|
||||
onClick: handleMenuClick,
|
||||
}));
|
||||
const disabled =
|
||||
actionItem.disabled ??
|
||||
(typeof actionItem.disable === 'boolean' ? actionItem.disable : undefined);
|
||||
|
||||
return {
|
||||
children,
|
||||
danger: actionItem.danger,
|
||||
disabled,
|
||||
icon: actionItem.icon,
|
||||
key: actionItem.key,
|
||||
label: actionItem.label,
|
||||
onClick: children ? undefined : handleMenuClick,
|
||||
} satisfies DropdownItem;
|
||||
});
|
||||
}, [handleMenuClick, menuItems]);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
// Don't show context menu if disabled in settings
|
||||
(event: MouseEvent<HTMLDivElement>) => {
|
||||
if (contextMenuMode === 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show context menu in editing mode
|
||||
if (editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the clicked element or its parents have an id containing "msg_"
|
||||
let target = event.target as HTMLElement;
|
||||
let hasMessageId = false;
|
||||
|
||||
|
|
@ -55,82 +344,23 @@ export const useChatItemContextMenu = ({
|
|||
target = target.parentElement as HTMLElement;
|
||||
}
|
||||
|
||||
if (!hasMessageId) {
|
||||
if (!hasMessageId || contextMenuItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Get selected text
|
||||
const selection = window.getSelection();
|
||||
const selectedText = selection?.toString().trim() || '';
|
||||
selectedTextRef.current = selection?.toString().trim() || '';
|
||||
|
||||
setContextMenuState({
|
||||
position: { x: event.clientX, y: event.clientY },
|
||||
selectedText,
|
||||
visible: true,
|
||||
});
|
||||
console.log(contextMenuItems);
|
||||
showContextMenu(contextMenuItems);
|
||||
},
|
||||
[contextMenuMode, editing],
|
||||
[contextMenuItems, contextMenuMode, editing],
|
||||
);
|
||||
|
||||
const hideContextMenu = useCallback(() => {
|
||||
setContextMenuState((prev) => ({ ...prev, visible: false }));
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(action: ActionIconGroupEvent) => {
|
||||
if (action.key === 'quote' && contextMenuState.selectedText) {
|
||||
// Handle quote action - this will be integrated with ChatInput
|
||||
onActionClick({
|
||||
...action,
|
||||
selectedText: contextMenuState.selectedText,
|
||||
} as ActionIconGroupEvent & { selectedText: string });
|
||||
} else {
|
||||
onActionClick(action);
|
||||
}
|
||||
hideContextMenu();
|
||||
},
|
||||
[contextMenuState.selectedText, onActionClick, hideContextMenu],
|
||||
);
|
||||
|
||||
// Close context menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = () => {
|
||||
if (contextMenuState.visible) {
|
||||
hideContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = (event: Event) => {
|
||||
if (contextMenuState.visible) {
|
||||
// Check if the scroll event is from a dropdown sub-menu
|
||||
const target = event.target as HTMLElement;
|
||||
if (target && target.classList && target.classList.contains('ant-dropdown-menu-sub')) {
|
||||
return; // Don't hide the context menu when scrolling within sub-menu
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
if (contextMenuState.visible) {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('scroll', handleScroll, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('scroll', handleScroll, true);
|
||||
};
|
||||
}
|
||||
}, [contextMenuState.visible, hideContextMenu]);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
contextMenuMode,
|
||||
contextMenuState,
|
||||
handleContextMenu,
|
||||
handleMenuClick,
|
||||
hideContextMenu,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { Block, type BlockProps, Center, Flexbox, Icon, type IconProps, Text } from '@lobehub/ui';
|
||||
import {
|
||||
Block,
|
||||
type BlockProps,
|
||||
Center,
|
||||
ContextMenuTrigger,
|
||||
Flexbox,
|
||||
type GenericItemType,
|
||||
Icon,
|
||||
type IconProps,
|
||||
Text,
|
||||
} from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { Loader2Icon } from 'lucide-react';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
|
|
@ -18,6 +28,11 @@ const styles = createStaticStyles(({ css }) => ({
|
|||
margin-inline-end: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ${cssVar.motionEaseOut};
|
||||
|
||||
&:has([data-popup-open]) {
|
||||
width: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
@ -32,6 +47,7 @@ const styles = createStaticStyles(({ css }) => ({
|
|||
export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
|
||||
actions?: ReactNode;
|
||||
active?: boolean;
|
||||
contextMenuItems?: GenericItemType[] | (() => GenericItemType[]);
|
||||
disabled?: boolean;
|
||||
extra?: ReactNode;
|
||||
icon?: IconProps['icon'];
|
||||
|
|
@ -40,13 +56,25 @@ export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
|
|||
}
|
||||
|
||||
const NavItem = memo<NavItemProps>(
|
||||
({ className, actions, active, icon, title, onClick, disabled, loading, extra, ...rest }) => {
|
||||
({
|
||||
className,
|
||||
actions,
|
||||
contextMenuItems,
|
||||
active,
|
||||
icon,
|
||||
title,
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
extra,
|
||||
...rest
|
||||
}) => {
|
||||
const iconColor = active ? cssVar.colorText : cssVar.colorTextDescription;
|
||||
const textColor = active ? cssVar.colorText : cssVar.colorTextSecondary;
|
||||
const variant = active ? 'filled' : 'borderless';
|
||||
const iconComponent = loading ? Loader2Icon : icon;
|
||||
|
||||
return (
|
||||
const Content = (
|
||||
<Block
|
||||
align={'center'}
|
||||
className={cx(styles.container, className)}
|
||||
|
|
@ -102,6 +130,8 @@ const NavItem = memo<NavItemProps>(
|
|||
</Flexbox>
|
||||
</Block>
|
||||
);
|
||||
if (!contextMenuItems) return Content;
|
||||
return <ContextMenuTrigger items={contextMenuItems}>{Content}</ContextMenuTrigger>;
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,19 @@
|
|||
import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
|
||||
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ActionsProps {
|
||||
dropdownMenu: MenuProps['items'];
|
||||
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
||||
}
|
||||
|
||||
const Actions = memo<ActionsProps>(({ dropdownMenu }) => {
|
||||
if (!dropdownMenu || dropdownMenu.length === 0) return null;
|
||||
if (!dropdownMenu || (typeof dropdownMenu !== 'function' && dropdownMenu.length === 0))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
arrow={false}
|
||||
menu={{
|
||||
items: dropdownMenu,
|
||||
onClick: ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={dropdownMenu}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const TopicItem = memo<TopicItemProps>(
|
|||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active}
|
||||
contextMenuItems={dropdownMenu}
|
||||
onClick={() => {
|
||||
onTopicChange(topicId);
|
||||
onClose();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Icon, type MenuProps } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
|
@ -12,7 +12,10 @@ interface UseDropdownMenuProps {
|
|||
topicTitle: string;
|
||||
}
|
||||
|
||||
export const useDropdownMenu = ({ onClose, topicId }: UseDropdownMenuProps): MenuProps['items'] => {
|
||||
export const useDropdownMenu = ({
|
||||
onClose,
|
||||
topicId,
|
||||
}: UseDropdownMenuProps): (() => MenuProps['items']) => {
|
||||
const { t } = useTranslation(['common', 'topic']);
|
||||
const { modal } = App.useApp();
|
||||
const removeTopic = useChatStore((s) => s.removeTopic);
|
||||
|
|
@ -32,7 +35,7 @@ export const useDropdownMenu = ({ onClose, topicId }: UseDropdownMenuProps): Men
|
|||
});
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
return useCallback(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,42 +1,19 @@
|
|||
import { ActionIcon, Dropdown } from '@lobehub/ui';
|
||||
import { ActionIcon, DropdownMenu as DropdownMenuUI } from '@lobehub/ui';
|
||||
import { type ItemType } from 'antd/es/menu/interface';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { useFileItemDropdown } from './useFileItemDropdown';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface DropdownMenuProps {
|
||||
fileType: string;
|
||||
filename: string;
|
||||
id: string;
|
||||
knowledgeBaseId?: string;
|
||||
onRenameStart?: () => void;
|
||||
sourceType?: string;
|
||||
url: string;
|
||||
className?: string;
|
||||
items: ItemType[] | (() => ItemType[]);
|
||||
}
|
||||
|
||||
const DropdownMenu = memo<DropdownMenuProps>(
|
||||
({ id, knowledgeBaseId, url, filename, fileType, sourceType, onRenameStart }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Only compute dropdown items when dropdown is actually open
|
||||
// This prevents expensive hook execution for all 20-25 visible items
|
||||
const { menuItems } = useFileItemDropdown({
|
||||
enabled: isOpen,
|
||||
fileType,
|
||||
filename,
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
onRenameStart,
|
||||
sourceType,
|
||||
url,
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} onOpenChange={setIsOpen} open={isOpen}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
);
|
||||
},
|
||||
);
|
||||
const DropdownMenu = memo<DropdownMenuProps>(({ items, className }) => {
|
||||
return (
|
||||
<DropdownMenuUI items={items}>
|
||||
<ActionIcon className={className} icon={MoreHorizontalIcon} size={'small'} />
|
||||
</DropdownMenuUI>
|
||||
);
|
||||
});
|
||||
|
||||
export default DropdownMenu;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
PencilIcon,
|
||||
Trash,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RepoIcon from '@/components/LibIcon';
|
||||
|
|
@ -33,11 +33,10 @@ interface UseFileItemDropdownParams {
|
|||
}
|
||||
|
||||
interface UseFileItemDropdownReturn {
|
||||
menuItems: ItemType[];
|
||||
menuItems: () => ItemType[];
|
||||
}
|
||||
|
||||
export const useFileItemDropdown = ({
|
||||
enabled = true,
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
url,
|
||||
|
|
@ -65,10 +64,7 @@ export const useFileItemDropdown = ({
|
|||
const isFolder = fileType === 'custom/folder';
|
||||
const isPage = sourceType === 'document' || fileType === 'custom/document';
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
// Skip building menu items when dropdown is not open (performance optimization)
|
||||
if (!enabled) return [];
|
||||
|
||||
const menuItems = useCallback(() => {
|
||||
// Filter out current knowledge base and create submenu items
|
||||
const availableKnowledgeBases = (knowledgeBases || []).filter(
|
||||
(kb) => kb.id !== knowledgeBaseId,
|
||||
|
|
@ -264,7 +260,7 @@ export const useFileItemDropdown = ({
|
|||
},
|
||||
] as ItemType[]
|
||||
).filter(Boolean);
|
||||
}, [enabled, inKnowledgeBase, isFolder, knowledgeBases, knowledgeBaseId, id]);
|
||||
}, [inKnowledgeBase, isFolder, knowledgeBases, knowledgeBaseId, id]);
|
||||
|
||||
return { menuItems };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Center, Checkbox, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { Button, Center, Checkbox, ContextMenuTrigger, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { App, Input } from 'antd';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -23,6 +23,7 @@ import { formatSize } from '@/utils/format';
|
|||
import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
|
||||
|
||||
import DropdownMenu from '../../ItemDropdown/DropdownMenu';
|
||||
import { useFileItemDropdown } from '../../ItemDropdown/useFileItemDropdown';
|
||||
import ChunksBadge from './ChunkTag';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
|
@ -37,10 +38,6 @@ const styles = createStaticStyles(({ css }) => {
|
|||
|
||||
&:hover {
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
|
||||
.file-list-item-hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
|
|
@ -58,12 +55,14 @@ const styles = createStaticStyles(({ css }) => {
|
|||
opacity: 0.5;
|
||||
`,
|
||||
|
||||
hover: cx(
|
||||
'file-list-item-hover',
|
||||
css`
|
||||
opacity: 0;
|
||||
`,
|
||||
),
|
||||
hover: css`
|
||||
opacity: 0;
|
||||
|
||||
&[data-popup-open],
|
||||
.file-list-item-group:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
item: css`
|
||||
padding-block: 0;
|
||||
padding-inline: 0 24px;
|
||||
|
|
@ -324,169 +323,172 @@ const FileListItem = memo<FileListItemProps>(
|
|||
}
|
||||
}, [pendingRenameItemId, id, isFolder, resourceManagerState]);
|
||||
|
||||
const { menuItems } = useFileItemDropdown({
|
||||
fileType,
|
||||
filename: name,
|
||||
id,
|
||||
knowledgeBaseId: resourceManagerState.libraryId,
|
||||
onRenameStart: isFolder ? handleRenameStart : undefined,
|
||||
sourceType,
|
||||
url,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(
|
||||
styles.container,
|
||||
selected && styles.selected,
|
||||
isDragging && styles.dragging,
|
||||
isOver && styles.dragOver,
|
||||
)}
|
||||
data-drop-target-id={id}
|
||||
data-is-folder={String(isFolder)}
|
||||
draggable={!!resourceManagerState.libraryId}
|
||||
height={48}
|
||||
horizontal
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
onDrop={handleDrop}
|
||||
paddingInline={8}
|
||||
style={{
|
||||
borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
<ContextMenuTrigger items={menuItems}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.item}
|
||||
distribution={'space-between'}
|
||||
flex={1}
|
||||
className={cx(
|
||||
styles.container,
|
||||
'file-list-item-group',
|
||||
selected && styles.selected,
|
||||
isDragging && styles.dragging,
|
||||
isOver && styles.dragOver,
|
||||
)}
|
||||
data-drop-target-id={id}
|
||||
data-is-folder={String(isFolder)}
|
||||
draggable={!!resourceManagerState.libraryId}
|
||||
height={48}
|
||||
horizontal
|
||||
onClick={handleItemClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
onDrop={handleDrop}
|
||||
paddingInline={8}
|
||||
style={{
|
||||
borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
<Flexbox align={'center'} className={styles.nameContainer} horizontal>
|
||||
<Center
|
||||
height={48}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
onSelectedChange(id, !selected, e.shiftKey, index);
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
style={{ paddingInline: 4 }}
|
||||
>
|
||||
<Checkbox checked={selected} />
|
||||
</Center>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
justify={'center'}
|
||||
style={{ fontSize: 24, marginInline: 8, width: 24 }}
|
||||
>
|
||||
{isFolder ? (
|
||||
<Icon icon={FolderIcon} size={24} />
|
||||
) : isPage ? (
|
||||
emoji ? (
|
||||
<span style={{ fontSize: 24 }}>{emoji}</span>
|
||||
) : (
|
||||
<Center height={24} width={24}>
|
||||
<Icon icon={FileText} size={24} />
|
||||
</Center>
|
||||
)
|
||||
) : (
|
||||
<FileIcon fileName={name} fileType={fileType} size={24} />
|
||||
)}
|
||||
</Flexbox>
|
||||
{isRenaming && isFolder ? (
|
||||
<Input
|
||||
onBlur={handleRenameConfirm}
|
||||
onChange={(e) => setRenamingValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRenameConfirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleRenameCancel();
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
ref={inputRef}
|
||||
size="small"
|
||||
style={{ flex: 1, maxWidth: 400 }}
|
||||
value={renamingValue}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.name}>{name || t('file:pageList.untitled')}</span>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={8}
|
||||
className={styles.item}
|
||||
distribution={'space-between'}
|
||||
flex={1}
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
{!isFolder &&
|
||||
(fileStoreState.isCreatingFileParseTask ||
|
||||
isNull(chunkingStatus) ||
|
||||
!chunkingStatus ? (
|
||||
<div
|
||||
className={fileStoreState.isCreatingFileParseTask ? undefined : styles.hover}
|
||||
title={t(
|
||||
isSupportedForChunking
|
||||
? 'FileManager.actions.chunkingTooltip'
|
||||
: 'FileManager.actions.chunkingUnsupported',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
disabled={!isSupportedForChunking}
|
||||
icon={FileBoxIcon}
|
||||
loading={fileStoreState.isCreatingFileParseTask}
|
||||
onClick={() => {
|
||||
fileStoreState.parseFiles([id]);
|
||||
}}
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
>
|
||||
{t(
|
||||
fileStoreState.isCreatingFileParseTask
|
||||
? 'FileManager.actions.createChunkingTask'
|
||||
: 'FileManager.actions.chunking',
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Flexbox align={'center'} className={styles.nameContainer} horizontal>
|
||||
<Center
|
||||
height={48}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
onSelectedChange(id, !selected, e.shiftKey, index);
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
style={{ paddingInline: 4 }}
|
||||
>
|
||||
<Checkbox checked={selected} />
|
||||
</Center>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
justify={'center'}
|
||||
style={{ fontSize: 24, marginInline: 8, width: 24 }}
|
||||
>
|
||||
{isFolder ? (
|
||||
<Icon icon={FolderIcon} size={24} />
|
||||
) : isPage ? (
|
||||
emoji ? (
|
||||
<span style={{ fontSize: 24 }}>{emoji}</span>
|
||||
) : (
|
||||
<Center height={24} width={24}>
|
||||
<Icon icon={FileText} size={24} />
|
||||
</Center>
|
||||
)
|
||||
) : (
|
||||
<FileIcon fileName={name} fileType={fileType} size={24} />
|
||||
)}
|
||||
</Flexbox>
|
||||
{isRenaming && isFolder ? (
|
||||
<Input
|
||||
onBlur={handleRenameConfirm}
|
||||
onChange={(e) => setRenamingValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRenameConfirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleRenameCancel();
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
ref={inputRef}
|
||||
size="small"
|
||||
style={{ flex: 1, maxWidth: 400 }}
|
||||
value={renamingValue}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ cursor: 'default' }}>
|
||||
<ChunksBadge
|
||||
chunkCount={chunkCount}
|
||||
chunkingError={chunkingError}
|
||||
chunkingStatus={chunkingStatus}
|
||||
embeddingError={embeddingError}
|
||||
embeddingStatus={embeddingStatus}
|
||||
finishEmbedding={finishEmbedding}
|
||||
id={id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.hover}>
|
||||
<DropdownMenu
|
||||
fileType={fileType}
|
||||
filename={name}
|
||||
id={id}
|
||||
knowledgeBaseId={resourceManagerState.libraryId}
|
||||
onRenameStart={isFolder ? handleRenameStart : undefined}
|
||||
sourceType={sourceType}
|
||||
url={url}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.name}>{name || t('file:pageList.untitled')}</span>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={8}
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{!isFolder &&
|
||||
(fileStoreState.isCreatingFileParseTask ||
|
||||
isNull(chunkingStatus) ||
|
||||
!chunkingStatus ? (
|
||||
<div
|
||||
className={fileStoreState.isCreatingFileParseTask ? undefined : styles.hover}
|
||||
title={t(
|
||||
isSupportedForChunking
|
||||
? 'FileManager.actions.chunkingTooltip'
|
||||
: 'FileManager.actions.chunkingUnsupported',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
disabled={!isSupportedForChunking}
|
||||
icon={FileBoxIcon}
|
||||
loading={fileStoreState.isCreatingFileParseTask}
|
||||
onClick={() => {
|
||||
fileStoreState.parseFiles([id]);
|
||||
}}
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
>
|
||||
{t(
|
||||
fileStoreState.isCreatingFileParseTask
|
||||
? 'FileManager.actions.createChunkingTask'
|
||||
: 'FileManager.actions.chunking',
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ cursor: 'default' }}>
|
||||
<ChunksBadge
|
||||
chunkCount={chunkCount}
|
||||
chunkingError={chunkingError}
|
||||
chunkingStatus={chunkingStatus}
|
||||
embeddingError={embeddingError}
|
||||
embeddingStatus={embeddingStatus}
|
||||
finishEmbedding={finishEmbedding}
|
||||
id={id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<DropdownMenu className={styles.hover} items={menuItems} />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{!isDragging && (
|
||||
<>
|
||||
<Flexbox className={styles.item} width={FILE_DATE_WIDTH}>
|
||||
{displayTime}
|
||||
</Flexbox>
|
||||
<Flexbox className={styles.item} width={FILE_SIZE_WIDTH}>
|
||||
{isFolder || isPage ? '-' : formatSize(size)}
|
||||
</Flexbox>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
{!isDragging && (
|
||||
<>
|
||||
<Flexbox className={styles.item} width={FILE_DATE_WIDTH}>
|
||||
{displayTime}
|
||||
</Flexbox>
|
||||
<Flexbox className={styles.item} width={FILE_SIZE_WIDTH}>
|
||||
{isFolder || isPage ? '-' : formatSize(size)}
|
||||
</Flexbox>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
</ContextMenuTrigger>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Checkbox } from '@lobehub/ui';
|
||||
import { Checkbox, showContextMenu } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
|
@ -13,6 +13,7 @@ import { documentService } from '@/services/document';
|
|||
import { type FileListItem } from '@/types/files';
|
||||
|
||||
import DropdownMenu from '../../ItemDropdown/DropdownMenu';
|
||||
import { useFileItemDropdown } from '../../ItemDropdown/useFileItemDropdown';
|
||||
import DefaultFileItem from './DefaultFileItem';
|
||||
import ImageFileItem from './ImageFileItem';
|
||||
import MarkdownFileItem from './MarkdownFileItem';
|
||||
|
|
@ -346,6 +347,15 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
|||
}
|
||||
}, [isMarkdown, isPage, url, isInView, markdownContent, id]);
|
||||
|
||||
const { menuItems } = useFileItemDropdown({
|
||||
fileType,
|
||||
filename: name,
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
sourceType,
|
||||
url,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
|
|
@ -357,6 +367,10 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
|||
data-drop-target-id={id}
|
||||
data-is-folder={isFolder}
|
||||
draggable={!!knowledgeBaseId}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
showContextMenu(menuItems());
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
|
|
@ -379,13 +393,7 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
|||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu
|
||||
fileType={fileType}
|
||||
filename={name}
|
||||
id={id}
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
url={url}
|
||||
/>
|
||||
<DropdownMenu items={menuItems} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -10,14 +10,15 @@ interface ActionIconWithChevronProps extends ComponentProps<typeof Button> {
|
|||
}
|
||||
|
||||
const ActionIconWithChevron = memo<ActionIconWithChevronProps>(
|
||||
({ icon, title, style, disabled, type = 'text', ...rest }) => {
|
||||
({ icon, title, style, disabled, className, ...rest }) => {
|
||||
return (
|
||||
<Button
|
||||
{...rest}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
style={{ paddingInline: 4, ...style }}
|
||||
title={title}
|
||||
type={type}
|
||||
{...rest}
|
||||
type={'text'}
|
||||
>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Icon color={cssVar.colorIcon} icon={icon} size={18} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Icon } from '@lobehub/ui';
|
||||
import { App, Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { type DropdownItem, DropdownMenu, Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import {
|
||||
BookMinusIcon,
|
||||
BookPlusIcon,
|
||||
|
|
@ -36,8 +35,8 @@ const BatchActionsDropdown = memo<BatchActionsDropdownProps>(
|
|||
|
||||
const libraryId = useResourceManagerStore((s) => s.libraryId);
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
const items: MenuProps['items'] = [];
|
||||
const menuItems = useMemo<DropdownItem[]>(() => {
|
||||
const items: DropdownItem[] = [];
|
||||
|
||||
// Show delete library option only when in a knowledge base and no files selected
|
||||
if (libraryId && selectCount === 0) {
|
||||
|
|
@ -138,18 +137,13 @@ const BatchActionsDropdown = memo<BatchActionsDropdownProps>(
|
|||
}, [libraryId, selectCount, onActionClick, t, modal, message]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
menu={{ items: menuItems }}
|
||||
placement="bottomLeft"
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={menuItems} placement="bottomLeft" triggerProps={{ disabled }}>
|
||||
<ActionIconWithChevron
|
||||
disabled={disabled}
|
||||
icon={CircleEllipsisIcon}
|
||||
title={t('FileManager.actions.batchActions', 'Batch actions')}
|
||||
/>
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
||||
import { ArrowDownAZ } from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -22,22 +21,23 @@ const SortDropdown = memo(() => {
|
|||
[t],
|
||||
);
|
||||
|
||||
const menuItems: MenuProps['items'] = sortOptions.map((option) => ({
|
||||
const menuItems: DropdownItem[] = sortOptions.map((option) => ({
|
||||
key: option.key,
|
||||
label: option.label,
|
||||
onClick: () => setSorter(option.key as 'name' | 'createdAt' | 'size'),
|
||||
style:
|
||||
option.key === (sorter || 'createdAt')
|
||||
? { backgroundColor: 'var(--ant-control-item-bg-active)' }
|
||||
: {},
|
||||
}));
|
||||
|
||||
const currentSortLabel =
|
||||
sortOptions.find((option) => option.key === sorter)?.label || t('FileManager.sort.dateAdded');
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{ items: menuItems, selectedKeys: [sorter || 'createdAt'] }}
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={menuItems}>
|
||||
<ActionIconWithChevron icon={ArrowDownAZ} title={currentSortLabel} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { Icon } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { type DropdownItem, DropdownMenu, Icon } from '@lobehub/ui';
|
||||
import { Grid3x3Icon, ListIcon } from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -20,31 +18,30 @@ const ViewSwitcher = memo(() => {
|
|||
const currentViewLabel =
|
||||
viewMode === 'list' ? t('FileManager.view.list') : t('FileManager.view.masonry');
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
const menuItems = useMemo<DropdownItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
icon: <Icon icon={ListIcon} />,
|
||||
key: 'list',
|
||||
label: t('FileManager.view.list'),
|
||||
onClick: () => setViewMode('list'),
|
||||
style: viewMode === 'list' ? { backgroundColor: 'var(--ant-control-item-bg-active)' } : {},
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Grid3x3Icon} />,
|
||||
key: 'masonry',
|
||||
label: t('FileManager.view.masonry'),
|
||||
onClick: () => setViewMode('masonry'),
|
||||
style:
|
||||
viewMode === 'masonry' ? { backgroundColor: 'var(--ant-control-item-bg-active)' } : {},
|
||||
},
|
||||
];
|
||||
}, [setViewMode, t]);
|
||||
}, [setViewMode, t, viewMode]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{ items: menuItems, selectedKeys: [viewMode] }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
>
|
||||
<DropdownMenu items={menuItems} placement="bottomRight">
|
||||
<ActionIconWithChevron icon={currentViewIcon} title={currentViewLabel} />
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { CaretDownFilled, LoadingOutlined } from '@ant-design/icons';
|
||||
import { ActionIcon, Block, Dropdown, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { ActionIcon, Block, Flexbox, Icon, showContextMenu } from '@lobehub/ui';
|
||||
import { App, Input } from 'antd';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { FileText, FolderIcon, FolderOpenIcon } from 'lucide-react';
|
||||
|
|
@ -328,100 +328,97 @@ const FileTreeRow = memo<{
|
|||
|
||||
return (
|
||||
<Flexbox gap={2}>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Block
|
||||
align={'center'}
|
||||
className={cx(
|
||||
styles.treeItem,
|
||||
isOver && styles.fileItemDragOver,
|
||||
isDragging && styles.dragging,
|
||||
)}
|
||||
clickable
|
||||
data-drop-target-id={item.id}
|
||||
data-is-folder={String(item.isFolder)}
|
||||
draggable
|
||||
gap={8}
|
||||
height={36}
|
||||
horizontal
|
||||
onClick={() => handleFolderClick(item.id, item.slug)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
onDrop={handleDrop}
|
||||
paddingInline={4}
|
||||
style={{
|
||||
paddingInlineStart: level * 12 + 4,
|
||||
}}
|
||||
variant={isActive ? 'filled' : 'borderless'}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Block
|
||||
align={'center'}
|
||||
className={cx(
|
||||
styles.treeItem,
|
||||
isOver && styles.fileItemDragOver,
|
||||
isDragging && styles.dragging,
|
||||
)}
|
||||
clickable
|
||||
data-drop-target-id={item.id}
|
||||
data-is-folder={String(item.isFolder)}
|
||||
draggable
|
||||
gap={8}
|
||||
height={36}
|
||||
horizontal
|
||||
onClick={() => handleFolderClick(item.id, item.slug)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
showContextMenu(menuItems());
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
onDrop={handleDrop}
|
||||
paddingInline={4}
|
||||
style={{
|
||||
paddingInlineStart: level * 12 + 4,
|
||||
}}
|
||||
variant={isActive ? 'filled' : 'borderless'}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActionIcon icon={LoadingOutlined as any} size={'small'} spin style={{ width: 20 }} />
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 0 : -90 }}
|
||||
initial={false}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={LoadingOutlined as any}
|
||||
icon={CaretDownFilled as any}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
size={'small'}
|
||||
spin
|
||||
style={{ width: 20 }}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
flex={1}
|
||||
gap={8}
|
||||
horizontal
|
||||
style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
|
||||
>
|
||||
<Icon icon={isExpanded ? FolderOpenIcon : FolderIcon} size={18} />
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
onBlur={handleRenameConfirm}
|
||||
onChange={(e) => setRenamingValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRenameConfirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleRenameCancel();
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
ref={inputRef}
|
||||
size="small"
|
||||
style={{ flex: 1 }}
|
||||
value={renamingValue}
|
||||
/>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 0 : -90 }}
|
||||
initial={false}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={CaretDownFilled as any}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
size={'small'}
|
||||
style={{ width: 20 }}
|
||||
/>
|
||||
</motion.div>
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
flex={1}
|
||||
gap={8}
|
||||
horizontal
|
||||
style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
|
||||
>
|
||||
<Icon icon={isExpanded ? FolderOpenIcon : FolderIcon} size={18} />
|
||||
{isRenaming ? (
|
||||
<Input
|
||||
onBlur={handleRenameConfirm}
|
||||
onChange={(e) => setRenamingValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRenameConfirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleRenameCancel();
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
ref={inputRef}
|
||||
size="small"
|
||||
style={{ flex: 1 }}
|
||||
value={renamingValue}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Dropdown>
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
|
@ -430,52 +427,54 @@ const FileTreeRow = memo<{
|
|||
const isActive = selectedKey === itemKey;
|
||||
return (
|
||||
<Flexbox gap={2}>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Block
|
||||
<Block
|
||||
align={'center'}
|
||||
className={cx(styles.treeItem, isDragging && styles.dragging)}
|
||||
clickable
|
||||
data-drop-target-id={item.id}
|
||||
data-is-folder={false}
|
||||
draggable
|
||||
gap={8}
|
||||
height={36}
|
||||
horizontal
|
||||
onClick={handleItemClick}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
showContextMenu(menuItems());
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
paddingInline={4}
|
||||
style={{
|
||||
paddingInlineStart: level * 12 + 4,
|
||||
}}
|
||||
variant={isActive ? 'filled' : 'borderless'}
|
||||
>
|
||||
<div style={{ width: 20 }} />
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.treeItem, isDragging && styles.dragging)}
|
||||
clickable
|
||||
data-drop-target-id={item.id}
|
||||
data-is-folder={false}
|
||||
draggable
|
||||
flex={1}
|
||||
gap={8}
|
||||
height={36}
|
||||
horizontal
|
||||
onClick={handleItemClick}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
paddingInline={4}
|
||||
style={{
|
||||
paddingInlineStart: level * 12 + 4,
|
||||
}}
|
||||
variant={isActive ? 'filled' : 'borderless'}
|
||||
style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ width: 20 }} />
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
flex={1}
|
||||
gap={8}
|
||||
horizontal
|
||||
style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
|
||||
{item.sourceType === 'document' ? (
|
||||
<Icon icon={FileText} size={18} />
|
||||
) : (
|
||||
<FileIcon fileName={item.name} fileType={item.fileType} size={18} />
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{item.sourceType === 'document' ? (
|
||||
<Icon icon={FileText} size={18} />
|
||||
) : (
|
||||
<FileIcon fileName={item.name} fileType={item.fileType} size={18} />
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Dropdown>
|
||||
{item.name}
|
||||
</span>
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { ModalHost } from '@lobehub/ui';
|
||||
import { ContextMenuHost, ModalHost, TooltipGroup } from '@lobehub/ui';
|
||||
import { LazyMotion, domMax } from 'motion/react';
|
||||
import { type ReactNode, Suspense } from 'react';
|
||||
|
||||
|
|
@ -66,8 +66,11 @@ const GlobalLayout = async ({
|
|||
<GroupWizardProvider>
|
||||
<DragUploadProvider>
|
||||
<LazyMotion features={domMax}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
<TooltipGroup layoutAnimation={false}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
</TooltipGroup>
|
||||
<ModalHost />
|
||||
<ContextMenuHost />
|
||||
</LazyMotion>
|
||||
</DragUploadProvider>
|
||||
</GroupWizardProvider>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { CLASSNAMES } from '@lobehub/ui';
|
||||
import { type Theme, css } from 'antd-style';
|
||||
|
||||
// fix ios input keyboard
|
||||
|
|
@ -55,4 +56,9 @@ export default ({ token }: { prefixCls: string; token: Theme }) => css`
|
|||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.${CLASSNAMES.ContextTrigger}[data-popup-open],
|
||||
.${CLASSNAMES.DropdownMenuTrigger}[data-popup-open] {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue