thoughts pills

This commit is contained in:
Nate Kelley 2025-01-28 11:16:34 -07:00
parent 52c4521750
commit aec79d84a6
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 183 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -55,9 +55,9 @@ export const MOCK_CHAT: BusterChat = {
response_messages: [
createMockResponseMessageText(),
createMockResponseMessageThought(),
createMockResponseMessageThought(),
createMockResponseMessageThought(),
createMockResponseMessageThought(),
// createMockResponseMessageThought(),
// createMockResponseMessageThought(),
// createMockResponseMessageThought(),
createMockResponseMessageFile(),
createMockResponseMessageFile()
]

View File

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