mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🧠 refactor: Improve Reasoning Component Structure and UX (#10320)
* refactor: Reasoning components with independent toggle buttons - Refactored ThinkingButton to remove unnecessary state and props. - Updated ContentParts to simplify content rendering and remove hover handling. - Improved Reasoning component to include independent toggle functionality for each THINK part. - Adjusted styles for better layout consistency and user experience. * refactor: isolate hover effects for Reasoning - Updated ThinkingButton to improve hover effects and layout consistency. - Refactored Reasoning component to include a new wrapper class for better styling. - Adjusted icon visibility and transitions for a smoother user experience. * fix: Prevent rendering of empty messages in Chat component - Added a check to skip rendering if the message text is only whitespace, improving the user interface by avoiding empty containers. * chore: Replace div with fragment in Thinking component for cleaner markup * chore: move Thinking component to Content Parts directory * refactor: prevent rendering of whitespace-only text in Part component only for edge cases
This commit is contained in:
parent
c0f1cfcaba
commit
9b4c4cafb6
5 changed files with 95 additions and 176 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import { memo, useMemo, useState, useCallback } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { ContentTypes } from 'librechat-data-provider';
|
import { ContentTypes } from 'librechat-data-provider';
|
||||||
import type {
|
import type {
|
||||||
TMessageContentParts,
|
TMessageContentParts,
|
||||||
|
|
@ -7,14 +6,11 @@ import type {
|
||||||
TAttachment,
|
TAttachment,
|
||||||
Agents,
|
Agents,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { ThinkingButton } from '~/components/Artifacts/Thinking';
|
|
||||||
import { MessageContext, SearchContext } from '~/Providers';
|
import { MessageContext, SearchContext } from '~/Providers';
|
||||||
import { showThinkingAtom } from '~/store/showThinking';
|
|
||||||
import MemoryArtifacts from './MemoryArtifacts';
|
import MemoryArtifacts from './MemoryArtifacts';
|
||||||
import Sources from '~/components/Web/Sources';
|
import Sources from '~/components/Web/Sources';
|
||||||
import { mapAttachments } from '~/utils/map';
|
import { mapAttachments } from '~/utils/map';
|
||||||
import { EditTextPart } from './Parts';
|
import { EditTextPart } from './Parts';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import Part from './Part';
|
import Part from './Part';
|
||||||
|
|
||||||
type ContentPartsProps = {
|
type ContentPartsProps = {
|
||||||
|
|
@ -52,53 +48,10 @@ const ContentParts = memo(
|
||||||
siblingIdx,
|
siblingIdx,
|
||||||
setSiblingIdx,
|
setSiblingIdx,
|
||||||
}: ContentPartsProps) => {
|
}: ContentPartsProps) => {
|
||||||
const localize = useLocalize();
|
|
||||||
const [showThinking, setShowThinking] = useAtom(showThinkingAtom);
|
|
||||||
const [isExpanded, setIsExpanded] = useState(showThinking);
|
|
||||||
const [isContentHovered, setIsContentHovered] = useState(false);
|
|
||||||
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
|
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
|
||||||
|
|
||||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||||
|
|
||||||
const handleContentEnter = useCallback(() => setIsContentHovered(true), []);
|
|
||||||
const handleContentLeave = useCallback(() => setIsContentHovered(false), []);
|
|
||||||
|
|
||||||
const hasReasoningParts = useMemo(() => {
|
|
||||||
const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
|
|
||||||
const allThinkPartsHaveContent =
|
|
||||||
content?.every((part) => {
|
|
||||||
if (part?.type !== ContentTypes.THINK) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof part.think === 'string') {
|
|
||||||
const cleanedContent = part.think.replace(/<\/?think>/g, '').trim();
|
|
||||||
return cleanedContent.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}) ?? false;
|
|
||||||
|
|
||||||
return hasThinkPart && allThinkPartsHaveContent;
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
// Extract all reasoning text for copy functionality
|
|
||||||
const reasoningContent = useMemo(() => {
|
|
||||||
if (!content) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
.filter((part) => part?.type === ContentTypes.THINK)
|
|
||||||
.map((part) => {
|
|
||||||
if (typeof part?.think === 'string') {
|
|
||||||
return part.think.replace(/<\/?think>/g, '').trim();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n\n');
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -147,91 +100,40 @@ const ContentParts = memo(
|
||||||
<SearchContext.Provider value={{ searchResults }}>
|
<SearchContext.Provider value={{ searchResults }}>
|
||||||
<MemoryArtifacts attachments={attachments} />
|
<MemoryArtifacts attachments={attachments} />
|
||||||
<Sources messageId={messageId} conversationId={conversationId || undefined} />
|
<Sources messageId={messageId} conversationId={conversationId || undefined} />
|
||||||
{hasReasoningParts && (
|
{content.map((part, idx) => {
|
||||||
<div onMouseEnter={handleContentEnter} onMouseLeave={handleContentLeave}>
|
if (!part) {
|
||||||
<div className="sticky top-0 z-10 mb-2 bg-surface-secondary pb-2 pt-2">
|
return null;
|
||||||
<ThinkingButton
|
}
|
||||||
isExpanded={isExpanded}
|
|
||||||
onClick={() =>
|
|
||||||
setIsExpanded((prev) => {
|
|
||||||
const val = !prev;
|
|
||||||
setShowThinking(val);
|
|
||||||
return val;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
effectiveIsSubmitting && isLast
|
|
||||||
? localize('com_ui_thinking')
|
|
||||||
: localize('com_ui_thoughts')
|
|
||||||
}
|
|
||||||
content={reasoningContent}
|
|
||||||
isContentHovered={isContentHovered}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{content
|
|
||||||
.filter((part) => part?.type === ContentTypes.THINK)
|
|
||||||
.map((part) => {
|
|
||||||
const originalIdx = content.indexOf(part);
|
|
||||||
return (
|
|
||||||
<MessageContext.Provider
|
|
||||||
key={`provider-${messageId}-${originalIdx}`}
|
|
||||||
value={{
|
|
||||||
messageId,
|
|
||||||
isExpanded,
|
|
||||||
conversationId,
|
|
||||||
partIndex: originalIdx,
|
|
||||||
nextType: content[originalIdx + 1]?.type,
|
|
||||||
isSubmitting: effectiveIsSubmitting,
|
|
||||||
isLatestMessage,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Part
|
|
||||||
part={part}
|
|
||||||
attachments={undefined}
|
|
||||||
isSubmitting={effectiveIsSubmitting}
|
|
||||||
key={`part-${messageId}-${originalIdx}`}
|
|
||||||
isCreatedByUser={isCreatedByUser}
|
|
||||||
isLast={originalIdx === content.length - 1}
|
|
||||||
showCursor={false}
|
|
||||||
/>
|
|
||||||
</MessageContext.Provider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{content
|
|
||||||
.filter((part) => part && part.type !== ContentTypes.THINK)
|
|
||||||
.map((part) => {
|
|
||||||
const originalIdx = content.indexOf(part);
|
|
||||||
const toolCallId =
|
|
||||||
(part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? '';
|
|
||||||
const attachments = attachmentMap[toolCallId];
|
|
||||||
|
|
||||||
return (
|
const toolCallId =
|
||||||
<MessageContext.Provider
|
(part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? '';
|
||||||
key={`provider-${messageId}-${originalIdx}`}
|
const partAttachments = attachmentMap[toolCallId];
|
||||||
value={{
|
|
||||||
messageId,
|
return (
|
||||||
isExpanded,
|
<MessageContext.Provider
|
||||||
conversationId,
|
key={`provider-${messageId}-${idx}`}
|
||||||
partIndex: originalIdx,
|
value={{
|
||||||
nextType: content[originalIdx + 1]?.type,
|
messageId,
|
||||||
isSubmitting: effectiveIsSubmitting,
|
isExpanded: true,
|
||||||
isLatestMessage,
|
conversationId,
|
||||||
}}
|
partIndex: idx,
|
||||||
>
|
nextType: content[idx + 1]?.type,
|
||||||
<Part
|
isSubmitting: effectiveIsSubmitting,
|
||||||
part={part}
|
isLatestMessage,
|
||||||
attachments={attachments}
|
}}
|
||||||
isSubmitting={effectiveIsSubmitting}
|
>
|
||||||
key={`part-${messageId}-${originalIdx}`}
|
<Part
|
||||||
isCreatedByUser={isCreatedByUser}
|
part={part}
|
||||||
isLast={originalIdx === content.length - 1}
|
attachments={partAttachments}
|
||||||
showCursor={originalIdx === content.length - 1 && isLast}
|
isSubmitting={effectiveIsSubmitting}
|
||||||
/>
|
key={`part-${messageId}-${idx}`}
|
||||||
</MessageContext.Provider>
|
isCreatedByUser={isCreatedByUser}
|
||||||
);
|
isLast={idx === content.length - 1}
|
||||||
})}
|
showCursor={idx === content.length - 1 && isLast}
|
||||||
|
/>
|
||||||
|
</MessageContext.Provider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SearchContext.Provider>
|
</SearchContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { DelayedRender } from '@librechat/client';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import type { TMessageContentProps, TDisplayProps } from '~/common';
|
import type { TMessageContentProps, TDisplayProps } from '~/common';
|
||||||
import Error from '~/components/Messages/Content/Error';
|
import Error from '~/components/Messages/Content/Error';
|
||||||
import Thinking from '~/components/Artifacts/Thinking';
|
|
||||||
import { useMessageContext } from '~/Providers';
|
import { useMessageContext } from '~/Providers';
|
||||||
import MarkdownLite from './MarkdownLite';
|
import MarkdownLite from './MarkdownLite';
|
||||||
import EditMessage from './EditMessage';
|
import EditMessage from './EditMessage';
|
||||||
|
import Thinking from './Parts/Thinking';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import Container from './Container';
|
import Container from './Container';
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ const Part = memo(
|
||||||
if (part.tool_call_ids != null && !text) {
|
if (part.tool_call_ids != null && !text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
/** Skip rendering if text is only whitespace to avoid empty Container */
|
||||||
|
if (!isLast && text.length > 0 && /^\s*$/.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||||
|
|
@ -75,7 +79,7 @@ const Part = memo(
|
||||||
if (typeof reasoning !== 'string') {
|
if (typeof reasoning !== 'string') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <Reasoning reasoning={reasoning} />;
|
return <Reasoning reasoning={reasoning} isLast={isLast ?? false} />;
|
||||||
} else if (part.type === ContentTypes.TOOL_CALL) {
|
} else if (part.type === ContentTypes.TOOL_CALL) {
|
||||||
const toolCall = part[ContentTypes.TOOL_CALL];
|
const toolCall = part[ContentTypes.TOOL_CALL];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo, useState, useCallback } from 'react';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
import { ContentTypes } from 'librechat-data-provider';
|
import { ContentTypes } from 'librechat-data-provider';
|
||||||
import { ThinkingContent } from '~/components/Artifacts/Thinking';
|
import { ThinkingContent, ThinkingButton } from './Thinking';
|
||||||
|
import { showThinkingAtom } from '~/store/showThinking';
|
||||||
import { useMessageContext } from '~/Providers';
|
import { useMessageContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
type ReasoningProps = {
|
type ReasoningProps = {
|
||||||
reasoning: string;
|
reasoning: string;
|
||||||
|
isLast: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,13 +30,16 @@ type ReasoningProps = {
|
||||||
* Key differences from legacy Thinking.tsx:
|
* Key differences from legacy Thinking.tsx:
|
||||||
* - Works with content parts array instead of plain text
|
* - Works with content parts array instead of plain text
|
||||||
* - Strips `<think>` tags instead of `:::thinking:::` markers
|
* - Strips `<think>` tags instead of `:::thinking:::` markers
|
||||||
* - Uses shared ThinkingButton via ContentParts.tsx
|
* - Each THINK part has its own independent toggle button
|
||||||
* - Controlled by MessageContext isExpanded state
|
* - Can be interleaved with other content types
|
||||||
*
|
*
|
||||||
* For legacy text-based messages, see Thinking.tsx component.
|
* For legacy text-based messages, see Thinking.tsx component.
|
||||||
*/
|
*/
|
||||||
const Reasoning = memo(({ reasoning }: ReasoningProps) => {
|
const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
|
||||||
const { isExpanded, nextType } = useMessageContext();
|
const localize = useLocalize();
|
||||||
|
const [showThinking] = useAtom(showThinkingAtom);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(showThinking);
|
||||||
|
const { isSubmitting, isLatestMessage, nextType } = useMessageContext();
|
||||||
|
|
||||||
// Strip <think> tags from the reasoning content (modern format)
|
// Strip <think> tags from the reasoning content (modern format)
|
||||||
const reasoningText = useMemo(() => {
|
const reasoningText = useMemo(() => {
|
||||||
|
|
@ -41,24 +49,45 @@ const Reasoning = memo(({ reasoning }: ReasoningProps) => {
|
||||||
.trim();
|
.trim();
|
||||||
}, [reasoning]);
|
}, [reasoning]);
|
||||||
|
|
||||||
|
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsExpanded((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||||
|
|
||||||
|
const label = useMemo(
|
||||||
|
() =>
|
||||||
|
effectiveIsSubmitting && isLast ? localize('com_ui_thinking') : localize('com_ui_thoughts'),
|
||||||
|
[effectiveIsSubmitting, localize, isLast],
|
||||||
|
);
|
||||||
|
|
||||||
if (!reasoningText) {
|
if (!reasoningText) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: The toggle button is rendered separately in ContentParts.tsx
|
|
||||||
// This component only handles the collapsible content area
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="group/reasoning">
|
||||||
className={cn(
|
<div className="sticky top-0 z-10 mb-2 bg-surface-secondary pb-2 pt-2">
|
||||||
'grid transition-all duration-300 ease-out',
|
<ThinkingButton
|
||||||
nextType !== ContentTypes.THINK && isExpanded && 'mb-4',
|
isExpanded={isExpanded}
|
||||||
)}
|
onClick={handleClick}
|
||||||
style={{
|
label={label}
|
||||||
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
content={reasoningText}
|
||||||
}}
|
/>
|
||||||
>
|
</div>
|
||||||
<div className="overflow-hidden">
|
<div
|
||||||
<ThinkingContent>{reasoningText}</ThinkingContent>
|
className={cn(
|
||||||
|
'grid transition-all duration-300 ease-out',
|
||||||
|
nextType !== ContentTypes.THINK && isExpanded && 'mb-4',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<ThinkingContent>{reasoningText}</ThinkingContent>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -35,25 +35,17 @@ export const ThinkingButton = memo(
|
||||||
onClick,
|
onClick,
|
||||||
label,
|
label,
|
||||||
content,
|
content,
|
||||||
isContentHovered = false,
|
|
||||||
}: {
|
}: {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
label: string;
|
label: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
isContentHovered?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const fontSize = useAtomValue(fontSizeAtom);
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
|
|
||||||
const [isButtonHovered, setIsButtonHovered] = useState(false);
|
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
const isHovered = useMemo(
|
|
||||||
() => isButtonHovered || isContentHovered,
|
|
||||||
[isButtonHovered, isContentHovered],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCopy = useCallback(
|
const handleCopy = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -71,23 +63,20 @@ export const ThinkingButton = memo(
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={() => setIsButtonHovered(true)}
|
|
||||||
onMouseLeave={() => setIsButtonHovered(false)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex flex-1 items-center justify-start rounded-lg leading-[18px]',
|
'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]',
|
||||||
fontSize,
|
fontSize,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isHovered ? (
|
<span className="relative mr-1.5 inline-flex h-[18px] w-[18px] items-center justify-center">
|
||||||
|
<Lightbulb className="icon-sm absolute text-text-secondary opacity-100 transition-opacity group-hover/button:opacity-0" />
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
'icon-sm mr-1.5 transform-gpu text-text-primary transition-transform duration-300',
|
'icon-sm absolute transform-gpu text-text-primary opacity-0 transition-all duration-300 group-hover/button:opacity-100',
|
||||||
isExpanded && 'rotate-180',
|
isExpanded && 'rotate-180',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
</span>
|
||||||
<Lightbulb className="icon-sm mr-1.5 text-text-secondary" />
|
|
||||||
)}
|
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
{content && (
|
{content && (
|
||||||
|
|
@ -132,16 +121,12 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const showThinking = useAtomValue(showThinkingAtom);
|
const showThinking = useAtomValue(showThinkingAtom);
|
||||||
const [isExpanded, setIsExpanded] = useState(showThinking);
|
const [isExpanded, setIsExpanded] = useState(showThinking);
|
||||||
const [isContentHovered, setIsContentHovered] = useState(false);
|
|
||||||
|
|
||||||
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsExpanded((prev) => !prev);
|
setIsExpanded((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleContentEnter = useCallback(() => setIsContentHovered(true), []);
|
|
||||||
const handleContentLeave = useCallback(() => setIsContentHovered(false), []);
|
|
||||||
|
|
||||||
const label = useMemo(() => localize('com_ui_thoughts'), [localize]);
|
const label = useMemo(() => localize('com_ui_thoughts'), [localize]);
|
||||||
|
|
||||||
// Extract text content for copy functionality
|
// Extract text content for copy functionality
|
||||||
|
|
@ -157,14 +142,13 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseEnter={handleContentEnter} onMouseLeave={handleContentLeave}>
|
<>
|
||||||
<div className="sticky top-0 z-10 mb-4 bg-surface-primary pb-2 pt-2">
|
<div className="sticky top-0 z-10 mb-4 bg-surface-primary pb-2 pt-2">
|
||||||
<ThinkingButton
|
<ThinkingButton
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
label={label}
|
label={label}
|
||||||
content={textContent}
|
content={textContent}
|
||||||
isContentHovered={isContentHovered}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -177,7 +161,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
|
||||||
<ThinkingContent>{children}</ThinkingContent>
|
<ThinkingContent>{children}</ThinkingContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue