diff --git a/frontend/src/components/PopupSelector.tsx b/frontend/src/components/PopupSelector.tsx new file mode 100644 index 000000000..ae469c2b5 --- /dev/null +++ b/frontend/src/components/PopupSelector.tsx @@ -0,0 +1,54 @@ +import React, { useRef } from 'react'; +import { useOutsideAlerter } from '../hooks'; + +export function PopupSelector({ + isOpen = false, + setIsPopupOpen, + positionTop = 0, + positionLeft = 0, + headerText, + onSelect, + selectionItems, +}: TPopupSelector) { + const popupRef = useRef(null); + useOutsideAlerter(popupRef, () => setIsPopupOpen(false), [], true); + + return isOpen ? ( +
+ {headerText && ( +
+ {headerText} +
+ )} + {selectionItems.map((item, idx) => { + return ( +
onSelect(item)} + > + + {item.name} + +
+ ); + })} +
+ ) : ( +
+ ); +} + +type TPopupSelector = { + isOpen: boolean; + setIsPopupOpen: React.Dispatch>; + onSelect: (item: SelectionItem) => void; + selectionItems: SelectionItem[]; + positionTop: number; + positionLeft: number; + handleOutsideClick?: () => void; + headerText?: string; +}; diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 115abcd89..91bd0fda7 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -11,16 +11,12 @@ import { selectStatus, updateQuery, } from './conversationSlice'; -import { selectConversationId } from '../preferences/preferenceSlice'; -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'; +import { ConversationInputBox } from './ConversationInputBox'; import ShareIcon from '../assets/share.svg'; import { ShareConversationModal } from '../modals/ShareConversationModal'; @@ -266,37 +262,13 @@ export default function Conversation() {
-
-
{ - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleQuestionSubmission(); - } - }} - >
- {status === 'loading' ? ( - - ) : ( -
- -
- )} -
+

{t('tagline')} diff --git a/frontend/src/conversation/ConversationInputBox.tsx b/frontend/src/conversation/ConversationInputBox.tsx new file mode 100644 index 000000000..1379e4d2f --- /dev/null +++ b/frontend/src/conversation/ConversationInputBox.tsx @@ -0,0 +1,124 @@ +import React, { + ClipboardEvent, + KeyboardEvent, + useState, + RefObject, + useLayoutEffect, +} from 'react'; +import Spinner from './../assets/spinner.svg'; +import SpinnerDark from './../assets/spinner-dark.svg'; +import Send from './../assets/send.svg'; +import SendDark from './../assets/send_dark.svg'; +import { useTranslation } from 'react-i18next'; +import { PopupSelector } from '../components/PopupSelector'; +import { useDispatch, useSelector } from 'react-redux'; +import { + selectSelectedDocs, + selectSourceDocs, + setSelectedDocs, +} from '../preferences/preferenceSlice'; +import { Doc } from '../models/misc'; +import { ConversationSourceList } from './ConversationSourceList'; + +export function ConversationInputBox({ + inputRef, + onSubmit, + handlePaste, + isDarkTheme, + status, +}: TConversationInputBox) { + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + const docs = useSelector(selectSourceDocs); + const selectedDocs = useSelector(selectSelectedDocs); + const dispatch = useDispatch(); + const { t } = useTranslation(); + + useLayoutEffect(() => { + if (inputRef.current) { + const rect = inputRef.current.getBoundingClientRect(); + setPopupPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + }); + } + }, []); + + const onPopupSelection = (selectedDocument: Doc) => { + dispatch(setSelectedDocs(selectedDocument)); + setIsPopupOpen(false); + }; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey === true && e.key === 'd') { + setIsPopupOpen(!isPopupOpen); + } else { + setIsPopupOpen(false); + } + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + onSubmit(); + } + }; + + return ( +

+
+ +
+
+ +
+ {status === 'loading' ? ( + + ) : ( +
+ +
+ )} +
+
+ ); +} + +//TODO: There may be a bug where if page is loaded with "None" source docs selected then the initial value of +//selectedDocs in the global data store is an array. This case is not caught as a TS error this array is passed +//unexpectedly even though it has a type set to Doc. This type check is used to counteract this unexpected behavior in +//the mean time. +function isDoc(doc: Doc | unknown): doc is Doc { + return !!doc && (doc as Doc).name !== undefined; +} + +type TConversationInputBox = { + inputRef: RefObject; + handlePaste: (e: ClipboardEvent) => void; + onSubmit: () => void; + isDarkTheme?: boolean; + status: string; +}; diff --git a/frontend/src/conversation/ConversationSourceList.tsx b/frontend/src/conversation/ConversationSourceList.tsx new file mode 100644 index 000000000..304340725 --- /dev/null +++ b/frontend/src/conversation/ConversationSourceList.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Doc } from '../models/misc'; +import Exit from '../assets/exit.svg'; +import { useDispatch } from 'react-redux'; +import { setSelectedDocs } from '../preferences/preferenceSlice'; + +export function ConversationSourceList({ docs }: TConversationSourceListProps) { + const dispatch = useDispatch(); + return ( +
+ {docs && + docs.map((doc, idx) => { + return ( +
+ Remove dispatch(setSelectedDocs(null))} + /> + {doc.name} +
+ ); + })} +
+ ); +} + +type TConversationSourceListProps = { + docs: Doc[]; +}; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 0f5aa708a..9d27270d0 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -10,6 +10,7 @@ "sourceDocs": "Source Docs", "none": "None", "cancel": "Cancel", + "selectADocument": "Select A Document", "demo": [ { "header": "Learn about DocsGPT",