diff --git a/app/ui/app/src/components/Message.tsx b/app/ui/app/src/components/Message.tsx index 5ec50d7e9..4623962b7 100644 --- a/app/ui/app/src/components/Message.tsx +++ b/app/ui/app/src/components/Message.tsx @@ -7,6 +7,7 @@ import React, { useState, useMemo, useRef } from "react"; import { Reasoning, getThinkingMessage, + ReasoningContent, } from "@/components/ai-elements/reasoning"; import { CollapsibleContent, @@ -958,14 +959,18 @@ function OtherRoleMessage({ -
- +
+ + {message.thinking} +
diff --git a/app/ui/app/src/components/StreamingMarkdownContent.tsx b/app/ui/app/src/components/StreamingMarkdownContent.tsx index 249a203a8..146969fc4 100644 --- a/app/ui/app/src/components/StreamingMarkdownContent.tsx +++ b/app/ui/app/src/components/StreamingMarkdownContent.tsx @@ -10,6 +10,7 @@ interface StreamingMarkdownContentProps { isStreaming?: boolean; size?: "sm" | "md" | "lg"; browserToolResult?: any; // TODO: proper type + className?: string; } // Helper to extract text from React nodes @@ -125,19 +126,26 @@ const CodeBlock = React.memo( ); const StreamingMarkdownContent: React.FC = - React.memo(({ content, isStreaming = false, size, browserToolResult }) => { - // Build the remark plugins array - keep default GFM and Math, add citations - const remarkPlugins = React.useMemo(() => { - return [ - defaultRemarkPlugins.gfm, - defaultRemarkPlugins.math, - remarkCitationParser, - ]; - }, []); + React.memo( + ({ + content, + isStreaming = false, + size, + browserToolResult, + className = "", + }) => { + // Build the remark plugins array - keep default GFM and Math, add citations + const remarkPlugins = React.useMemo(() => { + return [ + defaultRemarkPlugins.gfm, + defaultRemarkPlugins.math, + remarkCitationParser, + ]; + }, []); - return ( -
= dark:prose-ul:marker:text-neutral-300 dark:prose-li:marker:text-neutral-300 break-words + ${className} `} - > - = > {content} - -
- ); - }); +
+ ); + }, + ); interface StreamingMarkdownErrorBoundaryProps { content: string; diff --git a/app/ui/app/src/components/ai-elements/reasoning.tsx b/app/ui/app/src/components/ai-elements/reasoning.tsx index fca223995..0de01cdb0 100644 --- a/app/ui/app/src/components/ai-elements/reasoning.tsx +++ b/app/ui/app/src/components/ai-elements/reasoning.tsx @@ -1,11 +1,16 @@ "use client"; import { useControllableState } from "@radix-ui/react-use-controllable-state"; -import { Collapsible, CollapsibleTrigger } from "@radix-ui/react-collapsible"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@radix-ui/react-collapsible"; import { ChevronDownIcon } from "lucide-react"; import type { ComponentProps } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react"; import { Shimmer } from "./shimmer"; +import StreamingMarkdownContent from "../StreamingMarkdownContent"; type ReasoningContextValue = { isStreaming: boolean; @@ -99,7 +104,7 @@ export const getThinkingMessage = (isStreaming: boolean, duration?: number) => { if (duration === undefined) { return Thought for a few seconds; } - if (duration < 2) { + if (duration <= 2) { return Thought for a moment; } return Thought for {duration} seconds; @@ -133,5 +138,40 @@ export const ReasoningTrigger = memo( }, ); +export type ReasoningContentProps = ComponentProps< + typeof CollapsibleContent +> & { + children: string; + isStreaming?: boolean; +}; + +export const ReasoningContent = memo( + ({ + className, + children, + isStreaming = false, + ...props + }: ReasoningContentProps) => { + const reasoningContext = useReasoning(); + const actuallyStreaming = isStreaming ?? reasoningContext.isStreaming; + + return ( + +
+ +
+
+ ); + }, +); + Reasoning.displayName = "Reasoning"; ReasoningTrigger.displayName = "ReasoningTrigger"; +ReasoningContent.displayName = "ReasoningContent";