diff --git a/frontend/src/components/Input.tsx b/frontend/src/components/Input.tsx index 39dff2b0..3fd84bdc 100644 --- a/frontend/src/components/Input.tsx +++ b/frontend/src/components/Input.tsx @@ -35,7 +35,7 @@ const Input = ({
(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(); @@ -91,16 +86,6 @@ export default function Conversation() { }, }); - 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) { @@ -115,19 +100,6 @@ export default function Conversation() { } }, [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, @@ -146,7 +118,6 @@ export default function Conversation() { } else { question = question.trim(); if (question === '') return; - setEventInterrupt(false); !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries fetchStream.current = dispatch(fetchAnswer({ question })); } @@ -215,57 +186,6 @@ export default function Conversation() { 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'; @@ -276,23 +196,9 @@ export default function Conversation() { )}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 && (
@@ -327,64 +233,23 @@ export default function Conversation() {
{isShareModalOpen && ( -
-
-
- { - setShareModalState(false); - }} - conversationId={conversationId} - /> -
-
+ { + setShareModalState(false); + }} + conversationId={conversationId} + /> )}
)} -
- {queries.length > 0 && !hasScrolledToLast && ( - - )} - {queries.length > 0 ? ( -
- {queries.map((query, index) => { - return ( - - - - {prepResponseView(query, index)} - - ); - })} -
- ) : ( - - )} -
+
void; + handleQuestionSubmission: ( + updatedQuestion?: string, + updated?: boolean, + indx?: number, + ) => void; + handleFeedback?: (query: Query, feedback: FEEDBACK, index: number) => void; + queries: Query[]; + status: Status; +} + +export default function ConversationMessages({ + handleQuestion, + handleQuestionSubmission, + queries, + status, + handleFeedback, +}: ConversationMessagesProps) { + const [isDarkTheme] = useDarkTheme(); + const { t } = useTranslation(); + + const conversationRef = useRef(null); + const [hasScrolledToLast, setHasScrolledToLast] = useState(true); + const [eventInterrupt, setEventInterrupt] = useState(false); + + const handleUserInterruption = () => { + if (!eventInterrupt && status === 'loading') { + setEventInterrupt(true); + } + }; + + 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 checkScroll = () => { + const el = conversationRef.current; + if (!el) return; + const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10; + setHasScrolledToLast(isBottom); + }; + + useEffect(() => { + !eventInterrupt && scrollIntoView(); + }, [queries.length, queries[queries.length - 1]]); + + useEffect(() => { + if (status === 'idle') { + setEventInterrupt(false); + } + }, [status]); + + useEffect(() => { + conversationRef.current?.addEventListener('scroll', checkScroll); + return () => { + conversationRef.current?.removeEventListener('scroll', checkScroll); + }; + }, []); + + const prepResponseView = (query: Query, index: number) => { + let responseView; + if (query.response) { + responseView = ( + handleFeedback(query, feedback, index) + : undefined + } + /> + ); + } else if (query.error) { + const retryBtn = ( + + ); + responseView = ( + + ); + } + return responseView; + }; + + return ( +
+ {queries.length > 0 && !hasScrolledToLast && ( + + )} + + {queries.length > 0 ? ( +
+ {queries.map((query, index) => ( + + + {prepResponseView(query, index)} + + ))} +
+ ) : ( + + )} +
+ ); +} diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index b231ed1e..e72f90da 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,19 +1,18 @@ -import { Query } from './conversationModels'; -import { Fragment, useEffect, useRef, useState } from 'react'; +import { Fragment, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; +import { useDarkTheme } from '../hooks'; import conversationService from '../api/services/conversationService'; -import ConversationBubble from './ConversationBubble'; +import ConversationMessages from './ConversationMessages'; 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 { - selectClientAPIKey, setClientApiKey, - updateQuery, addQuery, fetchSharedAnswer, - selectStatus, } from './sharedConversationSlice'; import { setIdentifier, setFetchedData } from './sharedConversationSlice'; @@ -24,6 +23,8 @@ import { selectDate, selectTitle, selectQueries, + selectClientAPIKey, + selectStatus, } from './sharedConversationSlice'; import { useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; @@ -31,57 +32,33 @@ import { Helmet } from 'react-helmet'; export const SharedConversation = () => { const navigate = useNavigate(); const { identifier } = useParams(); //identifier is a uuid, not conversationId + const inputRef = useRef(null); + const [isDarkTheme] = useDarkTheme(); const queries = useSelector(selectQueries); const title = useSelector(selectTitle); const date = useSelector(selectDate); const apiKey = useSelector(selectClientAPIKey); const status = useSelector(selectStatus); - - const inputRef = useRef(null); - const sharedConversationRef = useRef(null); const { t } = useTranslation(); const dispatch = useDispatch(); - const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false); - const [eventInterrupt, setEventInterrupt] = useState(false); - const endMessageRef = useRef(null); - const handleUserInterruption = () => { - if (!eventInterrupt && status === 'loading') setEventInterrupt(true); + 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`; + } }; - useEffect(() => { - !eventInterrupt && scrollIntoView(); - }, [queries.length, queries[queries.length - 1]]); useEffect(() => { identifier && dispatch(setIdentifier(identifier)); - const element = document.getElementById('inputbox') as HTMLInputElement; - if (element) { - element.focus(); - } + fetchQueries(); }, []); - 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 (!sharedConversationRef?.current || eventInterrupt) return; - - if (status === 'idle' || !queries[queries.length - 1].response) { - sharedConversationRef.current.scrollTo({ - behavior: 'smooth', - top: sharedConversationRef.current.scrollHeight, - }); - } else { - sharedConversationRef.current.scrollTop = - sharedConversationRef.current.scrollHeight; - } - }; - const fetchQueries = () => { identifier && conversationService @@ -105,60 +82,6 @@ export const SharedConversation = () => { } }); }; - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault(); - const text = e.clipboardData.getData('text/plain'); - inputRef.current && (inputRef.current.innerText = text); - }; - const prepResponseView = (query: Query, index: number) => { - let responseView; - if (query.response) { - responseView = ( - - ); - } else if (query.error) { - responseView = ( - - ); - } - return responseView; - }; - 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 handleQuestion = ({ question, @@ -169,13 +92,19 @@ export const SharedConversation = () => { }) => { question = question.trim(); if (question === '') return; - setEventInterrupt(false); - !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries + !isRetry && dispatch(addQuery({ prompt: question })); dispatch(fetchSharedAnswer({ question })); }; - useEffect(() => { - fetchQueries(); - }, []); + + const handleQuestionSubmission = ( + updatedQuestion?: string, + updated?: boolean, + indx?: number, + ) => { + if (updatedQuestion && status !== 'loading') { + handleQuestion({ question: updatedQuestion }); + } + }; return ( <> @@ -194,94 +123,105 @@ export const SharedConversation = () => { content="Shared conversations with DocsGPT" /> -
-
-
-
-

- {title} -

-

- {t('sharedConv.subtitle')}{' '} - - DocsGPT - -

-

- {date} -

-
-
- {queries?.map((query, index) => { - return ( - - - {prepResponseView(query, index)} - - ); - })} -
+
+ {/* Header section */} +
+
+

+ {title} +

+

+ {t('sharedConv.subtitle')}{' '} + + DocsGPT + +

+

+ {date} +

-
- {apiKey ? ( -
-
{ - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleQuestionSubmission(); - } - }} - >
- {status === 'loading' ? ( - - ) : ( -
- -
- )} -
- ) : ( + {/* Conditionally render based on API key */} + {!apiKey ? ( +
- )} - - {t('sharedConv.meta')} - -
+ + {t('sharedConv.meta')} + +
+ ) : ( + <> + + + {/* Add the textarea input here */} +
+
+
+ +