mirror of https://github.com/buster-so/buster.git
thoughts pills
This commit is contained in:
parent
52c4521750
commit
aec79d84a6
|
@ -1,5 +1,8 @@
|
||||||
import { BusterChatMessage_thought } from '@/api/buster_socket/chats';
|
import {
|
||||||
import React, { useMemo } from 'react';
|
BusterChatMessage_thought,
|
||||||
|
BusterChatMessage_thoughtPill
|
||||||
|
} from '@/api/buster_socket/chats';
|
||||||
|
import React, { useState } from 'react';
|
||||||
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { animationConfig } from './animationConfig';
|
import { animationConfig } from './animationConfig';
|
||||||
|
@ -7,6 +10,9 @@ import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader';
|
||||||
import { Text } from '@/components/text';
|
import { Text } from '@/components/text';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { AppMaterialIcons } from '@/components';
|
import { AppMaterialIcons } from '@/components';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { PillContainer } from './ChatResponseMessage_ThoughtPills';
|
||||||
|
|
||||||
export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = React.memo(
|
export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = React.memo(
|
||||||
({ responseMessage: responseMessageProp, isCompletedStream }) => {
|
({ responseMessage: responseMessageProp, isCompletedStream }) => {
|
||||||
|
@ -15,6 +21,19 @@ export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = R
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const hasPills = thought_pills && thought_pills.length > 0;
|
const hasPills = thought_pills && thought_pills.length > 0;
|
||||||
|
|
||||||
|
const [myPills, setMyPills] = useState(thought_pills || []);
|
||||||
|
|
||||||
|
useHotkeys('j', () => {
|
||||||
|
const fourRandomPills: BusterChatMessage_thoughtPill[] = Array.from({ length: 5 }, () => {
|
||||||
|
return {
|
||||||
|
text: faker.lorem.word(),
|
||||||
|
type: 'term',
|
||||||
|
id: faker.string.uuid()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setMyPills(fourRandomPills);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence initial={!isCompletedStream}>
|
<AnimatePresence initial={!isCompletedStream}>
|
||||||
<motion.div className={cx(styles.container, 'flex space-x-1.5')} {...animationConfig}>
|
<motion.div className={cx(styles.container, 'flex space-x-1.5')} {...animationConfig}>
|
||||||
|
@ -22,15 +41,17 @@ export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = R
|
||||||
<StatusIndicator inProgress={in_progress} />
|
<StatusIndicator inProgress={in_progress} />
|
||||||
<VerticalBar inProgress={in_progress} hasPills={hasPills} />
|
<VerticalBar inProgress={in_progress} hasPills={hasPills} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex w-full flex-col space-y-2">
|
||||||
<div className="flex items-center space-x-1.5">
|
<div className="flex w-full items-center space-x-1.5 overflow-hidden">
|
||||||
<Text size="sm">{thought_title}</Text>
|
<Text size="sm" className="truncate">
|
||||||
|
{thought_title}
|
||||||
|
</Text>
|
||||||
<Text size="sm" type="tertiary">
|
<Text size="sm" type="tertiary">
|
||||||
{thought_secondary_title}
|
{thought_secondary_title}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PillContainer pills={thought_pills} />
|
<PillContainer pills={myPills} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
@ -83,27 +104,6 @@ const VerticalBar: React.FC<{ inProgress?: boolean; hasPills?: boolean }> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PillContainer: React.FC<{ pills: BusterChatMessage_thought['thought_pills'] }> = ({
|
|
||||||
pills
|
|
||||||
}) => {
|
|
||||||
const { styles, cx } = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0 }}
|
|
||||||
animate={{ height: 'auto' }}
|
|
||||||
exit={{ height: 0 }}
|
|
||||||
className={cx(styles.pillContainer, 'min-h-0.5')}></motion.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ThoughtPill: React.FC<{
|
|
||||||
pill: NonNullable<BusterChatMessage_thought['thought_pills']>[number];
|
|
||||||
}> = ({ pill }) => {
|
|
||||||
const { styles, cx } = useStyles();
|
|
||||||
return <div className={cx(styles.pill, '')}></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
container: css`
|
container: css`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -113,13 +113,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: ${token.colorTextPlaceholder};
|
background-color: ${token.colorTextPlaceholder};
|
||||||
`,
|
`,
|
||||||
pillContainer: css``,
|
|
||||||
pill: {
|
|
||||||
backgroundColor: token.controlItemBgActive,
|
|
||||||
border: `0.5px solid ${token.colorBorder}`,
|
|
||||||
borderRadius: token.borderRadiusLG,
|
|
||||||
padding: '2px 8px'
|
|
||||||
},
|
|
||||||
indicatorContainer: css`
|
indicatorContainer: css`
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
import {
|
||||||
|
BusterChatMessage_thought,
|
||||||
|
BusterChatMessage_thoughtPill
|
||||||
|
} from '@/api/buster_socket/chats';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { calculateTextWidth } from '@/utils';
|
||||||
|
import { useDebounce, useSize } from 'ahooks';
|
||||||
|
|
||||||
|
const duration = 0.25;
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: {
|
||||||
|
height: 0
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
height: '28px',
|
||||||
|
transition: {
|
||||||
|
duration: duration,
|
||||||
|
staggerChildren: 0.035,
|
||||||
|
delayChildren: 0.075
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pillVariants = {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PillContainer: React.FC<{ pills: BusterChatMessage_thought['thought_pills'] }> = ({
|
||||||
|
pills = []
|
||||||
|
}) => {
|
||||||
|
const { styles, cx } = useStyles();
|
||||||
|
const [visiblePills, setVisiblePills] = useState<BusterChatMessage_thoughtPill[]>([]);
|
||||||
|
const [hiddenCount, setHiddenCount] = useState(0);
|
||||||
|
const [maxSeen, setMaxSeen] = useState(0);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const useAnimation = true;
|
||||||
|
|
||||||
|
const size = useSize(containerRef);
|
||||||
|
const debouncedWidth = useDebounce(size?.width, {
|
||||||
|
wait: 320,
|
||||||
|
leading: true
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || !pills) return;
|
||||||
|
|
||||||
|
const containerWidth = containerRef.current.offsetWidth;
|
||||||
|
const pillPadding = 8; // 4px left + 4px right
|
||||||
|
const pillMargin = 6; // 1.5 * 4 for gap
|
||||||
|
const pillBorder = 1; // 0.5px * 2
|
||||||
|
const font = '11px -apple-system, BlinkMacSystemFont, sans-serif'; // Match your app's font
|
||||||
|
|
||||||
|
let currentLineWidth = 0;
|
||||||
|
const visible: BusterChatMessage_thoughtPill[] = [];
|
||||||
|
let hidden = 0;
|
||||||
|
|
||||||
|
// Calculate width needed for "+X more" pill
|
||||||
|
const moreTextWidth = calculateTextWidth('+99 more', font) + pillPadding + pillBorder;
|
||||||
|
|
||||||
|
for (let i = 0; i < pills.length; i++) {
|
||||||
|
const pill = pills[i];
|
||||||
|
const textWidth = calculateTextWidth(pill.text, font);
|
||||||
|
const pillWidth = textWidth + pillPadding + pillBorder;
|
||||||
|
|
||||||
|
// Check if adding this pill would exceed container width
|
||||||
|
if (
|
||||||
|
currentLineWidth + pillWidth + (visible.length > 0 ? pillMargin : 0) + moreTextWidth <=
|
||||||
|
containerWidth
|
||||||
|
) {
|
||||||
|
visible.push(pill);
|
||||||
|
currentLineWidth += pillWidth + (visible.length > 0 ? pillMargin : 0);
|
||||||
|
} else {
|
||||||
|
hidden++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisiblePills(visible);
|
||||||
|
setHiddenCount(hidden);
|
||||||
|
setMaxSeen(visible.length);
|
||||||
|
}, [pills, containerRef.current, debouncedWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={true}>
|
||||||
|
<motion.div
|
||||||
|
ref={containerRef}
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={visiblePills.length > 0 ? 'visible' : 'hidden'}
|
||||||
|
className={cx('flex w-full flex-wrap flex-nowrap gap-1.5 overflow-hidden pb-2.5')}>
|
||||||
|
{visiblePills.map((pill) => (
|
||||||
|
<Pill key={pill.id} useAnimation={useAnimation}>
|
||||||
|
{pill.text}
|
||||||
|
</Pill>
|
||||||
|
))}
|
||||||
|
{hiddenCount > 0 && <Pill useAnimation={useAnimation}>+{hiddenCount} more</Pill>}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pill: React.FC<{ children: React.ReactNode; useAnimation: boolean }> = ({
|
||||||
|
children,
|
||||||
|
useAnimation
|
||||||
|
}) => {
|
||||||
|
const { styles, cx } = useStyles();
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={true}>
|
||||||
|
<motion.div
|
||||||
|
variants={pillVariants}
|
||||||
|
className={cx(styles.pill, 'flex items-center justify-center truncate')}>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Pill.displayName = 'Pill';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
|
pill: {
|
||||||
|
color: token.colorTextTertiary,
|
||||||
|
backgroundColor: token.controlItemBgActive,
|
||||||
|
border: `0.5px solid ${token.colorBorder}`,
|
||||||
|
borderRadius: token.borderRadiusLG,
|
||||||
|
padding: '0px 4px',
|
||||||
|
height: '18px',
|
||||||
|
fontSize: '11px'
|
||||||
|
}
|
||||||
|
}));
|
|
@ -69,7 +69,7 @@ export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.m
|
||||||
// });
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContainer className="flex flex-col space-y-1">
|
<MessageContainer className="flex w-full flex-col space-y-1">
|
||||||
{responseMessages.map((responseMessage) => {
|
{responseMessages.map((responseMessage) => {
|
||||||
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
|
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,9 +9,9 @@ export const MessageContainer: React.FC<{
|
||||||
senderAvatar?: string | null;
|
senderAvatar?: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = React.memo(({ children, senderName, senderId, senderAvatar, className = '' }) => {
|
}> = React.memo(({ children, senderName, senderId, senderAvatar, className = '' }) => {
|
||||||
const { styles, cx } = useStyles();
|
const { cx } = useStyles();
|
||||||
return (
|
return (
|
||||||
<div className={cx('flex space-x-2')}>
|
<div className={cx('flex w-full space-x-2')}>
|
||||||
{senderName ? (
|
{senderName ? (
|
||||||
<BusterUserAvatar size={24} name={senderName} src={senderAvatar} useToolTip={true} />
|
<BusterUserAvatar size={24} name={senderName} src={senderAvatar} useToolTip={true} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -55,9 +55,9 @@ export const MOCK_CHAT: BusterChat = {
|
||||||
response_messages: [
|
response_messages: [
|
||||||
createMockResponseMessageText(),
|
createMockResponseMessageText(),
|
||||||
createMockResponseMessageThought(),
|
createMockResponseMessageThought(),
|
||||||
createMockResponseMessageThought(),
|
// createMockResponseMessageThought(),
|
||||||
createMockResponseMessageThought(),
|
// createMockResponseMessageThought(),
|
||||||
createMockResponseMessageThought(),
|
// createMockResponseMessageThought(),
|
||||||
createMockResponseMessageFile(),
|
createMockResponseMessageFile(),
|
||||||
createMockResponseMessageFile()
|
createMockResponseMessageFile()
|
||||||
]
|
]
|
||||||
|
|
|
@ -68,3 +68,13 @@ export const makeHumanReadble = (input: string | number | undefined | null): str
|
||||||
const capitalizedWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
|
const capitalizedWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
|
||||||
return capitalizedWords.join(' ');
|
return capitalizedWords.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const calculateTextWidth = (text: string, font: string): number => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) return 0;
|
||||||
|
context.font = font;
|
||||||
|
const width = context.measureText(text).width;
|
||||||
|
canvas.remove();
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue