remove old chat modal

This commit is contained in:
Nate Kelley 2025-03-03 14:30:56 -07:00
parent ac932fefee
commit 2efe1b72d9
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
7 changed files with 4 additions and 528 deletions

View File

@ -1,284 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Button, Modal, Input, InputRef, ConfigProvider, ThemeConfig } from 'antd';
import { AppMaterialIcons } from '@/components/ui';
import { useMemoizedFn, useMount, useThrottleFn } from 'ahooks';
import { useAntToken } from '@/styles/useAntToken';
import { useBusterNewChatContextSelector } from '@/context/Chats';
import { inputHasText, timeout } from '@/lib';
import { useBusterSearchContextSelector } from '@/context/Search';
import type { BusterSearchResult } from '@/api/asset_interfaces';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { NewChatModalDataSourceSelect } from './NewChatModalDatasourceSelect';
import { NoDatasets } from './NoDatasets';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useGetDatasets } from '@/api/buster_rest/datasets';
import { NewDatasetModal } from '../NewDatasetModal';
const { TextArea } = Input;
const themeConfig: ThemeConfig = {
components: {
Modal: {
paddingMD: 4,
paddingContentHorizontalLG: 4
}
}
};
const modalClassNames = {
body: 'p-0!'
};
export const NewChatModal = React.memo<{
open: boolean;
onClose: () => void;
}>(({ open, onClose }) => {
const token = useAntToken();
const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage);
const { openErrorNotification } = useBusterNotifications();
const { isFetched: isFetchedDatasets, data: datasetsList } = useGetDatasets();
const onBusterSearch = useBusterSearchContextSelector((x) => x.onBusterSearch);
const [selectedChatDataSource, setSelectedChatDataSource] = useState<{
id: string;
name: string;
} | null>(null);
const [openNewDatasetModal, setOpenNewDatasetModal] = useState(false);
const [prompt, setPrompt] = useState('');
const [suggestedPrompts, setSuggestedPrompts] = useState<BusterSearchResult[]>([]);
const [activeItem, setActiveItem] = useState<number | null>(null);
const [defaultSuggestedPrompts, setDefaultSuggestedPrompts] = useState<BusterSearchResult[]>([]);
const lastKeyPressed = useRef<string | null>(null);
const hasDatasets = datasetsList.length > 0 && isFetchedDatasets;
const shownPrompts = prompt.length > 1 ? suggestedPrompts : defaultSuggestedPrompts;
const memoizedHasDatasetStyle = useMemo(() => {
return {
padding: `${token.paddingSM}px ${token.paddingSM}px`,
paddingTop: token.paddingSM,
paddingBottom: 0
};
}, []);
const getSuggestedChatPrompts = useMemoizedFn(async (prompt: string) => {
const res = await onBusterSearch({
query: prompt
});
return res;
});
const { run: debouncedGetSuggestedChatPrompts } = useThrottleFn(
async (v: string) => {
try {
// const prompts = await getSuggestedChatPrompts(v);
// setSuggestedPrompts(prompts);
// return prompts;
return [];
} catch (e) {
openErrorNotification(e);
}
},
{ wait: 350 }
);
const getDefaultSuggestedPrompts = useMemoizedFn(() => {
getSuggestedChatPrompts('').then((prompts) => {
setDefaultSuggestedPrompts(prompts);
});
});
useEffect(() => {
if (open) {
if (defaultSuggestedPrompts.length === 0) {
getDefaultSuggestedPrompts();
}
const handleKeyPress = (event: KeyboardEvent) => {
lastKeyPressed.current = event.code;
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}
}, [open]);
return (
<ConfigProvider theme={themeConfig}>
<Modal
open={open}
onCancel={onClose}
closable={false}
onClose={onClose}
width={hasDatasets ? 725 : 350}
destroyOnClose={true}
footer={null}
classNames={modalClassNames}>
{hasDatasets && (
<div className="flex w-full flex-col" style={memoizedHasDatasetStyle}>
<NewChatModalDataSourceSelect
onSetSelectedChatDataSource={setSelectedChatDataSource}
selectedChatDataSource={selectedChatDataSource}
dataSources={datasetsList}
loading={!isFetchedDatasets}
/>
<NewChatInput
key={open ? 'open' : 'closed'}
setSuggestedPrompts={setSuggestedPrompts}
debouncedGetSuggestedChatPrompts={debouncedGetSuggestedChatPrompts}
shownPrompts={shownPrompts}
lastKeyPressed={lastKeyPressed}
activeItem={activeItem}
prompt={prompt}
selectedChatDataSource={selectedChatDataSource}
setPrompt={setPrompt}
/>
</div>
)}
{!hasDatasets && (
<NoDatasets onClose={onClose} setOpenNewDatasetModal={setOpenNewDatasetModal} />
)}
{/* {hasDatasets && showSuggested && <Divider className="m-0!" />} */}
{/* {hasDatasets && (
<SuggestedPromptsContainer
open={open}
activeItem={activeItem}
setActiveItem={setActiveItem}
prompts={shownPrompts}
onSelectPrompt={onSelectPrompt}
navigatingToMetricId={navigatingToMetricId}
/>
)} */}
</Modal>
{!hasDatasets && (
<NewDatasetModal
open={openNewDatasetModal}
onClose={() => setOpenNewDatasetModal(false)}
afterCreate={onClose}
/>
)}
</ConfigProvider>
);
});
NewChatModal.displayName = 'NewChatModal';
const NewChatInput: React.FC<{
setSuggestedPrompts: (prompts: BusterSearchResult[]) => void;
debouncedGetSuggestedChatPrompts: (prompt: string) => Promise<BusterSearchResult[] | undefined>;
shownPrompts: BusterSearchResult[];
lastKeyPressed: React.MutableRefObject<string | null>;
activeItem: number | null;
prompt: string;
setPrompt: (prompt: string) => void;
selectedChatDataSource: {
id: string;
name: string;
} | null;
}> = React.memo(
({
setSuggestedPrompts,
debouncedGetSuggestedChatPrompts,
activeItem,
shownPrompts,
lastKeyPressed,
prompt,
setPrompt,
selectedChatDataSource
}) => {
const token = useAntToken();
const inputRef = useRef<InputRef>(null);
const onStartNewChat = useBusterNewChatContextSelector((x) => x.onStartNewChat);
const onSelectSearchAsset = useBusterNewChatContextSelector((x) => x.onSelectSearchAsset);
const [loadingNewChat, setLoadingNewChat] = useState(false);
const onStartNewChatPreflight = useMemoizedFn(async () => {
setLoadingNewChat(true);
await onStartNewChat({ prompt, datasetId: selectedChatDataSource?.id });
await timeout(250);
setPrompt('');
setLoadingNewChat(false);
});
const onChangeText = useMemoizedFn((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.currentTarget.value;
setPrompt(value);
if (value.length < 1) {
setSuggestedPrompts([]);
} else {
debouncedGetSuggestedChatPrompts(e.currentTarget.value);
}
});
const onPressEnter = useMemoizedFn((v: React.KeyboardEvent<HTMLTextAreaElement>) => {
const value = v.currentTarget.value;
const lastKeyPressedWasUpOrDown =
lastKeyPressed.current === 'ArrowUp' || lastKeyPressed.current === 'ArrowDown';
if (
typeof activeItem === 'number' &&
shownPrompts[activeItem]?.name &&
lastKeyPressedWasUpOrDown
) {
const foundAsset = shownPrompts[activeItem];
if (foundAsset) {
onSelectSearchAsset(foundAsset);
}
v.stopPropagation();
v.preventDefault();
return;
}
if (v.shiftKey) {
return;
}
onStartNewChatPreflight();
});
const onClickSubmitButton = useMemoizedFn(() => {
onStartNewChatPreflight();
});
useMount(() => {
setTimeout(() => {
inputRef.current?.focus();
}, 175);
});
const autoSizeMemoized = useMemo(() => {
return { minRows: 1, maxRows: 16 };
}, []);
return (
<div className="flex min-h-[54px] items-center justify-between space-x-1 overflow-y-auto px-2">
<TextArea
ref={inputRef}
size="large"
className="w-full pl-0!"
autoSize={autoSizeMemoized}
disabled={loadingNewChat}
variant="borderless"
placeholder="Search for a metric..."
defaultValue={prompt}
onChange={onChangeText}
onPressEnter={onPressEnter}
/>
<Button
type="primary"
size="middle"
// color="default"
// variant="solid"
icon={<AppMaterialIcons icon="arrow_forward" size={token.fontSizeLG} />}
loading={loadingNewChat}
disabled={!inputHasText(prompt)}
onClick={onClickSubmitButton}
/>
</div>
);
}
);
NewChatInput.displayName = 'NewChatInput';

View File

@ -1,85 +0,0 @@
import type { BusterDatasetListItem } from '@/api/asset_interfaces';
import { AppMaterialIcons } from '@/components/ui';
import { SelectProps, Select } from 'antd';
import isEmpty from 'lodash/isEmpty';
import React, { useMemo } from 'react';
import { Text } from '@/components/ui';
import { useMemoizedFn } from 'ahooks';
import { cn } from '@/lib/utils';
export const NewChatModalDataSourceSelect: React.FC<{
dataSources: BusterDatasetListItem[];
onSetSelectedChatDataSource: (dataSource: { id: string; name: string } | null) => void;
selectedChatDataSource: { id: string; name: string } | null;
loading: boolean;
}> = React.memo(({ dataSources, selectedChatDataSource, onSetSelectedChatDataSource, loading }) => {
const AutoSelectDataSource = useMemo(
() => ({
label: (
<div className="flex items-center space-x-1">
<AppMaterialIcons size={14} className={cn(`text-icon-color min-w-[14px]`)} icon="stars" />
<span>Auto-select</span>
</div>
),
value: 'auto',
name: 'Auto-select'
}),
[]
);
const options: SelectProps['options'] = useMemo(() => {
return [
AutoSelectDataSource,
...dataSources.map((dataSource) => ({
label: (
<div className="flex items-center space-x-1">
<AppMaterialIcons className={cn(`text-icon-color min-w-[14px]`)} icon="database" />
<Text>{dataSource.name}</Text>
</div>
),
icon: <AppMaterialIcons icon="database" />,
name: dataSource.name,
value: dataSource.id
}))
];
}, [dataSources]);
const selected = useMemo(
() =>
options.find((option) => option.value === selectedChatDataSource?.id) || AutoSelectDataSource,
[options, selectedChatDataSource]
);
const onSelectPreflight = useMemoizedFn((value: string) => {
const selectedDataSource = dataSources.find((dataSource) => dataSource.id === value);
onSetSelectedChatDataSource(selectedDataSource || null);
});
const onChange = useMemoizedFn((v: (typeof options)[0]) => {
onSelectPreflight(v.value as string);
});
const onFilter: SelectProps['filterOption'] = useMemoizedFn((v, option) => {
return option.name?.toLowerCase().includes(v?.toLowerCase());
});
return (
<div>
<Select
defaultActiveFirstOption
defaultValue={options[0]}
value={selected}
disabled={isEmpty(dataSources) || loading}
options={options}
allowClear={false}
loading={loading}
labelInValue={true}
popupMatchSelectWidth={false}
onChange={onChange}
showSearch={true}
filterOption={onFilter}
/>
</div>
);
});
NewChatModalDataSourceSelect.displayName = 'NewChatModalDataSourceSelect';

View File

@ -1,31 +0,0 @@
import React from 'react';
import { Button } from 'antd';
import { Title, Text } from '@/components/ui/typography';
import { AppMaterialIcons } from '@/components/ui';
export const NoDatasets: React.FC<{
onClose: () => void;
setOpenNewDatasetModal: (open: boolean) => void;
}> = React.memo(({ onClose, setOpenNewDatasetModal }) => {
return (
<>
<div className="flex flex-col items-center space-y-3 p-3">
<div className="mt-0 flex w-full flex-col justify-center space-y-3 rounded-sm p-4">
<Title as="h4">{`You don't have any datasets yet.`}</Title>
<Text>In order to get started, create a dataset.</Text>
<Button
onClick={() => {
setOpenNewDatasetModal(true);
onClose();
}}
type="default"
icon={<AppMaterialIcons icon="table_view" />}>
Create dataset
</Button>
</div>
</div>
</>
);
});
NoDatasets.displayName = 'NoDatasets';

View File

@ -1,124 +0,0 @@
import type { BusterSearchResult } from '@/api/asset_interfaces';
import { CircleSpinnerLoader } from '@/components/ui/loaders';
import { boldHighlights } from '@/lib/element';
import { createStyles } from 'antd-style';
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
export const SuggestedPromptsContainer: React.FC<{
prompts: BusterSearchResult[];
onSelectPrompt: (prompt: BusterSearchResult) => void;
open: boolean;
activeItem: number | null;
setActiveItem: React.Dispatch<React.SetStateAction<number | null>>;
navigatingToMetricId: string | null;
}> = React.memo(
({ navigatingToMetricId, activeItem, setActiveItem, prompts, open, onSelectPrompt }) => {
const activeItemId = activeItem !== null ? prompts[activeItem]?.id : null;
useEffect(() => {
setActiveItem(null);
if (open) {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.code === 'Enter' && activeItem !== null) {
event.preventDefault();
event.stopPropagation();
onSelectPrompt(prompts[activeItem]);
}
if (event.code === 'ArrowDown') {
setActiveItem((prev) => {
if (prev === null) {
return 0;
}
return Math.min(prompts.length - 1, prev + 1);
});
} else if (event.code === 'ArrowUp') {
setActiveItem((prev) => {
if (prev === null) {
return 0;
}
return Math.max(0, prev - 1);
});
}
};
const handleClick = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
setActiveItem(null);
};
document.addEventListener('keydown', handleKeyPress);
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('keydown', handleKeyPress);
document.removeEventListener('click', handleClick);
};
}
}, [open, prompts]);
return (
<div className="flex max-h-[250px] w-full flex-col overflow-y-auto p-1">
{prompts.map((prompt, index) => (
<PromptItem
key={prompt.id + prompt.updated_at + index}
{...prompt}
onSelectPrompt={onSelectPrompt}
activeItemId={activeItemId}
loading={navigatingToMetricId === prompt.id}
/>
))}
</div>
);
}
);
SuggestedPromptsContainer.displayName = 'SuggestedPromptsContainer';
const PromptItem: React.FC<
BusterSearchResult & {
onSelectPrompt: (prompt: BusterSearchResult) => void;
activeItemId: string | null;
loading: boolean;
}
> = ({ onSelectPrompt, activeItemId, loading, ...prompt }) => {
const { highlights } = prompt;
const { styles, cx } = useStyles();
const boldedHTML = boldHighlights(prompt.name, highlights);
return (
<div
className={cx(
styles.item,
`flex w-full cursor-pointer items-center space-x-1.5 px-4 transition`,
prompt.id === activeItemId && 'active'
)}
onClick={() => onSelectPrompt(prompt)}>
{loading && (
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 'auto', opacity: 1 }}
transition={{ duration: 0.2 }}>
<CircleSpinnerLoader size={13} />
</motion.div>
)}
<div className="flex-1">{boldedHTML}</div>
</div>
);
};
const useStyles = createStyles(({ token, css }) => ({
item: css`
&:hover,
&.active {
background-color: ${token.colorBgTextHover};
}
white-space: pre;
min-height: 40px;
max-height: 40px;
border-radius: ${token.borderRadius}px;
`
}));

View File

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

View File

@ -1,4 +1,3 @@
import { type MutableRefObject, useCallback } from 'react';
import type { IBusterChat, IBusterChatMessage } from '../interfaces';
import { useMemoizedFn } from 'ahooks';
import { useQueryClient } from '@tanstack/react-query';

View File

@ -1,5 +1,6 @@
'use client';
import React from 'react';
import { InputTextAreaButton } from '@/components/ui/inputs/InputTextAreaButton';
import { useBusterNewChatContextSelector } from '@/context/Chats';
import { inputHasText } from '@/lib/text';
@ -14,13 +15,14 @@ const autoResizeConfig = {
export const NewChatInput: React.FC<{}> = () => {
const onStartNewChat = useBusterNewChatContextSelector((state) => state.onStartNewChat);
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const disabledSubmit = useMemo(() => {
return !inputHasText(inputValue);
}, [inputValue]);
const onSubmit = useMemoizedFn((value: string) => {
onStartNewChat({ prompt: value });
const onSubmit = useMemoizedFn(async (value: string) => {
await onStartNewChat({ prompt: value });
});
const onChange = useMemoizedFn((e: ChangeEvent<HTMLTextAreaElement>) => {