(fix:responsive) tools and source pop

This commit is contained in:
ManishMadan2882
2025-03-28 20:40:46 +05:30
parent 7c92558ad1
commit 727a8ef13d
2 changed files with 222 additions and 187 deletions

View File

@@ -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 { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Doc } from '../models/misc'; import { Doc } from '../models/misc';
@@ -32,6 +32,7 @@ export default function SourcesPopup({
const { t } = useTranslation(); const { t } = useTranslation();
const popupRef = useRef<HTMLDivElement>(null); const popupRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, maxHeight: 0, showAbove: false });
const embeddingsName = const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME || import.meta.env.VITE_EMBEDDINGS_NAME ||
@@ -44,19 +45,36 @@ export default function SourcesPopup({
option.name.toLowerCase().includes(searchTerm.toLowerCase()) option.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
const getPopupPosition = () => { useLayoutEffect(() => {
if (!anchorRef.current) return {}; if (!isOpen || !anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect(); const updatePosition = () => {
if (!anchorRef.current) return;
return {
position: 'fixed' as const, const rect = anchorRef.current.getBoundingClientRect();
top: `${rect.top - 8}px`, const viewportHeight = window.innerHeight;
left: `${rect.left}px`, const viewportWidth = window.innerWidth;
minWidth: `${rect.width}px`, const spaceAbove = rect.top;
transform: 'translateY(-100%)', 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 = () => { const handleEmptyDocumentSelect = () => {
dispatch(setSelectedDocs(null)); dispatch(setSelectedDocs(null));
@@ -93,112 +111,118 @@ export default function SourcesPopup({
return ( return (
<div <div
ref={popupRef} ref={popupRef}
style={getPopupPosition()} className="fixed z-50 bg-lotion dark:bg-charleston-green-2 rounded-xl shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033] flex flex-col"
className="bg-lotion dark:bg-charleston-green-2 rounded-xl shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033] w-full max-w-[95vw] md:max-w-md flex flex-col z-50 fixed" style={{
top: popupPosition.showAbove ? popupPosition.top : undefined,
bottom: popupPosition.showAbove ? undefined : window.innerHeight - popupPosition.top,
left: popupPosition.left,
maxWidth: Math.min(480, window.innerWidth * 0.95),
width: '100%',
height: popupPosition.maxHeight,
transform: popupPosition.showAbove ? 'translateY(-100%)' : 'none',
}}
> >
<div className="px-4 md:px-6 py-4"> <div className="flex flex-col h-full">
<h2 className="text-lg font-bold text-[#141414] dark:text-bright-gray mb-4 dark:text-[20px]"> <div className="px-4 md:px-6 py-4 flex-shrink-0">
{t('conversation.sources.text')} <h2 className="text-lg font-bold text-[#141414] dark:text-bright-gray mb-4 dark:text-[20px]">
</h2> {t('conversation.sources.text')}
</h2>
<Input
id="source-search" <Input
name="source-search" id="source-search"
type="text" name="source-search"
value={searchTerm} type="text"
onChange={(e) => setSearchTerm(e.target.value)} value={searchTerm}
placeholder={t('settings.documents.searchPlaceholder')} onChange={(e) => setSearchTerm(e.target.value)}
borderVariant="thin" placeholder={t('settings.documents.searchPlaceholder')}
className="mb-4" borderVariant="thin"
labelBgClassName="bg-lotion dark:bg-charleston-green-2" className="mb-4"
/> labelBgClassName="bg-lotion dark:bg-charleston-green-2"
</div> />
</div>
<div className="overflow-y-auto mx-4 border border-[#D9D9D9] dark:border-dim-gray rounded-md [&::-webkit-scrollbar-thumb]:bg-[#888] [&::-webkit-scrollbar-thumb]:hover:bg-[#555] [&::-webkit-scrollbar-track]:bg-[#E2E8F0] dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C]" <div className="flex-grow overflow-y-auto mx-4 border border-[#D9D9D9] dark:border-dim-gray rounded-md [&::-webkit-scrollbar-thumb]:bg-[#888] [&::-webkit-scrollbar-thumb]:hover:bg-[#555] [&::-webkit-scrollbar-track]:bg-[#E2E8F0] dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C]">
style={{ {options ? (
height: 'min(488px, calc(100vh - 220px))', <>
maxHeight: 'calc(100vh - 220px)' {filteredOptions?.map((option: any, index: number) => {
}}> if (option.model === embeddingsName) {
{options ? ( return (
<> <div
{filteredOptions?.map((option: any, index: number) => { key={index}
if (option.model === embeddingsName) { className="flex cursor-pointer items-center p-3 hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors border-b border-[#D9D9D9] dark:border-dim-gray border-opacity-80 dark:text-[14px]"
return ( onClick={() => {
<div dispatch(setSelectedDocs(option));
key={index} handlePostDocumentSelect(option);
className="flex cursor-pointer items-center p-3 hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors border-b border-[#D9D9D9] dark:border-dim-gray border-opacity-80 dark:text-[14px]" onClose();
onClick={() => { }}
dispatch(setSelectedDocs(option)); >
handlePostDocumentSelect(option); <img
onClose(); src={SourceIcon}
}} alt="Source"
> width={14} height={14}
<img className="mr-3 flex-shrink-0"
src={SourceIcon} />
alt="Source" <span className="text-[#5D5D5D] dark:text-bright-gray font-medium flex-grow overflow-hidden overflow-ellipsis whitespace-nowrap mr-3">
width={14} height={14} {option.name}
className="mr-3 flex-shrink-0" </span>
/> <div className={`w-4 h-4 border flex-shrink-0 flex items-center justify-center p-[0.5px] dark:border-[#757783] border-[#C6C6C6]`}>
<span className="text-[#5D5D5D] dark:text-bright-gray font-medium flex-grow overflow-hidden overflow-ellipsis whitespace-nowrap mr-3"> {selectedDocs &&
{option.name} (option.id ?
</span> selectedDocs.id === option.id : // For documents with MongoDB IDs
<div className={`w-4 h-4 border flex-shrink-0 flex items-center justify-center p-[0.5px] dark:border-[#757783] border-[#C6C6C6]`}> selectedDocs.date === option.date) && // For preloaded sources
{selectedDocs && <img
(option.id ? src={CheckIcon}
selectedDocs.id === option.id : // For documents with MongoDB IDs alt="Selected"
selectedDocs.date === option.date) && // For preloaded sources className="h-3 w-3"
<img />
src={CheckIcon} }
alt="Selected" </div>
className="h-3 w-3"
/>
}
</div> </div>
</div> );
); }
} return null;
return null; })}
})} <div
<div className="flex cursor-pointer items-center p-3 hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors border-b border-[#D9D9D9] dark:border-dim-gray border-opacity-80 dark:text-[14px]"
className="flex cursor-pointer items-center p-3 hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors border-b border-[#D9D9D9] dark:border-dim-gray border-opacity-80 dark:text-[14px]" onClick={handleEmptyDocumentSelect}
onClick={handleEmptyDocumentSelect} >
> <img width={14} height={14} src={SourceIcon} alt="Source" className="mr-3 flex-shrink-0" />
<img width={14} height={14} src={SourceIcon} alt="Source" className="mr-3 flex-shrink-0" /> <span className="text-[#5D5D5D] dark:text-bright-gray font-medium flex-grow mr-3">
<span className="text-[#5D5D5D] dark:text-bright-gray font-medium flex-grow mr-3"> {t('none')}
{t('none')} </span>
</span> <div className={`w-4 h-4 border flex-shrink-0 flex items-center justify-center p-[0.5px] dark:border-[#757783] border-[#C6C6C6]`}>
<div className={`w-4 h-4 border flex-shrink-0 flex items-center justify-center p-[0.5px] dark:border-[#757783] border-[#C6C6C6]`}> {selectedDocs === null && (
{selectedDocs === null && ( <img src={CheckIcon} alt="Selected" className="h-3 w-3" />
<img src={CheckIcon} alt="Selected" className="h-3 w-3" /> )}
)} </div>
</div> </div>
</>
) : (
<div className="p-4 text-center text-gray-500 dark:text-bright-gray dark:text-[14px]">
{t('noSourcesAvailable')}
</div> </div>
</> )}
) : ( </div>
<div className="p-4 text-center text-gray-500 dark:text-bright-gray dark:text-[14px]">
{t('noSourcesAvailable')}
</div>
)}
</div>
<div className="px-4 md:px-6 py-4 opacity-75 hover:opacity-100 transition-opacity duration-200"> <div className="px-4 md:px-6 py-4 opacity-75 hover:opacity-100 transition-opacity duration-200 flex-shrink-0">
<a <a
href="/settings/documents" href="/settings/documents"
className="text-violets-are-blue text-base font-medium flex items-center gap-2" className="text-violets-are-blue text-base font-medium flex items-center gap-2"
onClick={onClose} onClick={onClose}
> >
Go to Documents Go to Documents
<img src={RedirectIcon} alt="Redirect" className="w-3 h-3" /> <img src={RedirectIcon} alt="Redirect" className="w-3 h-3" />
</a> </a>
</div> </div>
<div className="px-4 md:px-6 py-3 flex justify-start"> <div className="px-4 md:px-6 py-3 flex justify-start flex-shrink-0">
<button <button
onClick={handleUploadClick} onClick={handleUploadClick}
className="py-2 px-4 rounded-full border text-violets-are-blue hover:bg-violets-are-blue border-violets-are-blue hover:text-white transition-colors duration-200 text-[14px] font-medium w-auto" className="py-2 px-4 rounded-full border text-violets-are-blue hover:bg-violets-are-blue border-violets-are-blue hover:text-white transition-colors duration-200 text-[14px] font-medium w-auto"
> >
Upload new Upload new
</button> </button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -29,7 +29,7 @@ export default function ToolsPopup({
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [isDarkTheme] = useDarkTheme(); const [isDarkTheme] = useDarkTheme();
const popupRef = useRef<HTMLDivElement>(null); const popupRef = useRef<HTMLDivElement>(null);
const [popupPosition, setPopupPosition] = useState({ bottom: 0, left: 0 }); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, maxHeight: 0, showAbove: false });
useLayoutEffect(() => { useLayoutEffect(() => {
if (!isOpen || !anchorRef.current) return; if (!isOpen || !anchorRef.current) return;
@@ -38,15 +38,24 @@ export default function ToolsPopup({
if (!anchorRef.current) return; if (!anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect(); 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({ setPopupPosition({
bottom: Math.min( top: showAbove ? rect.top - 8 : rect.bottom + 8,
window.innerHeight - rect.top + 10, left,
window.innerHeight - 20 maxHeight: Math.min(600, maxHeight),
), showAbove
left: Math.min(
rect.left,
window.innerWidth - Math.min(462, window.innerWidth * 0.95) - 10
)
}); });
}; };
@@ -67,11 +76,13 @@ export default function ToolsPopup({
} }
}; };
document.addEventListener('mousedown', handleClickOutside); if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
}; };
}, [onClose, anchorRef]); }, [onClose, anchorRef, isOpen]);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
@@ -113,45 +124,51 @@ export default function ToolsPopup({
if (!isOpen) return null; if (!isOpen) return null;
const filteredTools = userTools.filter((tool) =>
tool.displayName.toLowerCase().includes(searchTerm.toLowerCase())
);
return ( return (
<div <div
ref={popupRef} ref={popupRef}
className="absolute z-10 w-[462px] max-w-[95vw] rounded-lg border border-light-silver dark:border-dim-gray bg-lotion dark:bg-charleston-green-2 shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]" className="fixed z-[9999] rounded-lg border border-light-silver dark:border-dim-gray bg-lotion dark:bg-charleston-green-2 shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033]"
style={{ style={{
bottom: popupPosition.bottom, top: popupPosition.showAbove ? popupPosition.top : undefined,
bottom: popupPosition.showAbove ? undefined : window.innerHeight - popupPosition.top,
left: popupPosition.left, left: popupPosition.left,
position: 'fixed', maxWidth: Math.min(462, window.innerWidth * 0.95),
zIndex: 9999, width: '100%',
height: popupPosition.maxHeight,
transform: popupPosition.showAbove ? 'translateY(-100%)' : 'none',
}} }}
> >
<div className="p-4"> <div className="flex flex-col h-full">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <div className="p-4 flex-shrink-0">
{t('settings.tools.label')} <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h3> {t('settings.tools.label')}
</h3>
<Input <Input
id="tool-search" id="tool-search"
name="tool-search" name="tool-search"
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('settings.tools.searchPlaceholder')} placeholder={t('settings.tools.searchPlaceholder')}
labelBgClassName="bg-lotion dark:bg-charleston-green-2" labelBgClassName="bg-lotion dark:bg-charleston-green-2"
borderVariant="thin" borderVariant="thin"
className="mb-4" className="mb-4"
/> />
</div>
{loading ? ( {loading ? (
<div className="flex justify-center py-4"> <div className="flex justify-center py-4 flex-grow">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
</div> </div>
) : ( ) : (
<div className="border border-[#D9D9D9] dark:border-dim-gray rounded-md overflow-hidden"> <div className="mx-4 border border-[#D9D9D9] dark:border-dim-gray rounded-md overflow-hidden flex-grow">
<div <div className="h-full overflow-y-auto [&::-webkit-scrollbar-thumb]:bg-[#888] [&::-webkit-scrollbar-thumb]:hover:bg-[#555] [&::-webkit-scrollbar-track]:bg-[#E2E8F0] dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C]">
className="h-[calc(min(480px,100vh-220px))] overflow-y-auto [&::-webkit-scrollbar-thumb]:bg-[#888] [&::-webkit-scrollbar-thumb]:hover:bg-[#555] [&::-webkit-scrollbar-track]:bg-[#E2E8F0] dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C]" {filteredTools.length === 0 ? (
>
{userTools.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full py-8"> <div className="flex flex-col items-center justify-center h-full py-8">
<img <img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon} src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
@@ -163,50 +180,44 @@ export default function ToolsPopup({
</p> </p>
</div> </div>
) : ( ) : (
userTools filteredTools.map((tool) => (
.filter((tool) => <div
tool.displayName key={tool.id}
.toLowerCase() onClick={() => updateToolStatus(tool.id, !tool.status)}
.includes(searchTerm.toLowerCase()), 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"
) >
.map((tool) => ( <div className="flex items-center flex-grow mr-3">
<div <img
key={tool.id} src={`/toolIcons/tool_${tool.name}.svg`}
onClick={() => updateToolStatus(tool.id, !tool.status)} alt={`${tool.displayName} icon`}
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" className="h-5 w-5 mr-4 flex-shrink-0"
> />
<div className="flex items-center flex-grow mr-3"> <div className="overflow-hidden">
<img <p className="text-xs font-medium text-gray-900 dark:text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
src={`/toolIcons/tool_${tool.name}.svg`} {tool.displayName}
alt={`${tool.displayName} icon`} </p>
className="h-5 w-5 mr-4 flex-shrink-0"
/>
<div className="overflow-hidden">
<p className="text-xs font-medium text-gray-900 dark:text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
{tool.displayName}
</p>
</div>
</div>
<div className="flex items-center flex-shrink-0">
<div className={`w-4 h-4 border flex items-center justify-center p-[0.5px] dark:border-[#757783] border-[#C6C6C6]`}>
{tool.status && (
<img
src={CheckmarkIcon}
alt="Tool enabled"
width={12}
height={12}
/>
)}
</div>
</div> </div>
</div> </div>
)) <div className="flex items-center flex-shrink-0">
<div className={`w-4 h-4 border flex items-center justify-center p-[0.5px] dark:border-[#757783] border-[#C6C6C6]`}>
{tool.status && (
<img
src={CheckmarkIcon}
alt="Tool enabled"
width={12}
height={12}
/>
)}
</div>
</div>
</div>
))
)} )}
</div> </div>
</div> </div>
)} )}
<div className="mt-4 flex justify-start"> <div className="p-4 flex-shrink-0">
<a <a
href="/settings/tools" href="/settings/tools"
className="text-base text-purple-30 font-medium hover:text-violets-are-blue flex items-center" className="text-base text-purple-30 font-medium hover:text-violets-are-blue flex items-center"
@@ -222,4 +233,4 @@ export default function ToolsPopup({
</div> </div>
</div> </div>
); );
} }