mirror of https://github.com/buster-so/buster.git
remove old chat modal
This commit is contained in:
parent
ac932fefee
commit
2efe1b72d9
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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;
|
||||
`
|
||||
}));
|
|
@ -1 +0,0 @@
|
|||
export * from './NewChatModal';
|
|
@ -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';
|
||||
|
|
|
@ -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>) => {
|
||||
|
|
Loading…
Reference in New Issue