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 React, { useMemo } from 'react';
|
||||
import {
|
||||
BusterChatMessage_thought,
|
||||
BusterChatMessage_thoughtPill
|
||||
} from '@/api/buster_socket/chats';
|
||||
import React, { useState } from 'react';
|
||||
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { animationConfig } from './animationConfig';
|
||||
|
@ -7,6 +10,9 @@ import { CircleSpinnerLoader } from '@/components/loaders/CircleSpinnerLoader';
|
|||
import { Text } from '@/components/text';
|
||||
import { createStyles } from 'antd-style';
|
||||
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(
|
||||
({ responseMessage: responseMessageProp, isCompletedStream }) => {
|
||||
|
@ -15,6 +21,19 @@ export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = R
|
|||
const { styles, cx } = useStyles();
|
||||
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 (
|
||||
<AnimatePresence initial={!isCompletedStream}>
|
||||
<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} />
|
||||
<VerticalBar inProgress={in_progress} hasPills={hasPills} />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<Text size="sm">{thought_title}</Text>
|
||||
<div className="flex w-full flex-col space-y-2">
|
||||
<div className="flex w-full items-center space-x-1.5 overflow-hidden">
|
||||
<Text size="sm" className="truncate">
|
||||
{thought_title}
|
||||
</Text>
|
||||
<Text size="sm" type="tertiary">
|
||||
{thought_secondary_title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<PillContainer pills={thought_pills} />
|
||||
<PillContainer pills={myPills} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</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 }) => ({
|
||||
container: css`
|
||||
position: relative;
|
||||
|
@ -113,13 +113,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||
height: 100%;
|
||||
background-color: ${token.colorTextPlaceholder};
|
||||
`,
|
||||
pillContainer: css``,
|
||||
pill: {
|
||||
backgroundColor: token.controlItemBgActive,
|
||||
border: `0.5px solid ${token.colorBorder}`,
|
||||
borderRadius: token.borderRadiusLG,
|
||||
padding: '2px 8px'
|
||||
},
|
||||
indicatorContainer: css`
|
||||
width: 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 (
|
||||
<MessageContainer className="flex flex-col space-y-1">
|
||||
<MessageContainer className="flex w-full flex-col space-y-1">
|
||||
{responseMessages.map((responseMessage) => {
|
||||
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
|
||||
return (
|
||||
|
|
|
@ -9,9 +9,9 @@ export const MessageContainer: React.FC<{
|
|||
senderAvatar?: string | null;
|
||||
className?: string;
|
||||
}> = React.memo(({ children, senderName, senderId, senderAvatar, className = '' }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const { cx } = useStyles();
|
||||
return (
|
||||
<div className={cx('flex space-x-2')}>
|
||||
<div className={cx('flex w-full space-x-2')}>
|
||||
{senderName ? (
|
||||
<BusterUserAvatar size={24} name={senderName} src={senderAvatar} useToolTip={true} />
|
||||
) : (
|
||||
|
|
|
@ -55,9 +55,9 @@ export const MOCK_CHAT: BusterChat = {
|
|||
response_messages: [
|
||||
createMockResponseMessageText(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
createMockResponseMessageThought(),
|
||||
// createMockResponseMessageThought(),
|
||||
// createMockResponseMessageThought(),
|
||||
// createMockResponseMessageThought(),
|
||||
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));
|
||||
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