import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { Check, X } from 'lucide-react'; import { useParams } from 'react-router-dom'; import { Constants } from 'librechat-data-provider'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; import type { TConversation } from 'librechat-data-provider'; import { useConversations, useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks'; import { useUpdateConversationMutation } from '~/data-provider'; import EndpointIcon from '~/components/Endpoints/EndpointIcon'; import { NotificationSeverity } from '~/common'; import { useToastContext } from '~/Providers'; import { ConvoOptions } from './ConvoOptions'; import { cn } from '~/utils'; import store from '~/store'; type KeyEvent = KeyboardEvent; type ConversationProps = { conversation: TConversation; retainView: () => void; toggleNav: () => void; isLatestConvo: boolean; }; export default function Conversation({ conversation, retainView, toggleNav, isLatestConvo, }: ConversationProps) { const params = useParams(); const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]); const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? ''); const activeConvos = useRecoilValue(store.allConversationsSelector); const { data: endpointsConfig } = useGetEndpointsQuery(); const { navigateWithLastTools } = useNavigateToConvo(); const { refreshConversations } = useConversations(); const { showToast } = useToastContext(); const { conversationId, title } = conversation; const inputRef = useRef(null); const [titleInput, setTitleInput] = useState(title); const [renaming, setRenaming] = useState(false); const [isPopoverActive, setIsPopoverActive] = useState(false); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const localize = useLocalize(); const clickHandler = async (event: MouseEvent) => { if (event.button === 0 && (event.ctrlKey || event.metaKey)) { toggleNav(); return; } event.preventDefault(); if (currentConvoId === conversationId || isPopoverActive) { return; } toggleNav(); // set document title if (typeof title === 'string' && title.length > 0) { document.title = title; } /* Note: Latest Message should not be reset if existing convo */ navigateWithLastTools( conversation, !(conversationId ?? '') || conversationId === Constants.NEW_CONVO, ); }; const renameHandler = useCallback(() => { setIsPopoverActive(false); setTitleInput(title); setRenaming(true); }, [title]); useEffect(() => { if (renaming && inputRef.current) { inputRef.current.focus(); } }, [renaming]); const onRename = useCallback( (e: MouseEvent | FocusEvent | KeyEvent) => { e.preventDefault(); setRenaming(false); if (titleInput === title) { return; } if (typeof conversationId !== 'string' || conversationId === '') { return; } updateConvoMutation.mutate( { conversationId, title: titleInput ?? '' }, { onSuccess: () => refreshConversations(), onError: () => { setTitleInput(title); showToast({ message: 'Failed to rename conversation', severity: NotificationSeverity.ERROR, showIcon: true, }); }, }, ); }, [title, titleInput, conversationId, showToast, refreshConversations, updateConvoMutation], ); const handleKeyDown = useCallback( (e: KeyEvent) => { if (e.key === 'Escape') { setTitleInput(title); setRenaming(false); } else if (e.key === 'Enter') { onRename(e); } }, [title, onRename], ); const cancelRename = useCallback( (e: MouseEvent) => { e.preventDefault(); setTitleInput(title); setRenaming(false); }, [title], ); const isActiveConvo: boolean = useMemo( () => currentConvoId === conversationId || (isLatestConvo && currentConvoId === 'new' && activeConvos[0] != null && activeConvos[0] !== 'new'), [currentConvoId, conversationId, isLatestConvo, activeConvos], ); return (
{renaming ? (
setTitleInput(e.target.value)} onKeyDown={handleKeyDown} aria-label={`${localize('com_ui_rename')} ${localize('com_ui_chat')}`} />
) : (
{title}
{isActiveConvo ? (