diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 7ffe6dc1..10eeda64 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -1,10 +1,16 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDarkTheme } from '../hooks'; +import { useSelector } from 'react-redux'; import PaperPlane from '../assets/paper_plane.svg'; import SourceIcon from '../assets/source.svg'; import SpinnerDark from '../assets/spinner-dark.svg'; import Spinner from '../assets/spinner.svg'; +import SourcesPopup from './SourcesPopup'; +import { selectSelectedDocs } from '../preferences/preferenceSlice'; +import { ActiveState } from '../models/misc'; +import Upload from '../upload/Upload'; +import ClipIcon from '../assets/clip.svg'; interface MessageInputProps { value: string; @@ -22,6 +28,12 @@ export default function MessageInput({ const { t } = useTranslation(); const [isDarkTheme] = useDarkTheme(); const inputRef = useRef(null); + const sourceButtonRef = useRef(null); + const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false); + const [uploadModalState, setUploadModalState] = + useState('INACTIVE'); + + const selectedDocs = useSelector(selectSelectedDocs); const handleInput = () => { if (inputRef.current) { @@ -50,6 +62,10 @@ export default function MessageInput({ } }; + const handlePostDocumentSelect = (doc: any) => { + console.log('Selected document:', doc); + }; + return (
@@ -71,14 +87,30 @@ export default function MessageInput({ />
-
+
- + + {/* Additional badges can be added here in the future */}
@@ -104,6 +136,24 @@ export default function MessageInput({
+ + setIsSourcesPopupOpen(false)} + anchorRef={sourceButtonRef} + handlePostDocumentSelect={handlePostDocumentSelect} + setUploadModalState={setUploadModalState} + /> + + {uploadModalState === 'ACTIVE' && ( + setUploadModalState('INACTIVE')} + /> + )}
); } diff --git a/frontend/src/components/SourcesPopup.tsx b/frontend/src/components/SourcesPopup.tsx new file mode 100644 index 00000000..3625f0a4 --- /dev/null +++ b/frontend/src/components/SourcesPopup.tsx @@ -0,0 +1,162 @@ +import React, { useRef, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { Doc } from '../models/misc'; +import SourceIcon from '../assets/source.svg'; +import CheckIcon from '../assets/checkmark.svg'; +import { + selectSourceDocs, + selectSelectedDocs, + setSelectedDocs, +} from '../preferences/preferenceSlice'; +import { ActiveState } from '../models/misc'; + +type SourcesPopupProps = { + isOpen: boolean; + onClose: () => void; + anchorRef: React.RefObject; + handlePostDocumentSelect: (doc: Doc | null) => void; + setUploadModalState: React.Dispatch>; +}; + +export default function SourcesPopup({ + isOpen, + onClose, + anchorRef, + handlePostDocumentSelect, + setUploadModalState, +}: SourcesPopupProps) { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const popupRef = useRef(null); + const embeddingsName = + import.meta.env.VITE_EMBEDDINGS_NAME || + 'huggingface_sentence-transformers/all-mpnet-base-v2'; + + const options = useSelector(selectSourceDocs); + const selectedDocs = useSelector(selectSelectedDocs); + + const getPopupPosition = () => { + if (!anchorRef.current) return {}; + + const rect = anchorRef.current.getBoundingClientRect(); + + return { + position: 'fixed' as const, + top: `${rect.top - 8}px`, + left: `${rect.left}px`, + minWidth: `${rect.width}px`, + transform: 'translateY(-100%)', + }; + }; + + const handleEmptyDocumentSelect = () => { + dispatch(setSelectedDocs(null)); + handlePostDocumentSelect(null); + onClose(); + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + popupRef.current && + !popupRef.current.contains(event.target as Node) && + !anchorRef.current?.contains(event.target as Node) + ) { + onClose(); + } + }; + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + if (!isOpen) return null; + + const handleUploadClick = () => { + setUploadModalState('ACTIVE'); + onClose(); + }; + + return ( +
+
+

+ {t('conversation.sources.text')} +

+
+ +
+ {options ? ( + <> + {options.map((option: any, index: number) => { + if (option.model === embeddingsName) { + return ( +
{ + dispatch(setSelectedDocs(option)); + handlePostDocumentSelect(option); + onClose(); + }} + > + Source + + {option.name} + + {selectedDocs && selectedDocs.id === option.id && ( + Selected + )} +
+ ); + } + return null; + })} +
+ Source + + {t('none')} + + {selectedDocs === null && ( + Selected + )} +
+ + ) : ( +
+ {t('noSourcesAvailable')} +
+ )} +
+ +
+ +
+
+ ); +}