diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatContent.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatContent.tsx index 11c4f7982..f3e73bfbf 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatContent.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatContent.tsx @@ -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 { + 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 ( - -
- {typeOfItem} - {index} - {isPureChat ? 'pure chat' : isChat ? 'chat' : 'file'} -
- +
+ {typeOfItem} - {index} - {isPureChat ? 'pure chat' : isChat ? 'chat' : 'file'} +
); }); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx index 45ff892ba..7d51a8cd6 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx @@ -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: { diff --git a/web/src/app/app/_layouts/ChatLayout/ChatLayout.tsx b/web/src/app/app/_layouts/ChatLayout/ChatLayout.tsx index 23209c578..0b98128b7 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatLayout.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatLayout.tsx @@ -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 = React.memo( - ({ selectedFile, selectedLayout = 'chat' }) => { + ({ defaultSelectedFile, defaultSelectedLayout = 'chat', children, chatId }) => { const appSplitterRef = useRef(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 = 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 = React.memo( } - rightChildren={} + rightChildren={} autoSaveId="chat-splitter" defaultLayout={defaultSplitterLayout} preserveSide="left" - leftPanelMaxSize={hasFile ? 625 : undefined} leftPanelMinSize={hasFile ? 250 : undefined} - rightPanelMinSize={450} /> ); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx b/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx index 6b357a6c5..81255bb39 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatLayoutContext/ChatLayoutContext.tsx @@ -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; + 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 = { + 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 }; }; diff --git a/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainer.tsx b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainer.tsx index 26e52718a..46f03d46e 100644 --- a/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainer.tsx +++ b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainer.tsx @@ -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 = 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 = React.memo(({ children }) => { return ( -
- - {hasFile ? ( - -
- {selectedFileType} -
-
- ) : null} -
+
+ + {children}
); }); diff --git a/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/CollapseFileButton.tsx b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/CollapseFileButton.tsx new file mode 100644 index 000000000..985961df8 --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/CollapseFileButton.tsx @@ -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 ( + + {showCollapseButton && ( + + + + )} + + ); +}); + +CollapseFileButton.displayName = 'CollapseFileButton'; diff --git a/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx new file mode 100644 index 000000000..48570ea3a --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx @@ -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 ( +
+ +
+ ); +}); + +FileContainerHeader.displayName = 'FileContainerHeader'; + +const useStyles = createStyles(({ css, token }) => ({ + container: css` + min-height: ${appContentHeaderHeight}px; + height: ${appContentHeaderHeight}px; + border-bottom: 0.5px solid ${token.colorBorder}; + ` +})); diff --git a/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/index.ts b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/index.ts new file mode 100644 index 000000000..240b654b8 --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/FileContainer/FileContainerHeader/index.ts @@ -0,0 +1 @@ +export * from './FileContainerHeader'; diff --git a/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts b/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts index 5bec243fe..50c8095d4 100644 --- a/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts +++ b/web/src/app/app/_layouts/ChatLayout/hooks/useDefaultFile.ts @@ -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 }; }; diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 10a196fa3..4d5b605d4 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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', diff --git a/web/src/app/test/splitter/chat/[chatId]/dashboard/[dashboardId]/page.tsx b/web/src/app/test/splitter/chat/[chatId]/dashboard/[dashboardId]/page.tsx index 7d1e0b717..a01933221 100644 --- a/web/src/app/test/splitter/chat/[chatId]/dashboard/[dashboardId]/page.tsx +++ b/web/src/app/test/splitter/chat/[chatId]/dashboard/[dashboardId]/page.tsx @@ -1,3 +1,3 @@ export default function Page() { - return <>; + return
Dashboard swag
; } diff --git a/web/src/app/test/splitter/dashboard/[dashboardId]/page.tsx b/web/src/app/test/splitter/dashboard/[dashboardId]/page.tsx index 7d1e0b717..f99df442a 100644 --- a/web/src/app/test/splitter/dashboard/[dashboardId]/page.tsx +++ b/web/src/app/test/splitter/dashboard/[dashboardId]/page.tsx @@ -1,3 +1,3 @@ export default function Page() { - return <>; + return
Dashboard swag
; } diff --git a/web/src/app/test/splitter/layout.tsx b/web/src/app/test/splitter/layout.tsx index b459d9291..6f1daa448 100644 --- a/web/src/app/test/splitter/layout.tsx +++ b/web/src/app/test/splitter/layout.tsx @@ -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 (
- + + {children} +
); }