mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-18 13:25:45 +00:00
♻️ refactor: improve modal handling with createRawModal (#11071)
* feat: integrate TooltipGroup into SideBarLayout for enhanced UI interactions Signed-off-by: Innei <tukon479@gmail.com> * feat: refactor components to utilize createRawModal for improved modal handling and enhance UI interactions with TooltipGroup Signed-off-by: Innei <tukon479@gmail.com> * chore: update @lobehub/ui dependency to version 4.5.0 in package.json Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
+1
-1
@@ -208,7 +208,7 @@
|
||||
"@lobehub/icons": "^4.0.2",
|
||||
"@lobehub/market-sdk": "^0.25.0",
|
||||
"@lobehub/tts": "^4.0.2",
|
||||
"@lobehub/ui": "^4.4.5",
|
||||
"@lobehub/ui": "^4.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@neondatabase/serverless": "^1.0.2",
|
||||
"@next/third-parties": "^16.1.1",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Flexbox, Tag, Tooltip } from '@lobehub/ui';
|
||||
import { Flexbox, Tag, Tooltip, createRawModal } from '@lobehub/ui';
|
||||
import { Progress } from 'antd';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useEventCallback } from '@/hooks/useEventCallback';
|
||||
import { useFileStore } from '@/store/file';
|
||||
import { UPLOAD_STATUS_SET, type UploadFileItem } from '@/types/files/upload';
|
||||
|
||||
@@ -65,39 +66,34 @@ type FileItemProps = UploadFileItem;
|
||||
const ContextItem = memo<FileItemProps>((props) => {
|
||||
const { file, id, status, uploadState } = props;
|
||||
const [removeChatUploadFile] = useFileStore((s) => [s.removeChatUploadFile]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const basename = getFileBasename(file.name);
|
||||
const isUploading = UPLOAD_STATUS_SET.has(status);
|
||||
const progress = uploadState?.progress ?? 0;
|
||||
|
||||
const handleClick = () => {
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
const handleClick = useEventCallback(() => {
|
||||
createRawModal(FilePreviewModal, {
|
||||
file: props,
|
||||
});
|
||||
});
|
||||
|
||||
const handleClose = useEventCallback(() => {
|
||||
removeChatUploadFile(id);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Tag closable onClick={handleClick} onClose={() => removeChatUploadFile(id)} size={'large'}>
|
||||
<Flexbox className={styles.icon}>
|
||||
<Content {...props} />
|
||||
{isUploading && (
|
||||
<div className={styles.progress}>
|
||||
<Progress
|
||||
percent={progress}
|
||||
showInfo={false}
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
type="circle"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Tooltip title={file.name}>
|
||||
<span className={styles.name}>{basename}</span>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
<FilePreviewModal file={props} onClose={() => setPreviewOpen(false)} open={previewOpen} />
|
||||
</>
|
||||
<Tag closable onClick={handleClick} onClose={handleClose} size={'large'}>
|
||||
<Flexbox className={styles.icon}>
|
||||
<Content {...props} />
|
||||
{isUploading && (
|
||||
<div className={styles.progress}>
|
||||
<Progress percent={progress} showInfo={false} size={14} strokeWidth={2} type="circle" />
|
||||
</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Tooltip title={file.name}>
|
||||
<span className={styles.name}>{basename}</span>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const ChatItem = memo<ChatItemProps>(
|
||||
<ErrorContent customErrorRender={customErrorRender} error={error} id={id} />
|
||||
);
|
||||
|
||||
const avtarContent = (
|
||||
const avatarContent = (
|
||||
<Avatar
|
||||
alt={avatarProps?.alt || avatar.title || 'avatar'}
|
||||
loading={loading}
|
||||
@@ -84,7 +84,7 @@ const ChatItem = memo<ChatItemProps>(
|
||||
gap={8}
|
||||
>
|
||||
{showAvatar &&
|
||||
(customAvatarRender ? customAvatarRender(avatar, avtarContent) : avtarContent)}
|
||||
(customAvatarRender ? customAvatarRender(avatar, avatarContent) : avatarContent)}
|
||||
<Title avatar={avatar} showTitle={showTitle} time={time} titleAddon={titleAddon} />
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { ActionIcon, createRawModal } from '@lobehub/ui';
|
||||
import { LucideSettings } from 'lucide-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PluginDetailModalProps } from '@/features/PluginDetailModal';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
|
||||
const PluginDetailModal = dynamic(() => import('@/features/PluginDetailModal'), { ssr: false });
|
||||
const PluginDetailModal = dynamic<PluginDetailModalProps>(
|
||||
() => import('@/features/PluginDetailModal'),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
const Settings = memo<{ id: string }>(({ id }) => {
|
||||
const item = useToolStore(pluginSelectors.getToolManifestById(id));
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation('plugin');
|
||||
const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(item?.settings);
|
||||
if (!hasSettings) return;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
icon={LucideSettings}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('setting')}
|
||||
/>
|
||||
<PluginDetailModal
|
||||
id={id}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
open={open}
|
||||
schema={item?.settings}
|
||||
/>
|
||||
</>
|
||||
<ActionIcon
|
||||
icon={LucideSettings}
|
||||
onClick={() => {
|
||||
createRawModal(PluginDetailModal, {
|
||||
id,
|
||||
schema: item?.settings,
|
||||
});
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('setting')}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flexbox, type FlexboxProps } from '@lobehub/ui';
|
||||
import { Flexbox, type FlexboxProps, TooltipGroup } from '@lobehub/ui';
|
||||
import { type CSSProperties, type ReactNode, memo } from 'react';
|
||||
|
||||
import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
|
||||
@@ -35,18 +35,20 @@ const NavHeader = memo<NavHeaderProps>(
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
<Flexbox align={'center'} gap={2} horizontal justify={'flex-start'} style={styles?.left}>
|
||||
{showTogglePanelButton && !expand && <ToggleLeftPanelButton />}
|
||||
{left}
|
||||
</Flexbox>
|
||||
{children && (
|
||||
<Flexbox flex={1} style={styles?.center}>
|
||||
{children}
|
||||
<TooltipGroup>
|
||||
<Flexbox align={'center'} gap={2} horizontal justify={'flex-start'} style={styles?.left}>
|
||||
{showTogglePanelButton && !expand && <ToggleLeftPanelButton />}
|
||||
{left}
|
||||
</Flexbox>
|
||||
)}
|
||||
<Flexbox align={'center'} gap={2} horizontal justify={'flex-end'} style={styles?.right}>
|
||||
{right}
|
||||
</Flexbox>
|
||||
{children && (
|
||||
<Flexbox flex={1} style={styles?.center}>
|
||||
{children}
|
||||
</Flexbox>
|
||||
)}
|
||||
<Flexbox align={'center'} gap={2} horizontal justify={'flex-end'} style={styles?.right}>
|
||||
{right}
|
||||
</Flexbox>
|
||||
</TooltipGroup>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flexbox, ScrollShadow } from '@lobehub/ui';
|
||||
import { Flexbox, ScrollShadow, TooltipGroup } from '@lobehub/ui';
|
||||
import { type ReactNode, Suspense, memo } from 'react';
|
||||
|
||||
import Footer from '@/app/[variants]/(main)/home/_layout/Footer';
|
||||
@@ -15,7 +15,9 @@ const SideBarLayout = memo<SidebarLayoutProps>(({ header, body, footer }) => {
|
||||
<Flexbox gap={4} style={{ height: '100%', overflow: 'hidden' }}>
|
||||
<Suspense fallback={<SkeletonItem height={44} style={{ marginTop: 8 }} />}>{header}</Suspense>
|
||||
<ScrollShadow size={2} style={{ height: '100%' }}>
|
||||
<Suspense fallback={<SkeletonList paddingBlock={8} />}>{body}</Suspense>
|
||||
<TooltipGroup>
|
||||
<Suspense fallback={<SkeletonList paddingBlock={8} />}>{body}</Suspense>
|
||||
</TooltipGroup>
|
||||
</ScrollShadow>
|
||||
<Suspense>{footer || <Footer />}</Suspense>
|
||||
</Flexbox>
|
||||
|
||||
@@ -9,11 +9,11 @@ import { pluginHelpers } from '@/store/tool';
|
||||
import APIs from './APIs';
|
||||
import Meta from './Meta';
|
||||
|
||||
interface PluginDetailModalProps {
|
||||
export interface PluginDetailModalProps {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
onTabChange?: (key: string) => void;
|
||||
open?: boolean;
|
||||
open: boolean;
|
||||
schema: any;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const DropdownMenu = memo<DropdownMenuProps>(
|
||||
|
||||
// Only compute dropdown items when dropdown is actually open
|
||||
// This prevents expensive hook execution for all 20-25 visible items
|
||||
const { menuItems, moveModal } = useFileItemDropdown({
|
||||
const { menuItems } = useFileItemDropdown({
|
||||
enabled: isOpen,
|
||||
fileType,
|
||||
filename,
|
||||
@@ -32,12 +32,9 @@ const DropdownMenu = memo<DropdownMenuProps>(
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown menu={{ items: menuItems }} onOpenChange={setIsOpen} open={isOpen}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
{moveModal}
|
||||
</>
|
||||
<Dropdown menu={{ items: menuItems }} onOpenChange={setIsOpen} open={isOpen}>
|
||||
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
||||
</Dropdown>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+8
-16
@@ -1,4 +1,4 @@
|
||||
import { Icon, copyToClipboard } from '@lobehub/ui';
|
||||
import { Icon, copyToClipboard, createRawModal } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { type ItemType } from 'antd/es/menu/interface';
|
||||
import {
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PencilIcon,
|
||||
Trash,
|
||||
} from 'lucide-react';
|
||||
import { type ReactNode, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RepoIcon from '@/components/LibIcon';
|
||||
@@ -34,7 +34,6 @@ interface UseFileItemDropdownParams {
|
||||
|
||||
interface UseFileItemDropdownReturn {
|
||||
menuItems: ItemType[];
|
||||
moveModal: ReactNode;
|
||||
}
|
||||
|
||||
export const useFileItemDropdown = ({
|
||||
@@ -50,8 +49,6 @@ export const useFileItemDropdown = ({
|
||||
const { t } = useTranslation(['components', 'common', 'knowledgeBase']);
|
||||
const { message, modal } = App.useApp();
|
||||
|
||||
const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
|
||||
|
||||
const [removeFile, refreshFileList] = useFileStore((s) => [s.removeFileItem, s.refreshFileList]);
|
||||
const [removeFilesFromKnowledgeBase, addFilesToKnowledgeBase, useFetchKnowledgeBaseList] =
|
||||
useKnowledgeBaseStore((s) => [
|
||||
@@ -154,7 +151,11 @@ export const useFileItemDropdown = ({
|
||||
label: t('FileManager.actions.moveToFolder'),
|
||||
onClick: async ({ domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
setIsMoveModalOpen(true);
|
||||
|
||||
createRawModal(MoveToFolderModal, {
|
||||
fileId: id,
|
||||
knowledgeBaseId,
|
||||
});
|
||||
},
|
||||
},
|
||||
isFolder && {
|
||||
@@ -264,14 +265,5 @@ export const useFileItemDropdown = ({
|
||||
).filter(Boolean);
|
||||
}, [enabled, inKnowledgeBase, isFolder, knowledgeBases, knowledgeBaseId, id]);
|
||||
|
||||
const moveModal = (
|
||||
<MoveToFolderModal
|
||||
fileId={id}
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
onClose={() => setIsMoveModalOpen(false)}
|
||||
open={isMoveModalOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
return { menuItems, moveModal };
|
||||
return { menuItems };
|
||||
};
|
||||
|
||||
@@ -250,7 +250,7 @@ const ListView = memo(() => {
|
||||
/>
|
||||
);
|
||||
}}
|
||||
overscan={600}
|
||||
overscan={48 * 5}
|
||||
ref={virtuosoRef}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FileText, FolderIcon, FolderOpenIcon } from 'lucide-react';
|
||||
import * as motion from 'motion/react-m';
|
||||
import React, { memo, useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VList } from 'virtua';
|
||||
|
||||
import {
|
||||
getTransparentDragImage,
|
||||
@@ -130,13 +131,12 @@ interface TreeItem {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Recursive component to render folder and file tree
|
||||
const FileTreeItem = memo<{
|
||||
// Row component for folder / file tree (virtualized by flattening visible nodes)
|
||||
const FileTreeRow = memo<{
|
||||
expandedFolders: Set<string>;
|
||||
folderChildrenCache: Map<string, TreeItem[]>;
|
||||
item: TreeItem;
|
||||
level?: number;
|
||||
loadedFolders: Set<string>;
|
||||
loadingFolders: Set<string>;
|
||||
onLoadFolder: (_: string) => Promise<void>;
|
||||
onToggleFolder: (_: string) => void;
|
||||
@@ -147,12 +147,11 @@ const FileTreeItem = memo<{
|
||||
item,
|
||||
level = 0,
|
||||
expandedFolders,
|
||||
loadedFolders,
|
||||
loadingFolders,
|
||||
onToggleFolder,
|
||||
onLoadFolder,
|
||||
selectedKey,
|
||||
updateKey,
|
||||
|
||||
folderChildrenCache,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -215,7 +214,7 @@ const FileTreeItem = memo<{
|
||||
setRenamingValue(item.name);
|
||||
}, [item.name]);
|
||||
|
||||
const { menuItems, moveModal } = useFileItemDropdown({
|
||||
const { menuItems } = useFileItemDropdown({
|
||||
fileType: item.fileType,
|
||||
filename: item.name,
|
||||
id: item.id,
|
||||
@@ -225,9 +224,6 @@ const FileTreeItem = memo<{
|
||||
url: item.url,
|
||||
});
|
||||
|
||||
// Dynamically look up children from cache instead of using static item.children
|
||||
const children = folderChildrenCache.get(itemKey);
|
||||
|
||||
const isDragActive = useDragActive();
|
||||
const { setCurrentDrag } = useDragState();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -426,34 +422,6 @@ const FileTreeItem = memo<{
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Dropdown>
|
||||
{moveModal}
|
||||
|
||||
{isExpanded && children && children.length > 0 && (
|
||||
<motion.div
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
initial={false}
|
||||
style={{ overflow: 'hidden' }}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
>
|
||||
<Flexbox gap={2}>
|
||||
{children.map((child) => (
|
||||
<FileTreeItem
|
||||
expandedFolders={expandedFolders}
|
||||
folderChildrenCache={folderChildrenCache}
|
||||
item={child}
|
||||
key={child.id}
|
||||
level={level + 1}
|
||||
loadedFolders={loadedFolders}
|
||||
loadingFolders={loadingFolders}
|
||||
onLoadFolder={onLoadFolder}
|
||||
onToggleFolder={onToggleFolder}
|
||||
selectedKey={selectedKey}
|
||||
updateKey={updateKey}
|
||||
/>
|
||||
))}
|
||||
</Flexbox>
|
||||
</motion.div>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
@@ -508,13 +476,12 @@ const FileTreeItem = memo<{
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Dropdown>
|
||||
{moveModal}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FileTreeItem.displayName = 'FileTreeItem';
|
||||
FileTreeRow.displayName = 'FileTreeRow';
|
||||
|
||||
/**
|
||||
* As a sidebar along with the Explorer to work
|
||||
@@ -536,7 +503,7 @@ const FileTree = memo(() => {
|
||||
|
||||
// Get the persisted state for this knowledge base
|
||||
const state = React.useMemo(() => getTreeState(libraryId || ''), [libraryId]);
|
||||
const { expandedFolders, loadedFolders, folderChildrenCache, loadingFolders } = state;
|
||||
const { expandedFolders, folderChildrenCache, loadingFolders } = state;
|
||||
|
||||
// Fetch breadcrumb for current folder
|
||||
const { data: folderBreadcrumb } = useFetchFolderBreadcrumb(currentFolderSlug);
|
||||
@@ -582,6 +549,37 @@ const FileTree = memo(() => {
|
||||
return sortItems(mappedItems);
|
||||
}, [rootData, sortItems, updateKey]);
|
||||
|
||||
const visibleNodes = React.useMemo(() => {
|
||||
interface VisibleNode {
|
||||
item: TreeItem;
|
||||
key: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const result: VisibleNode[] = [];
|
||||
|
||||
const walk = (nodes: TreeItem[], level: number) => {
|
||||
for (const node of nodes) {
|
||||
const key = node.slug || node.id;
|
||||
|
||||
result.push({ item: node, key, level });
|
||||
|
||||
if (!node.isFolder) continue;
|
||||
if (!expandedFolders.has(key)) continue;
|
||||
|
||||
const children = folderChildrenCache.get(key);
|
||||
if (!children || children.length === 0) continue;
|
||||
|
||||
walk(children, level + 1);
|
||||
}
|
||||
};
|
||||
|
||||
walk(items, 0);
|
||||
|
||||
return result;
|
||||
// NOTE: expandedFolders / folderChildrenCache are mutated in-place, so rely on updateKey for recompute
|
||||
}, [items, expandedFolders, folderChildrenCache, updateKey]);
|
||||
|
||||
const handleLoadFolder = useCallback(
|
||||
async (folderId: string) => {
|
||||
// Set loading state
|
||||
@@ -751,21 +749,27 @@ const FileTree = memo(() => {
|
||||
: currentFolderSlug;
|
||||
|
||||
return (
|
||||
<Flexbox gap={2} paddingInline={4}>
|
||||
{items.map((item) => (
|
||||
<FileTreeItem
|
||||
expandedFolders={expandedFolders}
|
||||
folderChildrenCache={folderChildrenCache}
|
||||
item={item}
|
||||
key={item.id}
|
||||
loadedFolders={loadedFolders}
|
||||
loadingFolders={loadingFolders}
|
||||
onLoadFolder={handleLoadFolder}
|
||||
onToggleFolder={handleToggleFolder}
|
||||
selectedKey={selectedKey}
|
||||
updateKey={updateKey}
|
||||
/>
|
||||
))}
|
||||
<Flexbox paddingInline={4} style={{ height: '100%' }}>
|
||||
<VList
|
||||
bufferSize={typeof window !== 'undefined' ? window.innerHeight : 0}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{visibleNodes.map(({ item, key, level }) => (
|
||||
<div key={key} style={{ paddingBottom: 2 }}>
|
||||
<FileTreeRow
|
||||
expandedFolders={expandedFolders}
|
||||
folderChildrenCache={folderChildrenCache}
|
||||
item={item}
|
||||
level={level}
|
||||
loadingFolders={loadingFolders}
|
||||
onLoadFolder={handleLoadFolder}
|
||||
onToggleFolder={handleToggleFolder}
|
||||
selectedKey={selectedKey}
|
||||
updateKey={updateKey}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</VList>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useCallback, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
export const useEventCallback = <T extends (...args: any[]) => any>(fn: T) => {
|
||||
const ref = useRef<T>(fn);
|
||||
useLayoutEffect(() => {
|
||||
ref.current = fn;
|
||||
}, [fn]);
|
||||
return useCallback((...args: Parameters<T>) => {
|
||||
return ref.current(...args);
|
||||
}, []);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { ModalHost } from '@lobehub/ui';
|
||||
import { LazyMotion, domMax } from 'motion/react';
|
||||
import { type ReactNode, Suspense } from 'react';
|
||||
|
||||
@@ -66,6 +67,7 @@ const GlobalLayout = async ({
|
||||
<DragUploadProvider>
|
||||
<LazyMotion features={domMax}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
<ModalHost />
|
||||
</LazyMotion>
|
||||
</DragUploadProvider>
|
||||
</GroupWizardProvider>
|
||||
|
||||
Reference in New Issue
Block a user