import { Fragment, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useDarkTheme } from '../hooks'; import Hero from '../Hero'; import { AppDispatch } from '../store'; import ConversationBubble from './ConversationBubble'; import { addQuery, fetchAnswer, selectQueries, selectStatus, updateQuery, } from './conversationSlice'; import Send from './../assets/send.svg'; import SendDark from './../assets/send_dark.svg'; import Spinner from './../assets/spinner.svg'; import SpinnerDark from './../assets/spinner-dark.svg'; import { FEEDBACK, Query } from './conversationModels'; import { sendFeedback } from './conversationApi'; import { useTranslation } from 'react-i18next'; import ArrowDown from './../assets/arrow-down.svg'; import RetryIcon from '../components/RetryIcon'; export default function Conversation() { const queries = useSelector(selectQueries); const status = useSelector(selectStatus); const dispatch = useDispatch(); const endMessageRef = 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 { t } = useTranslation(); const handleUserInterruption = () => { if (!eventInterrupt && status === 'loading') setEventInterrupt(true); }; useEffect(() => { !eventInterrupt && scrollIntoView(); }, [queries.length, queries[queries.length - 1]]); useEffect(() => { const element = document.getElementById('inputbox') as HTMLInputElement; if (element) { element.focus(); } }, []); useEffect(() => { return () => { if (status !== 'idle') { fetchStream.current && fetchStream.current.abort(); //abort previous stream } }; }, [status]); useEffect(() => { const observerCallback: IntersectionObserverCallback = (entries) => { entries.forEach((entry) => { setHasScrolledToLast(entry.isIntersecting); }); }; const observer = new IntersectionObserver(observerCallback, { root: null, threshold: [1, 0.8], }); if (endMessageRef.current) { observer.observe(endMessageRef.current); } return () => { observer.disconnect(); }; }, [endMessageRef.current]); 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 = () => { endMessageRef?.current?.scrollIntoView({ behavior: 'smooth', block: 'start', }); }; const handleQuestion = ({ question, isRetry = false, }: { question: string; isRetry?: boolean; }) => { 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 } })); sendFeedback(query.prompt, query.response!, feedback).catch(() => dispatch(updateQuery({ index, query: { feedback: prevFeedback } })), ); }; const handleQuestionSubmission = () => { if (inputRef.current?.textContent && status !== 'loading') { if (lastQueryReturnedErr) { // update last failed query with new prompt dispatch( updateQuery({ index: queries.length - 1, query: { prompt: inputRef.current.textContent, }, }), ); handleQuestion({ question: queries[queries.length - 1].prompt, isRetry: true, }); } else { handleQuestion({ question: inputRef.current.textContent }); } inputRef.current.textContent = ''; } }; 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 handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); document.execCommand('insertText', false, text); }; return (
{queries.length > 0 && !hasScrolledToLast && ( )} {queries.length > 0 && (
{queries.map((query, index) => { return ( {prepResponseView(query, index)} ); })}
)} {queries.length === 0 && }
{ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleQuestionSubmission(); } }} >
{status === 'loading' ? ( ) : (
)}

{t('tagline')}

); }