| | import React from 'react'; |
| | import * as Ariakit from '@ariakit/react'; |
| | import { ChevronRight } from 'lucide-react'; |
| | import { PinIcon, MCPIcon } from '@librechat/client'; |
| | import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon'; |
| | import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; |
| | import { useBadgeRowContext } from '~/Providers'; |
| | import { cn } from '~/utils'; |
| |
|
| | interface MCPSubMenuProps { |
| | placeholder?: string; |
| | } |
| |
|
| | const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>( |
| | ({ placeholder, ...props }, ref) => { |
| | const { mcpServerManager } = useBadgeRowContext(); |
| | const { |
| | isPinned, |
| | mcpValues, |
| | setIsPinned, |
| | isInitializing, |
| | placeholderText, |
| | configuredServers, |
| | getConfigDialogProps, |
| | toggleServerSelection, |
| | getServerStatusIconProps, |
| | } = mcpServerManager; |
| |
|
| | const menuStore = Ariakit.useMenuStore({ |
| | focusLoop: true, |
| | showTimeout: 100, |
| | placement: 'right', |
| | }); |
| |
|
| | |
| | if (!configuredServers || configuredServers.length === 0) { |
| | return null; |
| | } |
| |
|
| | const configDialogProps = getConfigDialogProps(); |
| |
|
| | return ( |
| | <div ref={ref}> |
| | <Ariakit.MenuProvider store={menuStore}> |
| | <Ariakit.MenuItem |
| | {...props} |
| | render={ |
| | <Ariakit.MenuButton |
| | onClick={(e: React.MouseEvent<HTMLButtonElement>) => { |
| | e.stopPropagation(); |
| | menuStore.toggle(); |
| | }} |
| | className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" |
| | /> |
| | } |
| | > |
| | <div className="flex items-center gap-2"> |
| | <MCPIcon className="icon-md" /> |
| | <span>{placeholder || placeholderText}</span> |
| | <ChevronRight className="ml-auto h-3 w-3" /> |
| | </div> |
| | <button |
| | type="button" |
| | onClick={(e) => { |
| | e.stopPropagation(); |
| | setIsPinned(!isPinned); |
| | }} |
| | className={cn( |
| | 'rounded p-1 transition-all duration-200', |
| | 'hover:bg-surface-tertiary hover:shadow-sm', |
| | !isPinned && 'text-text-secondary hover:text-text-primary', |
| | )} |
| | aria-label={isPinned ? 'Unpin' : 'Pin'} |
| | > |
| | <div className="h-4 w-4"> |
| | <PinIcon unpin={isPinned} /> |
| | </div> |
| | </button> |
| | </Ariakit.MenuItem> |
| | <Ariakit.Menu |
| | portal={true} |
| | unmountOnHide={true} |
| | className={cn( |
| | 'animate-popover-left z-50 ml-3 flex min-w-[200px] flex-col rounded-xl', |
| | 'border border-border-light bg-surface-secondary p-1 shadow-lg', |
| | )} |
| | > |
| | {configuredServers.map((serverName) => { |
| | const statusIconProps = getServerStatusIconProps(serverName); |
| | const isSelected = mcpValues?.includes(serverName) ?? false; |
| | const isServerInitializing = isInitializing(serverName); |
| | |
| | const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />; |
| | |
| | return ( |
| | <Ariakit.MenuItem |
| | key={serverName} |
| | onClick={(event) => { |
| | event.preventDefault(); |
| | toggleServerSelection(serverName); |
| | }} |
| | disabled={isServerInitializing} |
| | className={cn( |
| | 'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer', |
| | 'scroll-m-1 outline-none transition-colors', |
| | 'hover:bg-black/[0.075] dark:hover:bg-white/10', |
| | 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', |
| | 'w-full min-w-0 justify-between text-sm', |
| | isServerInitializing && |
| | 'opacity-50 hover:bg-transparent dark:hover:bg-transparent', |
| | )} |
| | > |
| | <div className="flex flex-grow items-center gap-2"> |
| | <Ariakit.MenuItemCheck checked={isSelected} /> |
| | <span>{serverName}</span> |
| | </div> |
| | {statusIcon && <div className="ml-2 flex items-center">{statusIcon}</div>} |
| | </Ariakit.MenuItem> |
| | ); |
| | })} |
| | </Ariakit.Menu> |
| | </Ariakit.MenuProvider> |
| | {configDialogProps && <MCPConfigDialog {...configDialogProps} />} |
| | </div> |
| | ); |
| | }, |
| | ); |
| | |
| | MCPSubMenu.displayName = 'MCPSubMenu'; |
| | |
| | export default React.memo(MCPSubMenu); |
| | |