From aec79d84a6b0cd0861cb9c54b96955722b9f8418 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Tue, 28 Jan 2025 11:16:34 -0700 Subject: [PATCH] thoughts pills --- .../ChatResponseMessage_Thought.tsx | 61 ++++---- .../ChatResponseMessage_ThoughtPills.tsx | 140 ++++++++++++++++++ .../ChatResponseMessages.tsx | 2 +- .../ChatContent/MessageContainer.tsx | 4 +- web/src/context/Chats/MOCK_CHAT.ts | 6 +- web/src/utils/text.ts | 10 ++ 6 files changed, 183 insertions(+), 40 deletions(-) create mode 100644 web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_ThoughtPills.tsx diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought.tsx index 95e975e12..5dc0511e9 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_Thought.tsx @@ -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 = React.memo( ({ responseMessage: responseMessageProp, isCompletedStream }) => { @@ -15,6 +21,19 @@ export const ChatResponseMessage_Thought: React.FC = 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 ( @@ -22,15 +41,17 @@ export const ChatResponseMessage_Thought: React.FC = R -
-
- {thought_title} +
+
+ + {thought_title} + {thought_secondary_title}
- +
@@ -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 ( - - ); -}; - -const ThoughtPill: React.FC<{ - pill: NonNullable[number]; -}> = ({ pill }) => { - const { styles, cx } = useStyles(); - return
; -}; - 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; diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_ThoughtPills.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_ThoughtPills.tsx new file mode 100644 index 000000000..8caff8001 --- /dev/null +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessage_ThoughtPills.tsx @@ -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([]); + const [hiddenCount, setHiddenCount] = useState(0); + const [maxSeen, setMaxSeen] = useState(0); + const containerRef = useRef(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 ( + + 0 ? 'visible' : 'hidden'} + className={cx('flex w-full flex-wrap flex-nowrap gap-1.5 overflow-hidden pb-2.5')}> + {visiblePills.map((pill) => ( + + {pill.text} + + ))} + {hiddenCount > 0 && +{hiddenCount} more} + + + ); +}; + +const Pill: React.FC<{ children: React.ReactNode; useAnimation: boolean }> = ({ + children, + useAnimation +}) => { + const { styles, cx } = useStyles(); + return ( + + + {children} + + + ); +}; + +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' + } +})); diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx index 00f8f288b..0f8d3b804 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/ChatResponseMessages/ChatResponseMessages.tsx @@ -69,7 +69,7 @@ export const ChatResponseMessages: React.FC = React.m // }); return ( - + {responseMessages.map((responseMessage) => { const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type]; return ( diff --git a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx index a2c310016..2a2b32a44 100644 --- a/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx +++ b/web/src/app/app/_layouts/ChatLayout/ChatContainer/ChatContent/MessageContainer.tsx @@ -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 ( -
+
{senderName ? ( ) : ( diff --git a/web/src/context/Chats/MOCK_CHAT.ts b/web/src/context/Chats/MOCK_CHAT.ts index b84a3bacf..70cf41cd0 100644 --- a/web/src/context/Chats/MOCK_CHAT.ts +++ b/web/src/context/Chats/MOCK_CHAT.ts @@ -55,9 +55,9 @@ export const MOCK_CHAT: BusterChat = { response_messages: [ createMockResponseMessageText(), createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), - createMockResponseMessageThought(), + // createMockResponseMessageThought(), + // createMockResponseMessageThought(), + // createMockResponseMessageThought(), createMockResponseMessageFile(), createMockResponseMessageFile() ] diff --git a/web/src/utils/text.ts b/web/src/utils/text.ts index 4bf355682..8f961cdc4 100644 --- a/web/src/utils/text.ts +++ b/web/src/utils/text.ts @@ -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; +};