import { Fragment, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import Hero from '../Hero'; import ArrowDown from '../assets/arrow-down.svg'; import newChatIcon from '../assets/openNewChat.svg'; import Send from '../assets/send.svg'; import SendDark from '../assets/send_dark.svg'; import ShareIcon from '../assets/share.svg'; import SpinnerDark from '../assets/spinner-dark.svg'; import Spinner from '../assets/spinner.svg'; import RetryIcon from '../components/RetryIcon'; import { useDarkTheme, useMediaQuery } from '../hooks'; import { ShareConversationModal } from '../modals/ShareConversationModal'; import { selectConversationId } from '../preferences/preferenceSlice'; import { AppDispatch } from '../store'; import conversationService from '../api/services/conversationService'; import ConversationBubble from './ConversationBubble'; import { handleSendFeedback } from './conversationHandlers'; import { FEEDBACK, Query } from './conversationModels'; import { addQuery, fetchAnswer, resendQuery, selectQueries, selectStatus, setConversation, updateConversationId, updateQuery, } from './conversationSlice'; export default function Conversation() { const queries = useSelector(selectQueries); const navigate = useNavigate(); const status = useSelector(selectStatus); const conversationId = useSelector(selectConversationId); const dispatch = useDispatch(); const conversationRef = useRef(null); const inputRef = useRef(null); const [isDarkTheme] = useDarkTheme(); const [hasScrolledToLast, setHasScrolledToLast] = useState(true); const fetchStream = useRef(null); const [eventInterrupt, setEventInterrupt] = useState(false); const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false); const [isShareModalOpen, setShareModalState] = useState(false); const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const handleUserInterruption = () => { if (!eventInterrupt && status === 'loading') setEventInterrupt(true); }; useEffect(() => { !eventInterrupt && scrollIntoView(); if (queries.length == 0) { resetConversation(); } }, [queries.length, queries[queries.length - 1]]); useEffect(() => { const element = document.getElementById('inputbox') as HTMLTextAreaElement; if (element) { element.focus(); } }, []); useEffect(() => { if (queries.length) { queries[queries.length - 1].error && setLastQueryReturnedErr(true); queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry } }, [queries[queries.length - 1]]); const scrollIntoView = () => { if (!conversationRef?.current || eventInterrupt) return; if (status === 'idle' || !queries[queries.length - 1].response) { conversationRef.current.scrollTo({ behavior: 'smooth', top: conversationRef.current.scrollHeight, }); } else { conversationRef.current.scrollTop = conversationRef.current.scrollHeight; } }; const handleQuestion = ({ question, isRetry = false, updated = null, indx = undefined, }: { question: string; isRetry?: boolean; updated?: boolean | null; indx?: number; }) => { if (updated === true) { !isRetry && dispatch(resendQuery({ index: indx as number, prompt: question })); //dispatch only new queries fetchStream.current = dispatch(fetchAnswer({ question, indx })); } else { question = question.trim(); if (question === '') return; setEventInterrupt(false); !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries fetchStream.current = dispatch(fetchAnswer({ question })); } }; const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => { const prevFeedback = query.feedback; dispatch(updateQuery({ index, query: { feedback } })); handleSendFeedback(query.prompt, query.response!, feedback).catch(() => dispatch(updateQuery({ index, query: { feedback: prevFeedback } })), ); }; const handleQuestionSubmission = ( updatedQuestion?: string, updated?: boolean, indx?: number, ) => { if (updated === true) { handleQuestion({ question: updatedQuestion as string, updated, indx }); } else if (inputRef.current?.value && status !== 'loading') { if (lastQueryReturnedErr) { // update last failed query with new prompt dispatch( updateQuery({ index: queries.length - 1, query: { prompt: inputRef.current.value, }, }), ); handleQuestion({ question: queries[queries.length - 1].prompt, isRetry: true, }); } else { handleQuestion({ question: inputRef.current.value }); } inputRef.current.value = ''; handleInput(); } }; const resetConversation = () => { dispatch(setConversation([])); dispatch( updateConversationId({ query: { conversationId: null }, }), ); }; const newChat = () => { if (queries && queries.length > 0) resetConversation(); }; const prepResponseView = (query: Query, index: number) => { let responseView; if (query.response) { responseView = ( handleFeedback(query, feedback, index) } > ); } else if (query.error) { const retryBtn = ( ); responseView = ( ); } return responseView; }; const handleInput = () => { if (inputRef.current) { if (window.innerWidth < 350) inputRef.current.style.height = 'auto'; else inputRef.current.style.height = '64px'; inputRef.current.style.height = `${Math.min( inputRef.current.scrollHeight, 96, )}px`; } }; const checkScroll = () => { const el = conversationRef.current; if (!el) return; const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10; setHasScrolledToLast(isBottom); }; useEffect(() => { handleInput(); window.addEventListener('resize', handleInput); conversationRef.current?.addEventListener('scroll', checkScroll); return () => { window.removeEventListener('resize', handleInput); conversationRef.current?.removeEventListener('scroll', checkScroll); }; }, []); return (
{conversationId && queries.length > 0 && (
{' '}
{isMobile && queries.length > 0 && ( )}
{isShareModalOpen && ( { setShareModalState(false); }} conversationId={conversationId} /> )}
)}
{queries.length > 0 && !hasScrolledToLast && ( )} {queries.length > 0 ? (
{queries.map((query, index) => { return ( {prepResponseView(query, index)} ); })}
) : ( )}
{status === 'loading' ? ( ) : (
handleQuestionSubmission()} src={isDarkTheme ? SendDark : Send} >
)}

{t('tagline')}

); }