version id update

This commit is contained in:
Nate Kelley 2025-01-28 15:21:07 -07:00
parent 29b2c8996f
commit 330c3067d5
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
13 changed files with 328 additions and 115 deletions

View File

@ -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[];

View File

@ -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`

View File

@ -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';

View 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};
`
}));

View File

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

View 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>

View File

@ -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">

View File

@ -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';

View File

@ -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;
}
`
}));

View File

@ -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 (

View File

@ -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;
}
`
}));

View File

@ -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} />
) : (

View File

@ -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()
]
}
],