2025-01-29 19:46:58 -05:00
|
|
|
import { useState, useMemo, memo, useCallback } from 'react';
|
|
|
|
|
import { useRecoilValue } from 'recoil';
|
2025-01-24 10:52:08 -05:00
|
|
|
import { Atom, ChevronDown } from 'lucide-react';
|
2025-01-29 19:46:58 -05:00
|
|
|
import type { MouseEvent, FC } from 'react';
|
|
|
|
|
import { useLocalize } from '~/hooks';
|
|
|
|
|
import store from '~/store';
|
2025-01-24 10:52:08 -05:00
|
|
|
|
2025-01-29 19:46:58 -05:00
|
|
|
const BUTTON_STYLES = {
|
|
|
|
|
base: 'group mt-3 flex w-fit items-center justify-center rounded-xl bg-surface-tertiary px-3 py-2 text-xs leading-[18px] animate-thinking-appear',
|
|
|
|
|
icon: 'icon-sm ml-1.5 transform-gpu text-text-primary transition-transform duration-200',
|
|
|
|
|
} as const;
|
2025-01-24 10:52:08 -05:00
|
|
|
|
2025-01-29 19:46:58 -05:00
|
|
|
const CONTENT_STYLES = {
|
|
|
|
|
wrapper: 'relative pl-3 text-text-secondary',
|
|
|
|
|
border:
|
|
|
|
|
'absolute left-0 h-[calc(100%-10px)] border-l-2 border-border-medium dark:border-border-heavy',
|
|
|
|
|
partBorder:
|
|
|
|
|
'absolute left-0 h-[calc(100%)] border-l-2 border-border-medium dark:border-border-heavy',
|
|
|
|
|
text: 'whitespace-pre-wrap leading-[26px]',
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
export const ThinkingContent: FC<{ children: React.ReactNode; isPart?: boolean }> = memo(
|
|
|
|
|
({ isPart, children }) => (
|
|
|
|
|
<div className={CONTENT_STYLES.wrapper}>
|
|
|
|
|
<div className={isPart === true ? CONTENT_STYLES.partBorder : CONTENT_STYLES.border} />
|
|
|
|
|
<p className={CONTENT_STYLES.text}>{children}</p>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const ThinkingButton = memo(
|
|
|
|
|
({
|
|
|
|
|
isExpanded,
|
|
|
|
|
onClick,
|
|
|
|
|
label,
|
|
|
|
|
}: {
|
|
|
|
|
isExpanded: boolean;
|
|
|
|
|
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
|
|
|
label: string;
|
|
|
|
|
}) => (
|
|
|
|
|
<button type="button" onClick={onClick} className={BUTTON_STYLES.base}>
|
|
|
|
|
<Atom size={14} className="mr-1.5 text-text-secondary" />
|
|
|
|
|
{label}
|
|
|
|
|
<ChevronDown className={`${BUTTON_STYLES.icon} ${isExpanded ? 'rotate-180' : ''}`} />
|
|
|
|
|
</button>
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const Thinking: React.ElementType = memo(({ children }: { children: React.ReactNode }) => {
|
2025-01-24 10:52:08 -05:00
|
|
|
const localize = useLocalize();
|
2025-01-29 19:46:58 -05:00
|
|
|
const showThinking = useRecoilValue<boolean>(store.showThinking);
|
|
|
|
|
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-01-24 18:15:47 -05:00
|
|
|
if (children == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 10:52:08 -05:00
|
|
|
return (
|
|
|
|
|
<div className="mb-3">
|
2025-01-29 19:46:58 -05:00
|
|
|
<ThinkingButton isExpanded={isExpanded} onClick={handleClick} label={label} />
|
|
|
|
|
<div
|
|
|
|
|
className="grid transition-all duration-300 ease-out"
|
|
|
|
|
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">
|
|
|
|
|
<ThinkingContent>{children}</ThinkingContent>
|
2025-01-24 10:52:08 -05:00
|
|
|
</div>
|
2025-01-29 19:46:58 -05:00
|
|
|
</div>
|
2025-01-24 10:52:08 -05:00
|
|
|
</div>
|
|
|
|
|
);
|
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);
|