mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-01 17:43:15 +00:00
- Removed API Keys tab from SettingsBar and adjusted tab layout. - Improved styling for tab scrolling buttons and gradient indicators. - Introduced AgentDetailsModal for displaying agent access details. - Updated Analytics component to fetch agent data and handle analytics for selected agent. - Refactored Logs component to accept agentId as a prop for filtering logs. - Enhanced type definitions for InputProps to include textSize. - Cleaned up unused imports and optimized component structure across various files.
279 lines
9.3 KiB
TypeScript
279 lines
9.3 KiB
TypeScript
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import CheckmarkIcon from '../assets/checkmark.svg';
|
|
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
|
import NoFilesIcon from '../assets/no-files.svg';
|
|
import { useDarkTheme } from '../hooks';
|
|
import Input from './Input';
|
|
|
|
export type OptionType = {
|
|
id: string | number;
|
|
label: string;
|
|
icon?: string | React.ReactNode;
|
|
[key: string]: any;
|
|
};
|
|
|
|
type MultiSelectPopupProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
anchorRef: React.RefObject<HTMLElement>;
|
|
options: OptionType[];
|
|
selectedIds: Set<string | number>;
|
|
onSelectionChange: (newSelectedIds: Set<string | number>) => void;
|
|
title?: string;
|
|
searchPlaceholder?: string;
|
|
noOptionsMessage?: string;
|
|
loading?: boolean;
|
|
footerContent?: React.ReactNode;
|
|
showSearch?: boolean;
|
|
singleSelect?: boolean;
|
|
};
|
|
|
|
export default function MultiSelectPopup({
|
|
isOpen,
|
|
onClose,
|
|
anchorRef,
|
|
options,
|
|
selectedIds,
|
|
onSelectionChange,
|
|
title,
|
|
searchPlaceholder,
|
|
noOptionsMessage,
|
|
loading = false,
|
|
footerContent,
|
|
showSearch = true,
|
|
singleSelect = false,
|
|
}: MultiSelectPopupProps) {
|
|
const { t } = useTranslation();
|
|
const [isDarkTheme] = useDarkTheme();
|
|
|
|
const popupRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [popupPosition, setPopupPosition] = useState({
|
|
top: 0,
|
|
left: 0,
|
|
maxHeight: 0,
|
|
showAbove: false,
|
|
});
|
|
|
|
const filteredOptions = options.filter((option) =>
|
|
option.label.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
);
|
|
|
|
const handleOptionClick = (optionId: string | number) => {
|
|
let newSelectedIds: Set<string | number>;
|
|
if (singleSelect) newSelectedIds = new Set<string | number>();
|
|
else newSelectedIds = new Set(selectedIds);
|
|
if (newSelectedIds.has(optionId)) {
|
|
newSelectedIds.delete(optionId);
|
|
} else newSelectedIds.add(optionId);
|
|
onSelectionChange(newSelectedIds);
|
|
};
|
|
|
|
const renderIcon = (icon: string | React.ReactNode) => {
|
|
if (typeof icon === 'string') {
|
|
if (icon.startsWith('/') || icon.startsWith('http')) {
|
|
return (
|
|
<img
|
|
src={icon}
|
|
alt=""
|
|
className="mr-3 h-5 w-5 flex-shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<span className="mr-3 h-5 w-5 flex-shrink-0" aria-hidden="true">
|
|
{icon}
|
|
</span>
|
|
);
|
|
}
|
|
return <span className="mr-3 flex-shrink-0">{icon}</span>;
|
|
};
|
|
|
|
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 popupPadding = 16;
|
|
const popupMinWidth = 300;
|
|
const popupMaxWidth = 462;
|
|
const popupDefaultHeight = 300;
|
|
|
|
const spaceAbove = rect.top;
|
|
const spaceBelow = viewportHeight - rect.bottom;
|
|
const showAbove =
|
|
spaceBelow < popupDefaultHeight && spaceAbove >= popupDefaultHeight;
|
|
|
|
const maxHeight = Math.max(
|
|
150,
|
|
showAbove ? spaceAbove - popupPadding : spaceBelow - popupPadding,
|
|
);
|
|
|
|
const availableWidth = viewportWidth - 20;
|
|
const calculatedWidth = Math.min(popupMaxWidth, availableWidth);
|
|
|
|
let left = rect.left;
|
|
if (left + calculatedWidth > viewportWidth - 10) {
|
|
left = viewportWidth - calculatedWidth - 10;
|
|
}
|
|
left = Math.max(10, left);
|
|
|
|
setPopupPosition({
|
|
top: showAbove ? rect.top - 8 : rect.bottom + 8,
|
|
left: left,
|
|
maxHeight: Math.min(600, maxHeight),
|
|
showAbove,
|
|
});
|
|
};
|
|
|
|
updatePosition();
|
|
window.addEventListener('resize', updatePosition);
|
|
window.addEventListener('scroll', updatePosition, true);
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', updatePosition);
|
|
window.removeEventListener('scroll', updatePosition, true);
|
|
};
|
|
}, [isOpen, anchorRef]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
popupRef.current &&
|
|
!popupRef.current.contains(event.target as Node) &&
|
|
anchorRef.current &&
|
|
!anchorRef.current.contains(event.target as Node)
|
|
)
|
|
onClose();
|
|
};
|
|
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [onClose, anchorRef, isOpen]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) setSearchTerm('');
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
return (
|
|
<div
|
|
ref={popupRef}
|
|
className="fixed z-[9999] flex flex-col rounded-lg border border-light-silver bg-lotion shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033] dark:border-dim-gray dark:bg-charleston-green-2"
|
|
style={{
|
|
top: popupPosition.showAbove ? undefined : popupPosition.top,
|
|
bottom: popupPosition.showAbove
|
|
? window.innerHeight - popupPosition.top + 8
|
|
: undefined,
|
|
left: popupPosition.left,
|
|
maxWidth: `${Math.min(462, window.innerWidth - 20)}px`,
|
|
width: '100%',
|
|
maxHeight: `${popupPosition.maxHeight}px`,
|
|
}}
|
|
>
|
|
{(title || showSearch) && (
|
|
<div className="flex-shrink-0 p-4">
|
|
{title && (
|
|
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
|
|
{title}
|
|
</h3>
|
|
)}
|
|
{showSearch && (
|
|
<Input
|
|
id="multi-select-search"
|
|
name="multi-select-search"
|
|
type="text"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder={
|
|
searchPlaceholder ||
|
|
t('settings.tools.searchPlaceholder', 'Search...')
|
|
}
|
|
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
|
borderVariant="thin"
|
|
className="mb-4"
|
|
textSize="small"
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="mx-4 mb-4 flex-grow overflow-auto rounded-md border border-[#D9D9D9] dark:border-dim-gray">
|
|
{loading ? (
|
|
<div className="flex h-full items-center justify-center py-4">
|
|
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
|
|
</div>
|
|
) : (
|
|
<div className="h-full overflow-y-auto [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-400 dark:[&::-webkit-scrollbar-thumb]:bg-gray-600 [&::-webkit-scrollbar-track]:bg-gray-200 dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C] [&::-webkit-scrollbar]:w-2">
|
|
{filteredOptions.length === 0 ? (
|
|
<div className="flex h-full flex-col items-center justify-center px-4 py-8 text-center">
|
|
<img
|
|
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
|
alt="No options found"
|
|
className="mx-auto mb-3 h-16 w-16"
|
|
/>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{searchTerm
|
|
? 'No results found'
|
|
: noOptionsMessage || 'No options available'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
filteredOptions.map((option) => {
|
|
const isSelected = selectedIds.has(option.id);
|
|
return (
|
|
<div
|
|
key={option.id}
|
|
onClick={() => handleOptionClick(option.id)}
|
|
className="flex cursor-pointer items-center justify-between border-b border-[#D9D9D9] p-3 last:border-b-0 hover:bg-gray-100 dark:border-dim-gray dark:hover:bg-charleston-green-3"
|
|
role="option"
|
|
aria-selected={isSelected}
|
|
>
|
|
<div className="mr-3 flex flex-grow items-center overflow-hidden">
|
|
{option.icon && renderIcon(option.icon)}
|
|
<p
|
|
className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white"
|
|
title={option.label}
|
|
>
|
|
{option.label}
|
|
</p>
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
<div
|
|
className={`flex h-4 w-4 items-center justify-center rounded-sm border border-[#C6C6C6] bg-white dark:border-[#757783] dark:bg-charleston-green-2`}
|
|
aria-hidden="true"
|
|
>
|
|
{isSelected && (
|
|
<img
|
|
src={CheckmarkIcon}
|
|
alt="checkmark"
|
|
width={10}
|
|
height={10}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{footerContent && (
|
|
<div className="flex-shrink-0 border-t border-light-silver p-4 dark:border-dim-gray">
|
|
{footerContent}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|