import React, { useRef, useEffect, useState, useLayoutEffect } 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 RedirectIcon from '../assets/redirect.svg'; import Input from './Input'; 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 [searchTerm, setSearchTerm] = useState(''); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, maxHeight: 0, showAbove: false, }); const embeddingsName = import.meta.env.VITE_EMBEDDINGS_NAME || 'huggingface_sentence-transformers/all-mpnet-base-v2'; const options = useSelector(selectSourceDocs); const selectedDocs = useSelector(selectSelectedDocs); const filteredOptions = options?.filter((option) => option.name.toLowerCase().includes(searchTerm.toLowerCase()), ); useLayoutEffect(() => { if (!isOpen || !anchorRef.current) return; const updatePosition = () => { if (!anchorRef.current) return; const rect = anchorRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const spaceAbove = rect.top; const spaceBelow = viewportHeight - rect.bottom; const showAbove = spaceAbove > spaceBelow && spaceAbove >= 300; const maxHeight = showAbove ? spaceAbove - 16 : spaceBelow - 16; const left = Math.min( rect.left, viewportWidth - Math.min(480, viewportWidth * 0.95) - 10, ); setPopupPosition({ top: showAbove ? rect.top - 8 : rect.bottom + 8, left, maxHeight: Math.min(600, maxHeight), showAbove, }); }; updatePosition(); window.addEventListener('resize', updatePosition); return () => window.removeEventListener('resize', updatePosition); }, [isOpen, anchorRef]); 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')}

setSearchTerm(e.target.value)} placeholder={t('settings.sources.searchPlaceholder')} borderVariant="thin" className="mb-4" labelBgClassName="bg-lotion dark:bg-charleston-green-2" />
{options ? ( <> {filteredOptions?.map((option: any, index: number) => { if (option.model === embeddingsName) { const isSelected = selectedDocs && Array.isArray(selectedDocs) && selectedDocs.length > 0 && selectedDocs.some((doc) => option.id ? doc.id === option.id : doc.date === option.date, ); return (
{ if (isSelected) { const updatedDocs = selectedDocs && Array.isArray(selectedDocs) ? selectedDocs.filter((doc) => option.id ? doc.id !== option.id : doc.date !== option.date, ) : []; dispatch( setSelectedDocs( updatedDocs.length > 0 ? updatedDocs : null, ), ); handlePostDocumentSelect( updatedDocs.length > 0 ? updatedDocs : null, ); } else { const updatedDocs = selectedDocs && Array.isArray(selectedDocs) ? [...selectedDocs, option] : [option]; dispatch(setSelectedDocs(updatedDocs)); handlePostDocumentSelect(updatedDocs); } }} > Source {option.name}
{isSelected && ( Selected )}
); } return null; })} ) : (
{t('noSourcesAvailable')}
)}
); }