File size: 7,119 Bytes
fed832e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
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>
);
}
|