more chat container info

This commit is contained in:
Nate Kelley 2025-01-24 16:32:28 -07:00
parent 2068cf2046
commit de0d46bb2e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
38 changed files with 290 additions and 142 deletions

View File

@ -1,35 +0,0 @@
import React from 'react';
import { useChatSplitterContextSelector } from '../../ChatSplitterContext';
const colors = [
'red-200',
'blue-200',
'green-200',
'yellow-200',
'purple-200',
'pink-200',
'indigo-200',
'gray-200',
'orange-200',
'teal-200',
'cyan-200',
'lime-200'
];
export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElement> }> = ({
chatContentRef
}) => {
const hasFile = useChatSplitterContextSelector((state) => state.hasFile);
return (
<div ref={chatContentRef} className="h-full w-full overflow-y-auto">
<div className="mx-auto max-w-[600px]">
{Array.from({ length: 350 }).map((_, index) => (
<div key={index} className={`h-10 bg-${colors[index % 12]}`}>
{index}
</div>
))}
</div>
</div>
);
};

View File

@ -1,54 +0,0 @@
import React, { useMemo, useRef } from 'react';
import { AppSplitter, AppSplitterRef } from '@/components/layout/AppSplitter';
import { ChatContainer } from './ChatContainer';
import { FileContainer } from './FileContainer';
import { ChatSplitterContextProvider } from './ChatSplitterContext';
import { useChatSplitter } from './ChatSplitterContext';
import { SelectedFile } from './interfaces';
export interface ChatSplitterProps {
chatHeaderOptions?: [];
chatContent?: React.ReactNode;
showChatCollapse?: boolean;
defaultShowLayout?: 'chat' | 'file' | 'both';
defaultSelectedFile?: SelectedFile;
}
export const ChatSplitter: React.FC<ChatSplitterProps> = React.memo(
({ defaultSelectedFile, defaultShowLayout = 'chat', chatHeaderOptions, chatContent }) => {
const appSplitterRef = useRef<AppSplitterRef>(null);
const defaultLayout = useMemo(() => {
if (defaultShowLayout === 'chat') {
return ['100%', '0%'];
}
if (defaultShowLayout === 'file') {
return ['0%', '100%'];
}
return ['325px', 'auto'];
}, [defaultShowLayout]);
const useChatSplitterProps = useChatSplitter({
defaultSelectedFile,
appSplitterRef
});
const { selectedFile, hasFile } = useChatSplitterProps;
return (
<ChatSplitterContextProvider useChatSplitterProps={useChatSplitterProps}>
<AppSplitter
ref={appSplitterRef}
leftChildren={<ChatContainer selectedFile={selectedFile} chatContent={chatContent} />}
rightChildren={<FileContainer selectedFile={selectedFile} />}
autoSaveId="chat-splitter"
defaultLayout={defaultLayout}
preserveSide="left"
rightHidden={!hasFile}
leftPanelMaxSize={hasFile ? 600 : undefined}
/>
</ChatSplitterContextProvider>
);
}
);
ChatSplitter.displayName = 'ChatSplitter';

View File

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

View File

@ -1,2 +0,0 @@
export * from './ChatSplitter';
export * from './ChatSplitterContext';

View File

@ -1,5 +1,5 @@
import React, { useMemo, useRef } from 'react';
import type { ChatSplitterProps } from '../ChatSplitter';
import type { ChatSplitterProps } from '../ChatLayout';
import { ChatHeader } from './ChatHeader';
import { SelectedFile } from '../interfaces';
import { ChatContent } from './ChatContent';
@ -8,10 +8,11 @@ import { useScroll } from 'ahooks';
interface ChatContainerProps {
chatContent: ChatSplitterProps['chatContent'];
selectedFile: SelectedFile | undefined;
isPureFile: boolean;
}
export const ChatContainer: React.FC<ChatContainerProps> = React.memo(
({ chatContent, selectedFile }) => {
({ chatContent, selectedFile, isPureFile }) => {
const chatContentRef = useRef<HTMLDivElement>(null);
const scroll = useScroll(chatContentRef);
@ -21,6 +22,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = React.memo(
return scroll.top > trigger;
}, [chatContentRef, scroll?.top]);
if (isPureFile) return null;
return (
<div className="flex h-full w-full flex-col">
<ChatHeader selectedFile={selectedFile} showScrollOverflow={showScrollOverflow} />

View File

@ -0,0 +1,73 @@
import React, { useMemo } from 'react';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
const colors = [
'red-200',
'blue-200',
'green-200',
'yellow-200',
'purple-200',
'pink-200',
'indigo-200',
'gray-200',
'orange-200',
'teal-200',
'cyan-200',
'lime-200'
];
export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElement> }> = ({
chatContentRef
}) => {
const hasFile = useChatSplitterContextSelector((state) => state.hasFile);
return (
<div ref={chatContentRef} className="h-full w-full overflow-y-auto">
<div className="mx-auto max-w-[600px]">
{Array.from({ length: 350 }).map((_, index) => (
<ChatContentItem key={index} index={index} />
))}
</div>
</div>
);
};
const type = ['chat', 'dataset', 'collection', 'metric', 'dashboard'] as const;
const ChatContentItem = React.memo(({ index }: { index: number }) => {
const router = useRouter();
const typeOfItem = type[index % type.length];
const { isChat, isPureChat } = useMemo(
() => ({
isChat: index % 2 === 0,
isPureChat: index % 16 === 0
}),
[]
);
const link = useMemo(() => {
if (isPureChat) {
return `/test/splitter/chat/${index}`;
} else if (isChat && typeOfItem !== 'chat') {
return `/test/splitter/chat/${index}/${typeOfItem}/${index}`;
} else {
return `/test/splitter/${typeOfItem}/${index}`;
}
}, [index, typeOfItem, isPureChat, isChat]);
const onClick = () => {
router.push(link);
};
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>
);
});
ChatContentItem.displayName = 'ChatContentItem';

View File

@ -2,7 +2,7 @@ import { appContentHeaderHeight } from '@/components/layout/AppContentHeader';
import { createStyles } from 'antd-style';
import React from 'react';
import { SelectedFile } from '../../interfaces';
import { useChatSplitterContextSelector } from '../../ChatSplitterContext';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { ChatHeaderOptions } from './ChatHeaderOptions';
import { ChatHeaderTitle } from './ChatHeaderTitle';

View File

@ -1,6 +1,6 @@
import { Dropdown, MenuProps } from 'antd';
import React, { useMemo } from 'react';
import { useChatSplitterContextSelector } from '../../../ChatSplitterContext';
import { useChatSplitterContextSelector } from '../../../ChatLayoutContext';
import { HeaderOptionsRecord } from './config';
export const ChatContainerHeaderDropdown: React.FC<{

View File

@ -30,5 +30,12 @@ export const HeaderOptionsRecord: Record<AppChatMessageFileType, () => MenuProps
key: 'delete',
icon: <AppMaterialIcons icon="delete" />
}
],
chat: () => [
{
label: 'Delete',
key: 'delete',
icon: <AppMaterialIcons icon="delete" />
}
]
};

View File

@ -1,6 +1,6 @@
import { Text } from '@/components/text';
import React from 'react';
import { useChatSplitterContextSelector } from '../../ChatSplitterContext';
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
import { AnimatePresence, motion } from 'framer-motion';
const animation = {

View File

@ -0,0 +1,76 @@
'use client';
import React, { useMemo, useRef, useState } from 'react';
import { AppSplitter, AppSplitterRef } from '@/components/layout/AppSplitter';
import { ChatContainer } from './ChatContainer';
import { FileContainer } from './FileContainer';
import { ChatSplitterContextProvider } from './ChatLayoutContext';
import { useChatLayout } from './ChatLayoutContext';
import { SelectedFile } from './interfaces';
import { useUpdateEffect, useUpdateLayoutEffect } from 'ahooks';
export interface ChatSplitterProps {
chatContent?: React.ReactNode;
showChatCollapse?: boolean;
defaultShowLayout?: 'chat' | 'file' | 'both';
defaultSelectedFile?: SelectedFile;
}
export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
({ defaultSelectedFile, defaultShowLayout = 'chat', chatContent }) => {
const appSplitterRef = useRef<AppSplitterRef>(null);
const [isPureFile, setIsPureFile] = useState(defaultShowLayout === 'file');
const defaultSplitterLayout = useMemo(() => {
if (defaultShowLayout === 'chat') return ['100%', '0%'];
if (defaultShowLayout === 'file') return ['0%', '100%'];
return ['325px', 'auto'];
}, [defaultShowLayout]);
const useChatSplitterProps = useChatLayout({ defaultSelectedFile });
const { onSetSelectedFile, selectedFile, hasFile } = useChatSplitterProps;
useUpdateEffect(() => {
if (defaultSelectedFile && appSplitterRef.current) {
if (defaultShowLayout === 'chat') {
appSplitterRef.current?.animateWidth('100%', 'left');
} else if (defaultShowLayout === 'file') {
appSplitterRef.current?.animateWidth('100%', 'right');
} else if (appSplitterRef.current.isRightClosed || appSplitterRef.current.isLeftClosed) {
appSplitterRef.current?.animateWidth('320px', 'left');
}
}
if (defaultSelectedFile) onSetSelectedFile(defaultSelectedFile);
}, [defaultSelectedFile, defaultShowLayout]);
useUpdateLayoutEffect(() => {
if (isPureFile === true) setIsPureFile(defaultShowLayout === 'file');
}, [defaultShowLayout]);
return (
<ChatSplitterContextProvider useChatSplitterProps={useChatSplitterProps}>
<AppSplitter
ref={appSplitterRef}
leftChildren={
<ChatContainer
selectedFile={selectedFile}
chatContent={chatContent}
isPureFile={isPureFile}
/>
}
rightChildren={<FileContainer selectedFile={selectedFile} />}
autoSaveId="chat-splitter"
defaultLayout={defaultSplitterLayout}
preserveSide="left"
rightHidden={!hasFile}
leftPanelMaxSize={hasFile ? 625 : undefined}
leftPanelMinSize={hasFile ? 250 : undefined}
rightPanelMinSize={450}
/>
</ChatSplitterContextProvider>
);
}
);
ChatLayout.displayName = 'ChatSplitter';

View File

@ -5,15 +5,12 @@ import {
} from '@fluentui/react-context-selector';
import React, { PropsWithChildren, useMemo, useState } from 'react';
import { SelectedFile } from '../interfaces';
import { useUpdateEffect } from 'ahooks';
import type { AppSplitterRef } from '@/components/layout/AppSplitter';
interface UseChatSplitterProps {
defaultSelectedFile: SelectedFile | undefined;
appSplitterRef: React.RefObject<AppSplitterRef>;
}
export const useChatSplitter = ({ appSplitterRef, defaultSelectedFile }: UseChatSplitterProps) => {
export const useChatLayout = ({ defaultSelectedFile }: UseChatSplitterProps) => {
const [selectedFile, setSelectedFile] =
useState<UseChatSplitterProps['defaultSelectedFile']>(defaultSelectedFile);
@ -30,14 +27,6 @@ export const useChatSplitter = ({ appSplitterRef, defaultSelectedFile }: UseChat
setSelectedFile(file);
};
useUpdateEffect(() => {
if (appSplitterRef.current && appSplitterRef.current.isRightClosed) {
appSplitterRef.current?.animateWidth('320px', 'left');
}
setSelectedFile(defaultSelectedFile);
}, [defaultSelectedFile]);
return {
selectedFileTitle,
selectedFileType,
@ -47,8 +36,8 @@ export const useChatSplitter = ({ appSplitterRef, defaultSelectedFile }: UseChat
};
};
const ChatSplitterContext = createContext<ReturnType<typeof useChatSplitter>>(
{} as ReturnType<typeof useChatSplitter>
const ChatSplitterContext = createContext<ReturnType<typeof useChatLayout>>(
{} as ReturnType<typeof useChatLayout>
);
interface ChatSplitterContextProviderProps {}
@ -56,7 +45,7 @@ interface ChatSplitterContextProviderProps {}
export const ChatSplitterContextProvider: React.FC<
PropsWithChildren<
ChatSplitterContextProviderProps & {
useChatSplitterProps: ReturnType<typeof useChatSplitter>;
useChatSplitterProps: ReturnType<typeof useChatLayout>;
}
>
> = React.memo(({ children, useChatSplitterProps }) => {
@ -70,5 +59,5 @@ export const ChatSplitterContextProvider: React.FC<
ChatSplitterContextProvider.displayName = 'ChatSplitterContextProvider';
export const useChatSplitterContextSelector = <T,>(
selector: ContextSelector<ReturnType<typeof useChatSplitter>, T>
selector: ContextSelector<ReturnType<typeof useChatLayout>, T>
) => useContextSelector(ChatSplitterContext, selector);

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react';
import type { ChatSplitterProps } from '../ChatSplitter';
import type { ChatSplitterProps } from '../ChatLayout';
import { SelectedFile } from '../interfaces';
interface FileContainerProps {

View File

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

View File

@ -0,0 +1,40 @@
'use client';
import { useMemo } from 'react';
import { SelectedFile } from '../interfaces';
import { useParams } from 'next/navigation';
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
import { ChatSplitterProps } from '../ChatLayout';
export const useDefaultFile = () => {
const { metricId, collectionId, datasetId, dashboardId, chatId } = useParams() as {
metricId?: string;
collectionId?: string;
datasetId?: string;
dashboardId?: string;
chatId?: string;
};
const defaultFile: SelectedFile | undefined = useMemo(() => {
if (metricId) return { id: metricId, type: AppChatMessageFileType.Metric };
if (collectionId) return { id: collectionId, type: AppChatMessageFileType.Collection };
if (datasetId) return { id: datasetId, type: AppChatMessageFileType.Dataset };
if (dashboardId) return { id: dashboardId, type: AppChatMessageFileType.Dashboard };
if (chatId) return { id: chatId, type: AppChatMessageFileType.Chat };
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
const defaultLayout: ChatSplitterProps['defaultShowLayout'] = useMemo(() => {
const hasFileId = metricId || collectionId || datasetId || dashboardId;
if (chatId) {
if (hasFileId) return 'both';
return 'chat';
}
if (hasFileId) return 'file';
return 'chat';
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
return { defaultFile, defaultLayout };
};

View File

@ -0,0 +1,4 @@
export * from './ChatLayout';
export * from './ChatLayoutContext';
export * from './interfaces';
export * from './hooks';

View File

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

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -0,0 +1,37 @@
'use client';
import { ChatLayout, useDefaultFile } 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 { defaultFile, defaultLayout } = useDefaultFile();
const router = useRouter();
useHotkeys('m', () => {
const randomType: AppChatMessageFileType = (
['dataset', 'collection', 'metric', 'dashboard'] as AppChatMessageFileType[]
)[Math.floor(Math.random() * 4)];
const isPureChat = Math.random() < 0.15;
const isChat = Math.random() < 0.55;
const randomChatId = Math.floor(Math.random() * 1000);
const randomId = Math.floor(Math.random() * 1000000);
if (isPureChat) {
router.push(`/test/splitter/chat/${randomChatId}`);
return;
}
const route = isChat
? `/test/splitter/chat/${randomChatId}/${randomType}/${randomId}`
: `/test/splitter/${randomType}/${randomId}`;
router.push(route);
});
return (
<div className="h-screen w-screen">
<ChatLayout defaultShowLayout={defaultLayout} defaultSelectedFile={defaultFile} />
</div>
);
}

View File

@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@ -1,29 +1,5 @@
'use client';
import { ChatSplitter } from '@/app/app/_components/ChatSplitter';
import { SelectedFile } from '@/app/app/_components/ChatSplitter/interfaces';
import { useHotkeys } from 'react-hotkeys-hook';
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
import { useState } from 'react';
export default function Page() {
const [defaultFile, setDefaultFile] = useState<SelectedFile | undefined>({
id: '1',
type: 'dataset'
});
useHotkeys('m', () => {
const randomType: AppChatMessageFileType = (
['dataset', 'collection', 'metric', 'dashboard'] as AppChatMessageFileType[]
)[Math.floor(Math.random() * 4)];
const randomId = Math.floor(Math.random() * 1000000);
setDefaultFile({ id: randomId.toString(), type: randomType });
});
return (
<div className="h-screen w-screen">
<ChatSplitter defaultShowLayout="chat" defaultSelectedFile={defaultFile} />
</div>
);
return <></>;
}

View File

@ -30,7 +30,13 @@ export type AppChatMessageThought = {
}[];
};
export type AppChatMessageFileType = 'dataset' | 'collection' | 'metric' | 'dashboard';
export enum AppChatMessageFileType {
Dataset = 'dataset',
Collection = 'collection',
Metric = 'metric',
Dashboard = 'dashboard',
Chat = 'chat'
}
export type AppChatMessageFile = {
type: AppChatMessageFileType;

View File

@ -21,7 +21,8 @@
"paths": {
"@/*": ["./src/*"],
"@utils/*": ["./src/utils/*"],
"@appComponents/*": ["./src/app/app/_components/*"]
"@appComponents/*": ["./src/app/app/_components/*"],
"@chatLayout/*": ["./src/app/app/_layouts/ChatLayout/*"]
},
"target": "ES2017"
},