This commit is contained in:
Eva Ho 2025-11-06 16:23:57 -05:00
parent a9278710be
commit a077384107
3 changed files with 79 additions and 29 deletions

View File

@ -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({
</CollapsibleTrigger>
<CollapsibleContent
forceMount
className="relative ml-6 mt-3 outline-none overflow-hidden transition-all duration-300 ease-in-out data-[state=closed]:max-h-0 data-[state=closed]:opacity-0 data-[state=open]:max-h-28 data-[state=open]:opacity-100"
className={`relative ml-6 mt-3 outline-none overflow-hidden transition-all duration-300 ease-in-out data-[state=closed]:max-h-0 data-[state=closed]:opacity-0 data-[state=open]:opacity-100 ${
activelyThinking ? "data-[state=open]:max-h-28" : ""
}`}
>
<div className="text-xs text-neutral-500 dark:text-neutral-500 rounded-md max-h-28 overflow-y-auto">
<StreamingMarkdownContent
content={message.thinking}
isStreaming={!!activelyThinking}
size="sm"
/>
<div
className={`text-sm rounded-md ${
activelyThinking ? "max-h-28 overflow-y-auto" : ""
}`}
>
<ReasoningContent isStreaming={!!activelyThinking}>
{message.thinking}
</ReasoningContent>
</div>
</CollapsibleContent>
</Reasoning>

View File

@ -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<StreamingMarkdownContentProps> =
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 (
<div
className={`
return (
<div
className={`
max-w-full
${size === "sm" ? "prose-sm" : size === "lg" ? "prose-lg" : ""}
prose
@ -201,11 +209,8 @@ const StreamingMarkdownContent: React.FC<StreamingMarkdownContentProps> =
dark:prose-ul:marker:text-neutral-300
dark:prose-li:marker:text-neutral-300
break-words
${className}
`}
>
<StreamingMarkdownErrorBoundary
content={content}
isStreaming={isStreaming}
>
<Streamdown
parseIncompleteMarkdown={isStreaming}
@ -278,10 +283,10 @@ const StreamingMarkdownContent: React.FC<StreamingMarkdownContentProps> =
>
{content}
</Streamdown>
</StreamingMarkdownErrorBoundary>
</div>
);
});
</div>
);
},
);
interface StreamingMarkdownErrorBoundaryProps {
content: string;

View File

@ -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 <span>Thought for a few seconds</span>;
}
if (duration < 2) {
if (duration <= 2) {
return <span>Thought for a moment</span>;
}
return <span>Thought for {duration} seconds</span>;
@ -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 (
<CollapsibleContent
className={`data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in ${className || ""}`}
{...props}
>
<div className="[&_*]:!text-neutral-500 dark:[&_*]:!text-neutral-500">
<StreamingMarkdownContent
content={children}
isStreaming={actuallyStreaming}
size="sm"
/>
</div>
</CollapsibleContent>
);
},
);
Reasoning.displayName = "Reasoning";
ReasoningTrigger.displayName = "ReasoningTrigger";
ReasoningContent.displayName = "ReasoningContent";