2025-01-29 19:46:58 -05:00
|
|
|
import { useState, useMemo, memo, useCallback } from 'react';
|
2025-10-30 22:14:38 +01:00
|
|
|
import { useAtomValue } from 'jotai';
|
|
|
|
|
import { Lightbulb, ChevronDown } from 'lucide-react';
|
|
|
|
|
import { Clipboard, CheckMark } from '@librechat/client';
|
2025-01-29 19:46:58 -05:00
|
|
|
import type { MouseEvent, FC } from 'react';
|
2025-10-30 22:14:38 +01:00
|
|
|
import { showThinkingAtom } from '~/store/showThinking';
|
|
|
|
|
import { fontSizeAtom } from '~/store/fontSize';
|
2025-01-29 19:46:58 -05:00
|
|
|
import { useLocalize } from '~/hooks';
|
2025-01-30 12:36:35 -05:00
|
|
|
import { cn } from '~/utils';
|
2025-10-30 22:14:38 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ThinkingContent - Displays the actual thinking/reasoning content
|
|
|
|
|
* Used by both legacy text-based messages and modern content parts
|
|
|
|
|
*/
|
|
|
|
|
export const ThinkingContent: FC<{
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}> = memo(({ children }) => {
|
|
|
|
|
const fontSize = useAtomValue(fontSizeAtom);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative rounded-3xl border border-border-medium bg-surface-tertiary p-4 text-text-secondary">
|
|
|
|
|
<p className={cn('whitespace-pre-wrap leading-[26px]', fontSize)}>{children}</p>
|
2025-01-29 19:46:58 -05:00
|
|
|
</div>
|
2025-10-30 22:14:38 +01:00
|
|
|
);
|
|
|
|
|
});
|
2025-01-29 19:46:58 -05:00
|
|
|
|
2025-10-30 22:14:38 +01:00
|
|
|
/**
|
|
|
|
|
* ThinkingButton - Toggle button for expanding/collapsing thinking content
|
|
|
|
|
* Shows lightbulb icon by default, chevron on hover
|
|
|
|
|
* Shared between legacy Thinking component and modern ContentParts
|
|
|
|
|
*/
|
2025-01-29 19:46:58 -05:00
|
|
|
export const ThinkingButton = memo(
|
|
|
|
|
({
|
|
|
|
|
isExpanded,
|
|
|
|
|
onClick,
|
|
|
|
|
label,
|
2025-10-30 22:14:38 +01:00
|
|
|
content,
|
2025-01-29 19:46:58 -05:00
|
|
|
}: {
|
|
|
|
|
isExpanded: boolean;
|
|
|
|
|
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
|
|
|
label: string;
|
2025-10-30 22:14:38 +01:00
|
|
|
content?: string;
|
|
|
|
|
}) => {
|
|
|
|
|
const localize = useLocalize();
|
|
|
|
|
const fontSize = useAtomValue(fontSizeAtom);
|
|
|
|
|
|
|
|
|
|
const [isCopied, setIsCopied] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleCopy = useCallback(
|
|
|
|
|
(e: MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (content) {
|
|
|
|
|
navigator.clipboard.writeText(content);
|
|
|
|
|
setIsCopied(true);
|
|
|
|
|
setTimeout(() => setIsCopied(false), 2000);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[content],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex w-full items-center justify-between gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
className={cn(
|
2025-10-31 13:05:12 -04:00
|
|
|
'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]',
|
2025-10-30 22:14:38 +01:00
|
|
|
fontSize,
|
|
|
|
|
)}
|
|
|
|
|
>
|
2025-10-31 13:05:12 -04:00
|
|
|
<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" />
|
2025-10-30 22:14:38 +01:00
|
|
|
<ChevronDown
|
|
|
|
|
className={cn(
|
2025-10-31 13:05:12 -04:00
|
|
|
'icon-sm absolute transform-gpu text-text-primary opacity-0 transition-all duration-300 group-hover/button:opacity-100',
|
2025-10-30 22:14:38 +01:00
|
|
|
isExpanded && 'rotate-180',
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-10-31 13:05:12 -04:00
|
|
|
</span>
|
2025-10-30 22:14:38 +01:00
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
{content && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleCopy}
|
|
|
|
|
title={
|
|
|
|
|
isCopied
|
|
|
|
|
? localize('com_ui_copied_to_clipboard')
|
|
|
|
|
: localize('com_ui_copy_thoughts_to_clipboard')
|
|
|
|
|
}
|
|
|
|
|
className={cn(
|
|
|
|
|
'rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
|
|
|
|
|
'hover:bg-surface-hover hover:text-text-primary',
|
|
|
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-01-29 19:46:58 -05:00
|
|
|
);
|
|
|
|
|
|
2025-10-30 22:14:38 +01:00
|
|
|
/**
|
|
|
|
|
* Thinking Component (LEGACY SYSTEM)
|
|
|
|
|
*
|
|
|
|
|
* Used for simple text-based messages with `:::thinking:::` markers.
|
|
|
|
|
* This handles the old message format where text contains embedded thinking blocks.
|
|
|
|
|
*
|
|
|
|
|
* Pattern: `:::thinking\n{content}\n:::\n{response}`
|
|
|
|
|
*
|
|
|
|
|
* Used by:
|
|
|
|
|
* - MessageContent.tsx for plain text messages
|
|
|
|
|
* - Legacy message format compatibility
|
|
|
|
|
* - User messages when manually adding thinking content
|
|
|
|
|
*
|
|
|
|
|
* For modern structured content (agents/assistants), see Reasoning.tsx component.
|
|
|
|
|
*/
|
2025-01-29 19:46:58 -05:00
|
|
|
const Thinking: React.ElementType = memo(({ children }: { children: React.ReactNode }) => {
|
2025-01-24 10:52:08 -05:00
|
|
|
const localize = useLocalize();
|
2025-10-30 22:14:38 +01:00
|
|
|
const showThinking = useAtomValue(showThinkingAtom);
|
2025-01-29 19:46:58 -05:00
|
|
|
const [isExpanded, setIsExpanded] = useState(showThinking);
|
2025-01-24 10:52:08 -05:00
|
|
|
|
2025-01-29 19:46:58 -05:00
|
|
|
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
2025-01-24 10:52:08 -05:00
|
|
|
e.preventDefault();
|
2025-01-29 19:46:58 -05:00
|
|
|
setIsExpanded((prev) => !prev);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const label = useMemo(() => localize('com_ui_thoughts'), [localize]);
|
2025-01-24 10:52:08 -05:00
|
|
|
|
2025-10-30 22:14:38 +01:00
|
|
|
// Extract text content for copy functionality
|
|
|
|
|
const textContent = useMemo(() => {
|
|
|
|
|
if (typeof children === 'string') {
|
|
|
|
|
return children;
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}, [children]);
|
|
|
|
|
|
2025-01-24 18:15:47 -05:00
|
|
|
if (children == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 10:52:08 -05:00
|
|
|
return (
|
2025-10-31 13:05:12 -04:00
|
|
|
<>
|
2025-10-30 22:14:38 +01:00
|
|
|
<div className="sticky top-0 z-10 mb-4 bg-surface-primary pb-2 pt-2">
|
|
|
|
|
<ThinkingButton
|
|
|
|
|
isExpanded={isExpanded}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
label={label}
|
|
|
|
|
content={textContent}
|
|
|
|
|
/>
|
2025-01-30 12:36:35 -05:00
|
|
|
</div>
|
2025-01-29 19:46:58 -05:00
|
|
|
<div
|
2025-01-30 12:36:35 -05:00
|
|
|
className={cn('grid transition-all duration-300 ease-out', isExpanded && 'mb-8')}
|
2025-01-29 19:46:58 -05:00
|
|
|
style={{
|
|
|
|
|
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
|
|
|
|
}}
|
2025-01-24 10:52:08 -05:00
|
|
|
>
|
2025-01-29 19:46:58 -05:00
|
|
|
<div className="overflow-hidden">
|
2025-10-30 22:14:38 +01:00
|
|
|
<ThinkingContent>{children}</ThinkingContent>
|
2025-01-24 10:52:08 -05:00
|
|
|
</div>
|
2025-01-29 19:46:58 -05:00
|
|
|
</div>
|
2025-10-31 13:05:12 -04:00
|
|
|
</>
|
2025-01-24 10:52:08 -05:00
|
|
|
);
|
2025-01-29 19:46:58 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ThinkingButton.displayName = 'ThinkingButton';
|
|
|
|
|
ThinkingContent.displayName = 'ThinkingContent';
|
|
|
|
|
Thinking.displayName = 'Thinking';
|
2025-01-24 10:52:08 -05:00
|
|
|
|
2025-01-29 19:46:58 -05:00
|
|
|
export default memo(Thinking);
|