animation for collapse button

This commit is contained in:
Nate Kelley 2025-01-25 11:20:19 -07:00
parent d7aeb14569
commit 9f4dcb6baa
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
13 changed files with 176 additions and 67 deletions

View File

@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
const colors = [
'red-200',
@ -35,6 +36,7 @@ export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElem
const type = ['chat', 'dataset', 'collection', 'metric', 'dashboard'] as const;
const ChatContentItem = React.memo(({ index }: { index: number }) => {
const onSetSelectedFile = useChatSplitterContextSelector((state) => state.onSetSelectedFile);
const router = useRouter();
const typeOfItem = type[index % type.length];
const { isChat, isPureChat } = useMemo(
@ -56,15 +58,19 @@ const ChatContentItem = React.memo(({ index }: { index: number }) => {
}, [index, typeOfItem, isPureChat, isChat]);
const onClick = () => {
router.push(link);
if (isPureChat) {
router.push(link);
} else {
onSetSelectedFile({ id: index.toString(), type: typeOfItem as AppChatMessageFileType });
}
};
return (
<Link href={link}>
<div className={`h-10 cursor-pointer hover:bg-gray-100 bg-${colors[index % 12]}`}>
{typeOfItem} - {index} - {isPureChat ? 'pure chat' : isChat ? 'chat' : 'file'}
</div>
</Link>
<div
className={`h-10 cursor-pointer hover:bg-gray-100 bg-${colors[index % 12]}`}
onClick={onClick}>
{typeOfItem} - {index} - {isPureChat ? 'pure chat' : isChat ? 'chat' : 'file'}
</div>
);
});

View File

@ -1,7 +1,6 @@
import { appContentHeaderHeight } from '@/components/layout/AppContentHeader';
import { createStyles } from 'antd-style';
import React from 'react';
import { SelectedFile } from '../../interfaces';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { ChatHeaderOptions } from './ChatHeaderOptions';
import { ChatHeaderTitle } from './ChatHeaderTitle';
@ -34,6 +33,7 @@ ChatHeader.displayName = 'ChatContainerHeader';
const useStyles = createStyles(({ token }) => ({
header: {
height: appContentHeaderHeight,
minHeight: appContentHeaderHeight,
transition: 'box-shadow 0.2s ease-in-out'
},
scrollIndicator: {

View File

@ -11,23 +11,30 @@ import { useUpdateEffect, useUpdateLayoutEffect } from 'ahooks';
export interface ChatSplitterProps {
showChatCollapse?: boolean;
selectedLayout?: 'chat' | 'file' | 'both';
selectedFile?: SelectedFile;
defaultSelectedFile?: SelectedFile;
defaultSelectedLayout?: 'chat' | 'file' | 'both';
children?: React.ReactNode;
chatId: string | undefined;
}
export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
({ selectedFile, selectedLayout = 'chat' }) => {
({ defaultSelectedFile, defaultSelectedLayout = 'chat', children, chatId }) => {
const appSplitterRef = useRef<AppSplitterRef>(null);
const [isPureFile, setIsPureFile] = useState(selectedLayout === 'file');
const [isPureFile, setIsPureFile] = useState(defaultSelectedLayout === 'file');
const defaultSplitterLayout = useMemo(() => {
if (selectedLayout === 'chat') return ['100%', '0%'];
if (selectedLayout === 'file') return ['0%', '100%'];
if (defaultSelectedLayout === 'chat') return ['100%', '0%'];
if (defaultSelectedLayout === 'file') return ['0%', '100%'];
return ['325px', 'auto'];
}, [selectedLayout]);
}, [defaultSelectedLayout]);
const useChatSplitterProps = useChatLayout({ selectedFile });
const { onSetSelectedFile, hasFile } = useChatSplitterProps;
const useChatSplitterProps = useChatLayout({
appSplitterRef,
defaultSelectedFile,
defaultSelectedLayout,
chatId
});
const { onSetSelectedFile, selectedFileType, selectedLayout, hasFile } = useChatSplitterProps;
useUpdateEffect(() => {
if (appSplitterRef.current) {
@ -40,9 +47,7 @@ export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
animateWidth('320px', 'left');
}
}
if (selectedFile) onSetSelectedFile(selectedFile);
}, [selectedFile, selectedLayout]);
}, [selectedLayout]);
useUpdateLayoutEffect(() => {
if (isPureFile === true) setIsPureFile(selectedLayout === 'file');
@ -53,13 +58,11 @@ export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
<AppSplitter
ref={appSplitterRef}
leftChildren={isPureFile ? null : <ChatContainer />}
rightChildren={<FileContainer />}
rightChildren={<FileContainer children={children} />}
autoSaveId="chat-splitter"
defaultLayout={defaultSplitterLayout}
preserveSide="left"
leftPanelMaxSize={hasFile ? 625 : undefined}
leftPanelMinSize={hasFile ? 250 : undefined}
rightPanelMinSize={450}
/>
</ChatSplitterContextProvider>
);

View File

@ -3,33 +3,80 @@ import {
createContext,
useContextSelector
} from '@fluentui/react-context-selector';
import React, { PropsWithChildren, useMemo, useState } from 'react';
import { SelectedFile } from '../interfaces';
import React, { PropsWithChildren, useLayoutEffect, useMemo, useState } from 'react';
import type { SelectedFile } from '../interfaces';
import type { ChatSplitterProps } from '../ChatLayout';
import { useMemoizedFn } from 'ahooks';
import { useRouter } from 'next/navigation';
import type { AppSplitterRef } from '@/components/layout';
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
interface UseChatSplitterProps {
selectedFile: SelectedFile | undefined;
defaultSelectedFile: SelectedFile | undefined;
defaultSelectedLayout: ChatSplitterProps['defaultSelectedLayout'];
appSplitterRef: React.RefObject<AppSplitterRef>;
chatId: string | undefined;
}
export const useChatLayout = ({ selectedFile: selectedFileProp }: UseChatSplitterProps) => {
const selectedFileId = selectedFileProp?.id;
const selectedFileType = selectedFileProp?.type;
export const useChatLayout = ({
defaultSelectedFile,
defaultSelectedLayout,
appSplitterRef,
chatId
}: UseChatSplitterProps) => {
const router = useRouter();
const selectedFileId = defaultSelectedFile?.id;
const selectedFileType = defaultSelectedFile?.type;
const hasFile = !!selectedFileId;
const [selectedLayout, setSelectedLayout] = useState(defaultSelectedLayout);
const [selectedFile, setSelectedFile] = useState(defaultSelectedFile);
const selectedFileTitle: string = useMemo(() => {
if (!selectedFileId) return '';
return 'test';
}, [selectedFileId]);
const onSetSelectedFile = (file: SelectedFile) => {
// setSelectedFileId(file.id);
};
const onSetSelectedFile = useMemoizedFn((file: SelectedFile) => {
const isChatView = defaultSelectedLayout === 'chat' || defaultSelectedLayout === 'both';
const fileType = file.type;
const fileId = file.id;
if (isChatView && chatId) {
const routeRecord: Record<AppChatMessageFileType, string> = {
collection: `/test/splitter/chat/${chatId}/collection/${file.id}`,
dataset: `/test/splitter/chat/${chatId}/dataset/${file.id}`,
metric: `/test/splitter/chat/${chatId}/metric/${file.id}`,
dashboard: `/test/splitter/chat/${chatId}/dashboard/${file.id}`
};
if (routeRecord[fileType]) router.push(routeRecord[fileType]);
} else {
router.push(`/test/splitter/${fileType}/${fileId}`);
}
});
const onCollapseFileClick = useMemoizedFn((close?: boolean) => {
const isCloseAction = close ?? selectedLayout === 'both';
if (isCloseAction) {
setSelectedLayout('chat');
} else {
setSelectedLayout('both');
appSplitterRef.current?.animateWidth('320px', 'left');
}
});
useLayoutEffect(() => {
if (defaultSelectedLayout) setSelectedLayout(defaultSelectedLayout);
}, [defaultSelectedLayout]);
return {
selectedFileTitle,
selectedFileType,
selectedLayout,
selectedFileId,
hasFile,
onSetSelectedFile
onSetSelectedFile,
onCollapseFileClick
};
};

View File

@ -1,34 +1,15 @@
import React from 'react';
import { useChatSplitterContextSelector } from '../ChatLayoutContext';
import { AnimatePresence, motion } from 'framer-motion';
import { FileContainerHeader } from './FileContainerHeader';
interface FileContainerProps {}
const animation = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: 0.15 }
};
export const FileContainer: React.FC<FileContainerProps> = React.memo(({}) => {
const selectedFileId = useChatSplitterContextSelector((state) => state.selectedFileId);
const selectedFileType = useChatSplitterContextSelector((state) => state.selectedFileType);
const hasFile = !!selectedFileId;
interface FileContainerProps {
children: React.ReactNode;
}
export const FileContainer: React.FC<FileContainerProps> = React.memo(({ children }) => {
return (
<div className="h-full w-full bg-blue-300">
<AnimatePresence mode="wait" initial={false}>
{hasFile ? (
<motion.div key={'file-container'} {...animation} className={`h-full w-full`}>
<div
className={`h-[300px] w-[200px] ${selectedFileType === 'metric' ? 'bg-green-500' : 'bg-red-500'}`}>
{selectedFileType}
</div>
</motion.div>
) : null}
</AnimatePresence>
<div className="flex min-w-[450px] flex-col">
<FileContainerHeader />
{children}
</div>
);
});

View File

@ -0,0 +1,36 @@
import { AppMaterialIcons } from '@/components';
import { Button } from 'antd';
import React from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { useMemoizedFn } from 'ahooks';
const animation = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 }
};
export const CollapseFileButton: React.FC<{
showCollapseButton: boolean;
isOpen: boolean;
}> = React.memo(({ showCollapseButton, isOpen }) => {
const onCollapseFileClick = useChatSplitterContextSelector((state) => state.onCollapseFileClick);
const icon = !isOpen ? 'keyboard_double_arrow_left' : 'keyboard_double_arrow_right';
const onClick = useMemoizedFn(() => {
onCollapseFileClick();
});
return (
<AnimatePresence mode="wait">
{showCollapseButton && (
<motion.div variants={animation}>
<Button onClick={onClick} type="text" icon={<AppMaterialIcons icon={icon} />}></Button>
</motion.div>
)}
</AnimatePresence>
);
});
CollapseFileButton.displayName = 'CollapseFileButton';

View File

@ -0,0 +1,30 @@
import { appContentHeaderHeight } from '@/components/layout';
import { createStyles } from 'antd-style';
import React from 'react';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { CollapseFileButton } from './CollapseFileButton';
export const FileContainerHeader: React.FC = React.memo(() => {
const { styles, cx } = useStyles();
const selectedFileType = useChatSplitterContextSelector((state) => state.selectedFileType);
const selectedLayout = useChatSplitterContextSelector((state) => state.selectedLayout);
const showCollapseButton = true;
const isCollapseOpen = selectedLayout === 'both';
return (
<div className={cx(styles.container, 'flex w-full items-center px-3')}>
<CollapseFileButton showCollapseButton={showCollapseButton} isOpen={isCollapseOpen} />
</div>
);
});
FileContainerHeader.displayName = 'FileContainerHeader';
const useStyles = createStyles(({ css, token }) => ({
container: css`
min-height: ${appContentHeaderHeight}px;
height: ${appContentHeaderHeight}px;
border-bottom: 0.5px solid ${token.colorBorder};
`
}));

View File

@ -0,0 +1 @@
export * from './FileContainerHeader';

View File

@ -22,7 +22,7 @@ export const useSelectedFileByParams = () => {
if (dashboardId) return { id: dashboardId, type: AppChatMessageFileType.Dashboard };
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
const selectedLayout: ChatSplitterProps['selectedLayout'] = useMemo(() => {
const selectedLayout: ChatSplitterProps['defaultSelectedLayout'] = useMemo(() => {
const hasFileId = metricId || collectionId || datasetId || dashboardId;
if (chatId) {
@ -35,5 +35,5 @@ export const useSelectedFileByParams = () => {
return 'chat';
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
return { selectedFile, selectedLayout };
return { selectedFile, selectedLayout, chatId };
};

View File

@ -3,7 +3,6 @@ import React from 'react';
import '../styles/styles.scss';
import { BusterStyleProvider } from '@/context/BusterStyles/BusterStyles';
import { GlobalErrorComponent } from '../components/error/GlobalErrorComponent';
export const metadata: Metadata = {
title: 'Buster',

View File

@ -1,3 +1,3 @@
export default function Page() {
return <></>;
return <div className="h-full w-full bg-orange-500">Dashboard swag</div>;
}

View File

@ -1,3 +1,3 @@
export default function Page() {
return <></>;
return <div className="h-full w-full bg-red-500">Dashboard swag</div>;
}

View File

@ -1,12 +1,13 @@
'use client';
import React from 'react';
import { ChatLayout, useSelectedFileByParams } from '@chatLayout/index';
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
import { useRouter } from 'next/navigation';
import { useHotkeys } from 'react-hotkeys-hook';
export default function Layout({}: {}) {
const { selectedFile, selectedLayout } = useSelectedFileByParams();
export default function Layout({ children }: { children: React.ReactNode }) {
const { selectedFile, selectedLayout, chatId } = useSelectedFileByParams();
const router = useRouter();
useHotkeys('m', () => {
@ -31,7 +32,12 @@ export default function Layout({}: {}) {
return (
<div className="h-screen w-screen">
<ChatLayout selectedLayout={selectedLayout} selectedFile={selectedFile} />
<ChatLayout
chatId={chatId}
defaultSelectedLayout={selectedLayout}
defaultSelectedFile={selectedFile}>
{children}
</ChatLayout>
</div>
);
}