diff --git a/frontend/src/components/SourcesPopup.tsx b/frontend/src/components/SourcesPopup.tsx index 0da27718..9f8ce8ce 100644 --- a/frontend/src/components/SourcesPopup.tsx +++ b/frontend/src/components/SourcesPopup.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useState } from 'react'; +import React, { useRef, useEffect, useState, useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { Doc } from '../models/misc'; @@ -32,6 +32,7 @@ export default function SourcesPopup({ 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 || @@ -44,19 +45,36 @@ export default function SourcesPopup({ option.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - 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%)', + 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 handleEmptyDocumentSelect = () => { dispatch(setSelectedDocs(null)); @@ -93,112 +111,118 @@ export default function SourcesPopup({ return (
-
-

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

- - setSearchTerm(e.target.value)} - placeholder={t('settings.documents.searchPlaceholder')} - borderVariant="thin" - className="mb-4" - labelBgClassName="bg-lotion dark:bg-charleston-green-2" - /> -
+
+
+

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

+ + setSearchTerm(e.target.value)} + placeholder={t('settings.documents.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) { - return ( -
{ - dispatch(setSelectedDocs(option)); - handlePostDocumentSelect(option); - onClose(); - }} - > - Source - - {option.name} - -
- {selectedDocs && - (option.id ? - selectedDocs.id === option.id : // For documents with MongoDB IDs - selectedDocs.date === option.date) && // For preloaded sources - Selected - } +
+ {options ? ( + <> + {filteredOptions?.map((option: any, index: number) => { + if (option.model === embeddingsName) { + return ( +
{ + dispatch(setSelectedDocs(option)); + handlePostDocumentSelect(option); + onClose(); + }} + > + Source + + {option.name} + +
+ {selectedDocs && + (option.id ? + selectedDocs.id === option.id : // For documents with MongoDB IDs + selectedDocs.date === option.date) && // For preloaded sources + Selected + } +
-
- ); - } - return null; - })} -
- Source - - {t('none')} - -
- {selectedDocs === null && ( - Selected - )} + ); + } + return null; + })} +
+ Source + + {t('none')} + +
+ {selectedDocs === null && ( + Selected + )} +
+ + ) : ( +
+ {t('noSourcesAvailable')}
- - ) : ( -
- {t('noSourcesAvailable')} -
- )} -
+ )} +
- + -
- +
+ +
); diff --git a/frontend/src/components/ToolsPopup.tsx b/frontend/src/components/ToolsPopup.tsx index 4e13aa2a..929e7ada 100644 --- a/frontend/src/components/ToolsPopup.tsx +++ b/frontend/src/components/ToolsPopup.tsx @@ -29,7 +29,7 @@ export default function ToolsPopup({ const [searchTerm, setSearchTerm] = useState(''); const [isDarkTheme] = useDarkTheme(); const popupRef = useRef(null); - const [popupPosition, setPopupPosition] = useState({ bottom: 0, left: 0 }); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, maxHeight: 0, showAbove: false }); useLayoutEffect(() => { if (!isOpen || !anchorRef.current) return; @@ -38,15 +38,24 @@ export default function ToolsPopup({ 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(462, viewportWidth * 0.95) - 10 + ); + setPopupPosition({ - bottom: Math.min( - window.innerHeight - rect.top + 10, - window.innerHeight - 20 - ), - left: Math.min( - rect.left, - window.innerWidth - Math.min(462, window.innerWidth * 0.95) - 10 - ) + top: showAbove ? rect.top - 8 : rect.bottom + 8, + left, + maxHeight: Math.min(600, maxHeight), + showAbove }); }; @@ -67,11 +76,13 @@ export default function ToolsPopup({ } }; - document.addEventListener('mousedown', handleClickOutside); + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } return () => { document.removeEventListener('mousedown', handleClickOutside); }; - }, [onClose, anchorRef]); + }, [onClose, anchorRef, isOpen]); useEffect(() => { if (isOpen) { @@ -113,45 +124,51 @@ export default function ToolsPopup({ if (!isOpen) return null; + const filteredTools = userTools.filter((tool) => + tool.displayName.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return (
-
-

- {t('settings.tools.label')} -

+
+
+

+ {t('settings.tools.label')} +

- setSearchTerm(e.target.value)} - placeholder={t('settings.tools.searchPlaceholder')} - labelBgClassName="bg-lotion dark:bg-charleston-green-2" - borderVariant="thin" - className="mb-4" - /> + setSearchTerm(e.target.value)} + placeholder={t('settings.tools.searchPlaceholder')} + labelBgClassName="bg-lotion dark:bg-charleston-green-2" + borderVariant="thin" + className="mb-4" + /> +
{loading ? ( -
+
) : ( -
-
- {userTools.length === 0 ? ( +
+
+ {filteredTools.length === 0 ? (
) : ( - userTools - .filter((tool) => - tool.displayName - .toLowerCase() - .includes(searchTerm.toLowerCase()), - ) - .map((tool) => ( -
updateToolStatus(tool.id, !tool.status)} - className="flex items-center justify-between p-3 border-b border-[#D9D9D9] dark:border-dim-gray hover:bg-gray-100 dark:hover:bg-charleston-green-3" - > -
- {`${tool.displayName} -
-

- {tool.displayName} -

-
-
-
-
- {tool.status && ( - Tool enabled - )} -
+ filteredTools.map((tool) => ( +
updateToolStatus(tool.id, !tool.status)} + className="flex items-center justify-between p-3 border-b border-[#D9D9D9] dark:border-dim-gray hover:bg-gray-100 dark:hover:bg-charleston-green-3" + > +
+ {`${tool.displayName} +
+

+ {tool.displayName} +

- )) +
+
+ {tool.status && ( + Tool enabled + )} +
+
+
+ )) )}
)} -