|
import { Fragment, useEffect, useRef, useState } from 'react'; |
|
import { useDispatch, useSelector } from 'react-redux'; |
|
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 { FEEDBACK, Query } from './conversationModels'; |
|
import { sendFeedback } from './conversationApi'; |
|
import ArrowDown from './../assets/arrow-down.svg'; |
|
|
|
export default function Conversation() { |
|
const queries = useSelector(selectQueries); |
|
const status = useSelector(selectStatus); |
|
const dispatch = useDispatch<AppDispatch>(); |
|
const endMessageRef = useRef<HTMLDivElement>(null); |
|
const inputRef = useRef<HTMLDivElement>(null); |
|
const isDarkTheme = document.documentElement.classList.contains('dark'); |
|
const [hasScrolledToLast, setHasScrolledToLast] = useState(true); |
|
|
|
useEffect(() => { |
|
scrollIntoView(); |
|
}, [queries.length, queries[queries.length - 1]]); |
|
|
|
useEffect(() => { |
|
const element = document.getElementById('inputbox') as HTMLInputElement; |
|
if (element) { |
|
element.focus(); |
|
} |
|
}, []); |
|
|
|
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]); |
|
|
|
const scrollIntoView = () => { |
|
endMessageRef?.current?.scrollIntoView({ |
|
behavior: 'smooth', |
|
block: 'start', |
|
}); |
|
}; |
|
|
|
const handleQuestion = (question: string) => { |
|
question = question.trim(); |
|
if (question === '') return; |
|
dispatch(addQuery({ prompt: question })); |
|
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 prepResponseView = (query: Query, index: number) => { |
|
let responseView; |
|
if (query.error) { |
|
responseView = ( |
|
<ConversationBubble |
|
ref={endMessageRef} |
|
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`} |
|
key={`${index}ERROR`} |
|
message={query.error} |
|
type="ERROR" |
|
></ConversationBubble> |
|
); |
|
} else if (query.response) { |
|
responseView = ( |
|
<ConversationBubble |
|
ref={endMessageRef} |
|
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`} |
|
key={`${index}ANSWER`} |
|
message={query.response} |
|
type={'ANSWER'} |
|
sources={query.sources} |
|
feedback={query.feedback} |
|
handleFeedback={(feedback: FEEDBACK) => |
|
handleFeedback(query, feedback, index) |
|
} |
|
></ConversationBubble> |
|
); |
|
} |
|
return responseView; |
|
}; |
|
|
|
const handlePaste = (e: React.ClipboardEvent) => { |
|
e.preventDefault(); |
|
const text = e.clipboardData.getData('text/plain'); |
|
document.execCommand('insertText', false, text); |
|
}; |
|
|
|
return ( |
|
<div className="flex flex-col justify-center p-4 md:flex-row"> |
|
{queries.length > 0 && !hasScrolledToLast && ( |
|
<button |
|
onClick={scrollIntoView} |
|
aria-label="scroll to bottom" |
|
className="fixed bottom-32 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 dark:bg-purple-taupe bg-opacity-50 md:h-9 md:w-9 md:bg-opacity-100 " |
|
> |
|
<img |
|
src={ArrowDown} |
|
alt="arrow down" |
|
className="h4- w-4 opacity-50 md:h-5 md:w-5" |
|
/> |
|
</button> |
|
)} |
|
|
|
{queries.length > 0 && ( |
|
<div className="mt-20 flex flex-col transition-all md:w-3/4"> |
|
{queries.map((query, index) => { |
|
return ( |
|
<Fragment key={index}> |
|
<ConversationBubble |
|
className={'last:mb-27 mb-7'} |
|
key={`${index}QUESTION`} |
|
message={query.prompt} |
|
type="QUESTION" |
|
sources={query.sources} |
|
></ConversationBubble> |
|
{prepResponseView(query, index)} |
|
</Fragment> |
|
); |
|
})} |
|
</div> |
|
)} |
|
{queries.length === 0 && ( |
|
<Hero className="mt-24 h-[100vh] md:mt-52"></Hero> |
|
)} |
|
<div className="relative bottom-0 flex w-10/12 flex-col items-end self-center bg-white dark:bg-raisin-black pt-3 md:fixed md:w-[65%]"> |
|
<div className="flex h-full w-full"> |
|
<div |
|
id="inputbox" |
|
ref={inputRef} |
|
tabIndex={1} |
|
placeholder="Type your message here..." |
|
contentEditable |
|
onPaste={handlePaste} |
|
className={`border-000000 overflow-x-hidden; max-h-24 min-h-[2.6rem] w-full overflow-y-auto whitespace-pre-wrap rounded-3xl border bg-white dark:bg-transparent dark:text-bright-gray py-2 pl-4 pr-9 text-base leading-7 opacity-100 focus:outline-none`} |
|
onKeyDown={(e) => { |
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
e.preventDefault(); |
|
if (inputRef.current?.textContent && status !== 'loading') { |
|
handleQuestion(inputRef.current.textContent); |
|
inputRef.current.textContent = ''; |
|
} |
|
} |
|
}} |
|
></div> |
|
{status === 'loading' ? ( |
|
<img |
|
src={Spinner} |
|
className="relative right-[38px] bottom-[7px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent" |
|
></img> |
|
) : ( |
|
<div className="relative right-[43px] bottom-[7px] -mr-[35px] h-[35px] w-[35px] cursor-pointer self-end rounded-full hover:bg-gray-3000"> |
|
<img |
|
className="ml-[9px] mt-[9px] text-white" |
|
onClick={() => { |
|
if (inputRef.current?.textContent) { |
|
handleQuestion(inputRef.current.textContent); |
|
inputRef.current.textContent = ''; |
|
} |
|
}} |
|
src={isDarkTheme ? SendDark : Send} |
|
></img> |
|
</div> |
|
)} |
|
</div> |
|
<p className="text-gray-595959 dark:text-bright-gray w-[100vw] self-center bg-transparent p-5 text-center text-xs md:w-full"> |
|
This is a chatbot that uses the GPT-3, Faiss and LangChain to answer |
|
questions. |
|
</p> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|