mirror of https://github.com/buster-so/buster.git
version id update
This commit is contained in:
parent
29b2c8996f
commit
330c3067d5
|
@ -39,19 +39,20 @@ export type BusterChatMessage_thought = {
|
|||
thought_secondary_title: string;
|
||||
thought_pills?: BusterChatMessage_thoughtPill[];
|
||||
hidden?: boolean; //if left undefined, will automatically be set to false if stream has ended
|
||||
in_progress?: boolean; //if left undefined, will automatically be set to true if the chat stream is in progress AND there is no message after it
|
||||
status?: 'loading' | 'completed' | 'failed'; //if left undefined, will automatically be set to 'loading' if the chat stream is in progress AND there is no message after it
|
||||
};
|
||||
|
||||
export type BusterChatMessage_fileMetadata = {
|
||||
status: 'loading' | 'completed' | 'failed';
|
||||
message: string;
|
||||
timestamp?: number;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export type BusterChatMessage_file = {
|
||||
id: string;
|
||||
type: 'file';
|
||||
file_type: FileType;
|
||||
file_name: string;
|
||||
version_number: number;
|
||||
version_id: string;
|
||||
metadata?: BusterChatMessage_fileMetadata[];
|
||||
|
|
|
@ -1,24 +1,64 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Input, Button } from 'antd';
|
||||
import { Text } from '@/components/text';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { AppMaterialIcons } from '@/components';
|
||||
import { inputHasText } from '@/utils';
|
||||
|
||||
export const ChatInput: React.FC = () => {
|
||||
const autoSize = { minRows: 4, maxRows: 4 };
|
||||
|
||||
export const ChatInput: React.FC = React.memo(() => {
|
||||
const { styles, cx } = useStyles();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const disableSendButton = useMemo(() => {
|
||||
return !inputHasText(inputValue);
|
||||
}, [inputValue]);
|
||||
|
||||
const handleInputChange = useMemoizedFn((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
});
|
||||
|
||||
const onSubmit = useMemoizedFn(async () => {
|
||||
if (disableSendButton) return;
|
||||
console.log('submit');
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.inputContainer,
|
||||
'relative z-10 flex flex-col items-center space-y-1 px-3 pb-2 pt-0.5'
|
||||
'relative z-10 flex flex-col items-center space-y-1.5 px-3 pb-2 pt-0.5'
|
||||
)}>
|
||||
<Input.TextArea />
|
||||
<div className="relative w-full">
|
||||
<Input.TextArea
|
||||
className="w-full"
|
||||
defaultValue={inputValue}
|
||||
placeholder="Edit or follow up"
|
||||
rows={4}
|
||||
autoSize={autoSize}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<Button
|
||||
shape="circle"
|
||||
disabled={disableSendButton}
|
||||
icon={<AppMaterialIcons icon="arrow_upward" />}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Text size="xs" type="tertiary">
|
||||
Our AI may make mistakes. Check important info.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ChatInput.displayName = 'ChatInput';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
inputContainer: css`
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
||||
|
||||
export const ChatResponseMessage_File: React.FC<ChatResponseMessageProps> = React.memo(
|
||||
({ responseMessage }) => {
|
||||
return <div>ChatResponseMessage_File</div>;
|
||||
}
|
||||
);
|
||||
|
||||
ChatResponseMessage_File.displayName = 'ChatResponseMessage_File';
|
|
@ -0,0 +1,173 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { ChatResponseMessageProps } from '../ChatResponseMessages';
|
||||
import { createStyles } from 'antd-style';
|
||||
import type {
|
||||
BusterChatMessage_file,
|
||||
BusterChatMessage_fileMetadata
|
||||
} from '@/api/buster_socket/chats';
|
||||
import { Text } from '@/components/text';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { animationConfig } from '../animationConfig';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { StatusIndicator } from '../StatusIndicator';
|
||||
import { formatDate } from '@/utils';
|
||||
|
||||
export const ChatResponseMessage_File: React.FC<ChatResponseMessageProps> = React.memo(
|
||||
({ responseMessage: responseMessageProp, isCompletedStream }) => {
|
||||
const { cx, styles } = useStyles();
|
||||
const responseMessage = responseMessageProp as BusterChatMessage_file;
|
||||
const { file_name, file_type, version_id, version_number, id, metadata = [] } = responseMessage;
|
||||
|
||||
const onClickCard = useMemoizedFn(() => {
|
||||
console.log('clicked');
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={!isCompletedStream}>
|
||||
<motion.div
|
||||
id={id}
|
||||
{...animationConfig}
|
||||
className={cx(styles.fileCard, 'file-card flex flex-col overflow-hidden')}>
|
||||
<VerticalDivider className="top-line mb-0.5" />
|
||||
<div
|
||||
onClick={onClickCard}
|
||||
className={cx(
|
||||
styles.fileContainer,
|
||||
'transition-shadow duration-300',
|
||||
'flex flex-col items-center',
|
||||
'cursor-pointer overflow-hidden'
|
||||
)}>
|
||||
<ChatResponseMessageHeader file_name={file_name} version_number={version_number} />
|
||||
<ChatResponseMessageBody metadata={metadata} />
|
||||
</div>
|
||||
<VerticalDivider className="bottom-line mt-0.5" />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChatResponseMessage_File.displayName = 'ChatResponseMessage_File';
|
||||
|
||||
const ChatResponseMessageHeader: React.FC<{ file_name: string; version_number: number }> = ({
|
||||
file_name,
|
||||
version_number
|
||||
}) => {
|
||||
const { cx, styles } = useStyles();
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.fileHeader,
|
||||
'file-header',
|
||||
'flex w-full items-center space-x-1.5 overflow-hidden px-2.5'
|
||||
)}>
|
||||
<Text className="truncate">{file_name}</Text>
|
||||
<div className={cx(styles.fileVersion, 'flex items-center space-x-1.5')}>
|
||||
<Text type="secondary" lineHeight={11} size="sm">
|
||||
v{version_number}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatResponseMessageBody: React.FC<{
|
||||
metadata: BusterChatMessage_fileMetadata[];
|
||||
}> = ({ metadata }) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center space-y-0.5 px-2.5 py-2">
|
||||
{metadata.map((metadata, index) => (
|
||||
<MetadataItem metadata={metadata} key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MetadataItem: React.FC<{ metadata: BusterChatMessage_fileMetadata }> = ({ metadata }) => {
|
||||
const { status, message, timestamp } = metadata;
|
||||
|
||||
const timestampFormatted = useMemo(() => {
|
||||
if (!timestamp) return '';
|
||||
return formatDate({
|
||||
date: timestamp,
|
||||
format: 'll'
|
||||
});
|
||||
}, [timestamp]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-start space-x-1.5 overflow-hidden">
|
||||
<div>
|
||||
<StatusIndicator status={status} />
|
||||
</div>
|
||||
|
||||
<Text className="truncate" size="xs" type="secondary">
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
{timestamp && (
|
||||
<Text type="tertiary" className="truncate" size="xs">
|
||||
{timestampFormatted}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VerticalDivider: React.FC<{ className?: string }> = ({ className }) => {
|
||||
const { cx, styles } = useStyles();
|
||||
return <div className={cx(styles.verticalDivider, 'vertical-divider', className)} />;
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
fileCard: css`
|
||||
& + .file-card {
|
||||
.vertical-divider.top-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.file-card {
|
||||
.thought-card + & {
|
||||
.vertical-divider.top-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.vertical-divider.bottom-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
fileContainer: css`
|
||||
border-radius: ${token.borderRadius}px;
|
||||
border: 0.5px solid ${token.colorBorder};
|
||||
|
||||
&:hover {
|
||||
border-color: ${token.colorTextTertiary};
|
||||
box-shadow: ${token.boxShadowTertiary};
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: black;
|
||||
box-shadow: ${token.boxShadowTertiary};
|
||||
}
|
||||
`,
|
||||
fileHeader: css`
|
||||
background: ${token.controlItemBgActive};
|
||||
border-bottom: 0.5px solid ${token.colorBorder};
|
||||
height: 32px;
|
||||
`,
|
||||
fileVersion: css`
|
||||
border-radius: ${token.borderRadius}px;
|
||||
padding: 4px;
|
||||
background: ${token.colorFillTertiary};
|
||||
`,
|
||||
verticalDivider: css`
|
||||
height: 9px;
|
||||
width: 0.5px;
|
||||
margin-left: 8px;
|
||||
background: ${token.colorTextTertiary};
|
||||
`
|
||||
}));
|
|
@ -0,0 +1 @@
|
|||
export * from './ChatResponseMessage_File';
|
|
@ -30,7 +30,7 @@ export const ChatResponseMessage_Text: React.FC<ChatResponseMessageProps> = Reac
|
|||
}, [responseMessage?.message_chunk, responseMessage?.message]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="text-card">
|
||||
{textChunks.map((chunk, index) => (
|
||||
<AnimatePresence key={index} initial={!isCompletedStream}>
|
||||
<motion.span {...animationConfig}>{chunk}</motion.span>
|
||||
|
|
|
@ -6,24 +6,28 @@ import { animationConfig } from '../animationConfig';
|
|||
import { Text } from '@/components/text';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { PillContainer } from './ChatResponseMessage_ThoughtPills';
|
||||
import { StatusIndicator } from './StatusIndicator';
|
||||
import { StatusIndicator } from '../StatusIndicator';
|
||||
import { VerticalBar } from './VerticalBar';
|
||||
|
||||
export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = React.memo(
|
||||
({ responseMessage: responseMessageProp, isCompletedStream, isLastMessageItem }) => {
|
||||
const responseMessage = responseMessageProp as BusterChatMessage_thought;
|
||||
const { thought_title, thought_secondary_title, thought_pills, in_progress } = responseMessage;
|
||||
const { thought_title, thought_secondary_title, thought_pills, status } = responseMessage;
|
||||
const { cx } = useStyles();
|
||||
const hasPills = thought_pills && thought_pills.length > 0;
|
||||
|
||||
const showLoadingIndicator = in_progress ?? (isLastMessageItem && !isCompletedStream);
|
||||
const showLoadingIndicator =
|
||||
(status ?? (isLastMessageItem && !isCompletedStream)) ? 'loading' : 'completed';
|
||||
const inProgress = showLoadingIndicator === 'loading';
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={!isCompletedStream}>
|
||||
<motion.div className={cx('relative flex space-x-1.5')} {...animationConfig}>
|
||||
<motion.div
|
||||
className={cx('relative flex space-x-1.5', 'thought-card')}
|
||||
{...animationConfig}>
|
||||
<div className="flex w-4 min-w-4 flex-col items-center pt-0.5">
|
||||
<StatusIndicator inProgress={showLoadingIndicator} />
|
||||
<VerticalBar inProgress={in_progress} hasPills={hasPills} />
|
||||
<StatusIndicator status={showLoadingIndicator} />
|
||||
<VerticalBar inProgress={inProgress} hasPills={hasPills} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col space-y-2">
|
||||
<div className="flex w-full items-center space-x-1.5 overflow-hidden">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {
|
||||
import type {
|
||||
BusterChatMessage_thought,
|
||||
BusterChatMessage_thoughtPill
|
||||
} from '@/api/buster_socket/chats';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { calculateTextWidth } from '@/utils';
|
||||
import { useDebounce, useMemoizedFn, useSize } from 'ahooks';
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import React from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader';
|
||||
import { AppMaterialIcons } from '@/components/icons';
|
||||
|
||||
const animationConfig = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.25 }
|
||||
};
|
||||
|
||||
export const StatusIndicator: React.FC<{ inProgress?: boolean }> = React.memo(({ inProgress }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.indicatorContainer,
|
||||
inProgress && 'in-progress',
|
||||
'flex items-center justify-center transition-all delay-100 duration-300'
|
||||
)}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={inProgress ? 'in-progress' : 'completed'}
|
||||
{...animationConfig}
|
||||
className={cx(
|
||||
styles.indicator,
|
||||
inProgress && 'in-progress',
|
||||
'flex items-center justify-center transition-all duration-300'
|
||||
)}>
|
||||
{inProgress ? (
|
||||
<CircleSpinnerLoader size={8} />
|
||||
) : (
|
||||
<AppMaterialIcons className="" icon="check" size={6} />
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
StatusIndicator.displayName = 'StatusIndicator';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
indicatorContainer: css`
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: ${token.colorTextPlaceholder};
|
||||
border-radius: 100%;
|
||||
|
||||
&.in-progress {
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
indicator: css`
|
||||
color: white;
|
||||
padding: 1px;
|
||||
border-radius: 100%;
|
||||
background-color: ${token.colorTextPlaceholder};
|
||||
box-shadow: 0 0 0 0.7px white inset;
|
||||
|
||||
&.in-progress {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
`
|
||||
}));
|
|
@ -30,7 +30,7 @@ export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.m
|
|||
const lastMessageIndex = responseMessages.length - 1;
|
||||
|
||||
return (
|
||||
<MessageContainer className="flex w-full flex-col space-y-1">
|
||||
<MessageContainer className="flex w-full flex-col space-y-1 overflow-hidden">
|
||||
{responseMessages.map((responseMessage, index) => {
|
||||
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader';
|
||||
import { AppMaterialIcons } from '@/components/icons';
|
||||
|
||||
const animationConfig = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.25 }
|
||||
};
|
||||
|
||||
export const StatusIndicator: React.FC<{ status?: 'completed' | 'loading' | 'failed' }> =
|
||||
React.memo(({ status }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const inProgress = status === 'loading';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
styles.indicatorContainer,
|
||||
inProgress && 'in-progress',
|
||||
'flex items-center justify-center transition-all delay-100 duration-300'
|
||||
)}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={inProgress ? 'in-progress' : 'completed'}
|
||||
{...animationConfig}
|
||||
className={cx(
|
||||
styles.indicator,
|
||||
inProgress && 'in-progress',
|
||||
'flex items-center justify-center transition-all duration-300'
|
||||
)}>
|
||||
{inProgress ? (
|
||||
<CircleSpinnerLoader size={8} />
|
||||
) : (
|
||||
<AppMaterialIcons className="" icon="check" size={6} />
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
StatusIndicator.displayName = 'StatusIndicator';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
indicatorContainer: css`
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: ${token.colorTextPlaceholder};
|
||||
border-radius: 100%;
|
||||
|
||||
&.in-progress {
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
indicator: css`
|
||||
color: white;
|
||||
padding: 1px;
|
||||
border-radius: 100%;
|
||||
background-color: ${token.colorTextPlaceholder};
|
||||
box-shadow: 0 0 0 0.7px white inset;
|
||||
|
||||
&.in-progress {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
`
|
||||
}));
|
|
@ -11,7 +11,7 @@ export const MessageContainer: React.FC<{
|
|||
}> = React.memo(({ children, senderName, senderId, senderAvatar, className = '' }) => {
|
||||
const { cx } = useStyles();
|
||||
return (
|
||||
<div className={cx('flex w-full space-x-2')}>
|
||||
<div className={cx('flex w-full space-x-2 overflow-hidden')}>
|
||||
{senderName ? (
|
||||
<BusterUserAvatar size={24} name={senderName} src={senderAvatar} useToolTip={true} />
|
||||
) : (
|
||||
|
|
|
@ -6,7 +6,8 @@ import {
|
|||
type BusterChatMessageRequest,
|
||||
type BusterChatMessageResponse,
|
||||
FileType,
|
||||
BusterChatMessage_thoughtPill
|
||||
BusterChatMessage_thoughtPill,
|
||||
BusterChatMessage_fileMetadata
|
||||
} from '@/api/buster_socket/chats';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
|
@ -45,17 +46,31 @@ export const createMockResponseMessageThought = (): BusterChatMessage_thought =>
|
|||
thought_secondary_title: faker.lorem.word(),
|
||||
thought_pills: fourRandomPills,
|
||||
hidden: false,
|
||||
in_progress: undefined
|
||||
status: undefined
|
||||
};
|
||||
};
|
||||
|
||||
const createMockResponseMessageFile = (): BusterChatMessage_file => {
|
||||
const randomMetadataCount = faker.number.int(3);
|
||||
const randomMetadata: BusterChatMessage_fileMetadata[] = Array.from(
|
||||
{ length: randomMetadataCount },
|
||||
() => {
|
||||
return {
|
||||
status: 'completed',
|
||||
message: faker.lorem.sentence(),
|
||||
timestamp: faker.date.recent().toISOString()
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
type: 'file',
|
||||
file_type: 'metric',
|
||||
version_number: 1,
|
||||
version_id: faker.string.uuid()
|
||||
version_id: faker.string.uuid(),
|
||||
file_name: faker.company.name(),
|
||||
metadata: randomMetadata
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -72,22 +87,8 @@ export const MOCK_CHAT: BusterChat = {
|
|||
createMockResponseMessageText(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought()
|
||||
// createMockResponseMessageFile(),
|
||||
// createMockResponseMessageFile()
|
||||
createMockResponseMessageFile(),
|
||||
createMockResponseMessageFile()
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue