feat: agents route replacing chatbots

- 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.
This commit is contained in:
Siddhant Rai
2025-04-11 17:24:22 +05:30
parent 94c7bba168
commit fa1f9d7009
29 changed files with 2001 additions and 579 deletions

View File

@@ -90,7 +90,7 @@ export default function ContextMenu({
onClick={(e) => e.stopPropagation()}
>
<div
className="flex w-32 flex-col rounded-xl text-sm shadow-xl md:w-36 dark:bg-charleston-green-2 bg-lotion"
className="flex w-32 flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2 md:w-36"
style={{ minWidth: '144px' }}
>
{options.map((option, index) => (
@@ -102,26 +102,22 @@ export default function ContextMenu({
option.onClick(event);
setIsOpen(false);
}}
className={`
flex justify-start items-center gap-4 p-3
transition-colors duration-200 ease-in-out
${index === 0 ? 'rounded-t-xl' : ''}
${index === options.length - 1 ? 'rounded-b-xl' : ''}
${
option.variant === 'danger'
? 'dark:text-red-2000 dark:hover:bg-charcoal-grey text-rosso-corsa hover:bg-bright-gray'
: 'dark:text-bright-gray dark:hover:bg-charcoal-grey text-eerie-black hover:bg-bright-gray'
}
`}
className={`flex items-center justify-start gap-4 p-3 transition-colors duration-200 ease-in-out ${index === 0 ? 'rounded-t-xl' : ''} ${index === options.length - 1 ? 'rounded-b-xl' : ''} ${
option.variant === 'danger'
? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey'
: 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey'
} `}
>
{option.icon && (
<img
width={option.iconWidth || 16}
height={option.iconHeight || 16}
src={option.icon}
alt={option.label}
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
/>
<div className="flex w-4 justify-center">
<img
width={option.iconWidth || 16}
height={option.iconHeight || 16}
src={option.icon}
alt={option.label}
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
/>
</div>
)}
<span>{option.label}</span>
</button>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import Arrow2 from '../assets/dropdown-arrow.svg';
import Edit from '../assets/edit.svg';
import Trash from '../assets/trash.svg';
@@ -9,6 +10,10 @@ function Dropdown({
onSelect,
size = 'w-32',
rounded = 'xl',
buttonBackgroundColor = 'white',
buttonDarkBackgroundColor = 'transparent',
optionsBackgroundColor = 'white',
optionsDarkBackgroundColor = 'dark-charcoal',
border = 'border-2',
borderColor = 'silver',
darkBorderColor = 'dim-gray',
@@ -17,6 +22,8 @@ function Dropdown({
showDelete,
onDelete,
placeholder,
placeholderTextColor = 'gray-500',
darkPlaceholderTextColor = 'gray-400',
contentSize = 'text-base',
}: {
options:
@@ -37,6 +44,10 @@ function Dropdown({
| ((value: { value: number; description: string }) => void);
size?: string;
rounded?: 'xl' | '3xl';
buttonBackgroundColor?: string;
buttonDarkBackgroundColor?: string;
optionsBackgroundColor?: string;
optionsDarkBackgroundColor?: string;
border?: 'border' | 'border-2';
borderColor?: string;
darkBorderColor?: string;
@@ -45,6 +56,8 @@ function Dropdown({
showDelete?: boolean;
onDelete?: (value: string) => void;
placeholder?: string;
placeholderTextColor?: string;
darkPlaceholderTextColor?: string;
contentSize?: string;
}) {
const dropdownRef = React.useRef<HTMLDivElement>(null);
@@ -71,7 +84,7 @@ function Dropdown({
<div
className={[
typeof selectedValue === 'string'
? 'relative mt-2'
? 'relative'
: 'relative align-middle',
size,
].join(' ')}
@@ -79,7 +92,7 @@ function Dropdown({
>
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${darkBorderColor} dark:bg-transparent ${
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-${buttonBackgroundColor} px-5 py-3 dark:border-${darkBorderColor} dark:bg-${buttonDarkBackgroundColor} ${
isOpen ? `${borderTopRadius}` : `${borderRadius}`
}`}
>
@@ -89,8 +102,9 @@ function Dropdown({
</span>
) : (
<span
className={`truncate dark:text-bright-gray ${
!selectedValue && 'text-silver dark:text-gray-400'
className={`truncate ${selectedValue && `dark:text-bright-gray`} ${
!selectedValue &&
`text-${placeholderTextColor} dark:text-${darkPlaceholderTextColor}`
} ${contentSize}`}
>
{selectedValue && 'label' in selectedValue
@@ -116,7 +130,7 @@ function Dropdown({
</button>
{isOpen && (
<div
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-white shadow-lg dark:border-${darkBorderColor} dark:bg-dark-charcoal`}
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-${optionsBackgroundColor} shadow-lg dark:border-${darkBorderColor} dark:bg-${optionsDarkBackgroundColor}`}
>
{options.map((option: any, index) => (
<div

View File

@@ -13,6 +13,7 @@ const Input = ({
className = '',
colorVariant = 'silver',
borderVariant = 'thick',
textSize = 'medium',
children,
labelBgClassName = 'bg-white dark:bg-raisin-black',
onChange,
@@ -28,6 +29,10 @@ const Input = ({
thin: 'border',
thick: 'border-2',
};
const textSizeStyles = {
small: 'text-sm',
medium: 'text-base',
};
const inputRef = useRef<HTMLInputElement>(null);
@@ -35,15 +40,7 @@ const Input = ({
<div className={`relative ${className}`}>
<input
ref={inputRef}
className={`peer h-[42px] w-full rounded-full px-3 py-1
bg-transparent outline-none
text-jet dark:text-bright-gray
placeholder-transparent
${colorStyles[colorVariant]}
${borderStyles[borderVariant]}
[&:-webkit-autofill]:bg-transparent
[&:-webkit-autofill]:appearance-none
[&:-webkit-autofill_selected]:bg-transparent`}
className={`peer h-[42px] w-full rounded-full bg-transparent px-3 py-1 text-jet placeholder-transparent outline-none dark:text-bright-gray ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
type={type}
id={id}
name={name}
@@ -61,15 +58,11 @@ const Input = ({
{placeholder && (
<label
htmlFor={id}
className={`absolute left-3 -top-2.5 px-2 text-xs transition-all
peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:text-base
peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs
peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400
cursor-none pointer-events-none ${labelBgClassName}`}
className={`absolute -top-2.5 left-3 px-2 ${textSizeStyles[textSize]} transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400 ${labelBgClassName}`}
>
{placeholder}
{required && (
<span className="text-[#D30000] dark:text-[#D42626] ml-0.5">*</span>
<span className="ml-0.5 text-[#D30000] dark:text-[#D42626]">*</span>
)}
</label>
)}

View File

@@ -0,0 +1,278 @@
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>
);
}

View File

@@ -1,7 +1,8 @@
import React, { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ArrowLeft from '../assets/arrow-left.svg';
import ArrowRight from '../assets/arrow-right.svg';
import { useTranslation } from 'react-i18next';
type HiddenGradientType = 'left' | 'right' | undefined;
@@ -10,7 +11,6 @@ const useTabs = () => {
const tabs = [
t('settings.general.label'),
t('settings.documents.label'),
t('settings.apiKeys.label'),
t('settings.analytics.label'),
t('settings.logs.label'),
t('settings.tools.label'),
@@ -48,18 +48,18 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
[containerRef.current],
);
return (
<div className="relative mt-6 flex flex-row items-center space-x-1 md:space-x-0 overflow-auto">
<div className="relative mt-6 flex flex-row items-center space-x-1 overflow-auto md:space-x-0">
<div
className={`${hiddenGradient === 'left' ? 'hidden' : ''} md:hidden absolute inset-y-0 left-6 w-14 bg-gradient-to-r from-white dark:from-raisin-black pointer-events-none`}
className={`${hiddenGradient === 'left' ? 'hidden' : ''} pointer-events-none absolute inset-y-0 left-6 w-14 bg-gradient-to-r from-white dark:from-raisin-black md:hidden`}
></div>
<div
className={`${hiddenGradient === 'right' ? 'hidden' : ''} md:hidden absolute inset-y-0 right-6 w-14 bg-gradient-to-l from-white dark:from-raisin-black pointer-events-none`}
className={`${hiddenGradient === 'right' ? 'hidden' : ''} pointer-events-none absolute inset-y-0 right-6 w-14 bg-gradient-to-l from-white dark:from-raisin-black md:hidden`}
></div>
<div className="md:hidden z-10">
<div className="z-10 md:hidden">
<button
onClick={() => scrollTabs(-1)}
className="flex h-6 w-6 items-center rounded-full justify-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
className="flex h-6 w-6 items-center justify-center rounded-full transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs left"
>
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
@@ -67,7 +67,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
</div>
<div
ref={containerRef}
className="flex flex-nowrap overflow-x-auto no-scrollbar md:space-x-4 scroll-smooth snap-x"
className="no-scrollbar flex snap-x flex-nowrap overflow-x-auto scroll-smooth md:space-x-4"
role="tablist"
aria-label="Settings tabs"
>
@@ -75,7 +75,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<button
key={index}
onClick={() => setActiveTab(tab)}
className={`snap-start h-9 rounded-3xl px-4 font-bold transition-colors ${
className={`h-9 snap-start rounded-3xl px-4 font-bold transition-colors ${
activeTab === tab
? 'bg-[#F4F4F5] text-neutral-900 dark:bg-dark-charcoal dark:text-white'
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
@@ -89,10 +89,10 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
</button>
))}
</div>
<div className="md:hidden z-10">
<div className="z-10 md:hidden">
<button
onClick={() => scrollTabs(1)}
className="flex h-6 w-6 rounded-full items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700"
className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs right"
>
<img src={ArrowRight} alt="right-arrow" className="h-3" />

View File

@@ -3,6 +3,7 @@ export type InputProps = {
value: string | string[] | number;
colorVariant?: 'silver' | 'jet' | 'gray';
borderVariant?: 'thin' | 'thick';
textSize?: 'small' | 'medium';
isAutoFocused?: boolean;
id?: string;
maxLength?: number;