import React, { useState, useRef } from 'react'; import { useRecoilState } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { VisuallyHidden } from '@ariakit/react'; import { GitFork, InfoIcon } from 'lucide-react'; import { useToastContext } from '@librechat/client'; import { ForkOptions } from 'librechat-data-provider'; import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react'; import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks'; import { useForkConvoMutation } from '~/data-provider'; import { cn } from '~/utils'; import store from '~/store'; interface PopoverButtonProps { children: React.ReactNode; setting: ForkOptions; onClick: (setting: ForkOptions) => void; setActiveSetting: React.Dispatch>; timeoutRef: React.MutableRefObject; hoverInfo?: React.ReactNode | string; hoverTitle?: React.ReactNode | string; hoverDescription?: React.ReactNode | string; label: string; } const optionLabels: Record = { [ForkOptions.DIRECT_PATH]: 'com_ui_fork_visible', [ForkOptions.INCLUDE_BRANCHES]: 'com_ui_fork_branches', [ForkOptions.TARGET_LEVEL]: 'com_ui_fork_all_target', [ForkOptions.DEFAULT]: 'com_ui_fork_from_message', }; const chevronDown = ( ); const PopoverButton: React.FC = ({ children, setting, onClick, setActiveSetting, timeoutRef, hoverInfo, hoverTitle, hoverDescription, label, }) => { const localize = useLocalize(); return (
onClick(setting)} onMouseEnter={() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setActiveSetting(optionLabels[setting]); }} onMouseLeave={() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { setActiveSetting(optionLabels[ForkOptions.DEFAULT]); }, 175); }} className="mx-0.5 w-14 flex-1 rounded-xl border-2 border-border-medium bg-surface-secondary text-text-secondary transition duration-200 ease-in-out hover:bg-surface-hover hover:text-text-primary" aria-label={label} > {children} {label} } /> {localize('com_ui_fork_more_details_about', { 0: label })} {chevronDown} {((hoverInfo != null && hoverInfo !== '') || (hoverTitle != null && hoverTitle !== '') || (hoverDescription != null && hoverDescription !== '')) && (

{hoverInfo && hoverInfo} {hoverTitle && {hoverTitle}} {hoverDescription && hoverDescription}

)}
); }; interface CheckboxOptionProps { id: string; checked: boolean; onToggle: (checked: boolean) => void; labelKey: TranslationKeys; infoKey: TranslationKeys; showToastOnCheck?: boolean; } const CheckboxOption: React.FC = ({ id, checked, onToggle, labelKey, infoKey, showToastOnCheck = false, }) => { const localize = useLocalize(); const { showToast } = useToastContext(); return (
{ const value = e.target.checked; if (value && showToastOnCheck) { showToast({ message: localize('com_ui_fork_remember_checked'), status: 'info', }); } onToggle(value); }} className="h-4 w-4 rounded-sm border border-primary ring-offset-background transition duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground" aria-label={localize(labelKey)} />
} />
{localize(infoKey)} {chevronDown}

{localize(infoKey)}

); }; export default function Fork({ messageId, conversationId: _convoId, forkingSupported = false, latestMessageId, isLast = false, }: { messageId: string; conversationId: string | null; forkingSupported?: boolean; latestMessageId?: string; isLast?: boolean; }) { const localize = useLocalize(); const { showToast } = useToastContext(); const [remember, setRemember] = useState(false); const { navigateToConvo } = useNavigateToConvo(); const [isActive, setIsActive] = useState(false); const timeoutRef = useRef(null); const [forkSetting, setForkSetting] = useRecoilState(store.forkSetting); const [activeSetting, setActiveSetting] = useState(optionLabels.default); const [splitAtTarget, setSplitAtTarget] = useRecoilState(store.splitAtTarget); const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberDefaultFork); const popoverStore = Ariakit.usePopoverStore({ placement: 'bottom', }); const buttonStyle = cn( 'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200', 'hover:text-text-primary hover:bg-surface-hover', 'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible', !isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100', 'focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white focus-visible:outline-none', isActive && 'active text-text-primary bg-surface-hover', ); const forkConvo = useForkConvoMutation({ onSuccess: (data) => { navigateToConvo(data.conversation); showToast({ message: localize('com_ui_fork_success'), status: 'success', }); }, onMutate: () => { showToast({ message: localize('com_ui_fork_processing'), status: 'info', }); }, onError: (error) => { /** Rate limit error (429 status code) */ const isRateLimitError = (error as any)?.response?.status === 429 || (error as any)?.status === 429 || (error as any)?.statusCode === 429; showToast({ message: isRateLimitError ? localize('com_ui_fork_error_rate_limit') : localize('com_ui_fork_error'), status: 'error', }); }, }); const conversationId = _convoId ?? ''; if (!forkingSupported || !conversationId || !messageId) { return null; } const onClick = (option: string) => { if (remember) { setRememberGlobal(true); setForkSetting(option); } forkConvo.mutate({ messageId, conversationId, option, splitAtTarget, latestMessageId, }); }; const forkOptionsConfig = [ { setting: ForkOptions.DIRECT_PATH, label: localize(optionLabels[ForkOptions.DIRECT_PATH]), icon: