diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 2f28b4fd..9cfa4244 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['docker-model-runner-download-from-ui'] # put your current branch to create a build. Core team only. + branches: ['4855-thinking-block-toggle'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index 6b08a3b6..0c6f41c7 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -115,7 +115,7 @@ const HistoricalMessage = ({ {isRefusalMessage && ( { + ({ role, message, messageId }) => { // If the message is not from the assistant, we can render it directly // as normal since the user cannot think (lol) if (role !== "assistant") @@ -240,18 +240,16 @@ const RenderChatContent = memo( // This can occur when the assistant starts with and then 's later. if ( message.match(THOUGHT_REGEX_OPEN) && - message.match(THOUGHT_REGEX_CLOSE) + !message.match(THOUGHT_REGEX_CLOSE) ) { - const closingTag = message.match(THOUGHT_REGEX_CLOSE)?.[0]; - const splitMessage = message.split(closingTag); - thoughtChain = splitMessage[0] + closingTag; - msgToRender = splitMessage[1]; + thoughtChain = message; + msgToRender = ""; } return ( <> {thoughtChain && ( - + )} @@ -94,7 +95,7 @@ export function WorkspaceProfileImage({ workspace }) { return ; } -function RenderAssistantChatContent({ message }) { +function RenderAssistantChatContent({ message, messageId }) { const contentRef = useRef(""); const thoughtChainRef = useRef(null); @@ -121,7 +122,11 @@ function RenderAssistantChatContent({ message }) { message.match(THOUGHT_REGEX_OPEN) && !message.match(THOUGHT_REGEX_CLOSE); if (thinking) return ( - + ); return ( @@ -130,7 +135,7 @@ function RenderAssistantChatContent({ message }) { )} { + if (!messageId) return false; + return expansionStates[messageId] ?? false; + }, + [expansionStates] + ); + + const setExpanded = useCallback((messageId, expanded) => { + if (!messageId) return; + setExpansionStates((prev) => ({ + ...prev, + [messageId]: expanded, + })); + }, []); + + return ( + + {children} + + ); +} + +export function useThoughtExpansion(messageId) { + const context = useContext(ThoughtExpansionContext); + if (!context) { + // Fallback when used outside provider - use local state only + return { expanded: false, setExpanded: () => {} }; + } + return { + expanded: context.getExpanded(messageId), + setExpanded: (value) => context.setExpanded(messageId, value), + }; +} + const THOUGHT_KEYWORDS = ["thought", "thinking", "think", "thought_chain"]; const CLOSING_TAGS = [...THOUGHT_KEYWORDS, "response", "answer"]; export const THOUGHT_REGEX_OPEN = new RegExp( @@ -40,16 +92,31 @@ function contentIsNotEmpty(content = "") { /** * Component to render a thought chain. * @param {string} content - The content of the thought chain. - * @param {boolean} expanded - Whether the thought chain is expanded. + * @param {string} messageId - The unique ID for this message (used to persist expansion state). * @returns {JSX.Element} */ export const ThoughtChainComponent = forwardRef( - ({ content: initialContent, expanded }, ref) => { + ({ content: initialContent, messageId }, ref) => { const [content, setContent] = useState(initialContent); const [hasReadableContent, setHasReadableContent] = useState( contentIsNotEmpty(initialContent) ); - const [isExpanded, setIsExpanded] = useState(expanded); + const { expanded: persistedExpanded, setExpanded: setPersistedExpanded } = + useThoughtExpansion(messageId); + const [localExpanded, setLocalExpanded] = useState(false); + + // Use persisted state if messageId is provided, otherwise use local state + const isExpanded = messageId ? persistedExpanded : localExpanded; + const setIsExpanded = messageId ? setPersistedExpanded : setLocalExpanded; + + // Sync content state with prop changes (for streaming through HistoricalMessage) + useEffect(() => { + if (initialContent !== content) { + setContent(initialContent); + setHasReadableContent(contentIsNotEmpty(initialContent)); + } + }, [initialContent]); + useImperativeHandle(ref, () => ({ updateContent: (newContent) => { setContent(newContent); @@ -65,8 +132,6 @@ export const ThoughtChainComponent = forwardRef( const tagStrippedContent = content .replace(THOUGHT_REGEX_OPEN, "") .replace(THOUGHT_REGEX_CLOSE, ""); - const autoExpand = - isThinking && tagStrippedContent.length > THOUGHT_PREVIEW_LENGTH; const canExpand = tagStrippedContent.length > THOUGHT_PREVIEW_LENGTH; if (!content || !content.length || !hasReadableContent) return null; @@ -83,10 +148,10 @@ export const ThoughtChainComponent = forwardRef( transition: "all 0.1s ease-in-out", borderRadius: "6px", }} - className={`${isExpanded || autoExpand ? "" : `${canExpand ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`} + className={`${isExpanded ? "" : `${canExpand ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`} >
{isThinking || isComplete ? ( <> @@ -115,16 +180,16 @@ export const ThoughtChainComponent = forwardRef(
- {!autoExpand && canExpand ? ( + {canExpand ? (