LibreChat/client/src/components/Chat/Input/MCPSelect.tsx
Dustin Healy b6dcefc53a
🌐 refactor: Interpolate Localization Keys (#10650)
* fix: replace string concatenation of localization keys with interpolations and add keys for unlocalized string literals

* chore: update test for new localization key

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-11 16:35:56 -05:00

111 lines
3.5 KiB
TypeScript

import React, { memo, useCallback } from 'react';
import { MultiSelect, MCPIcon } from '@librechat/client';
import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
import { useBadgeRowContext } from '~/Providers';
function MCPSelectContent() {
const { conversationId, mcpServerManager } = useBadgeRowContext();
const {
localize,
isPinned,
mcpValues,
isInitializing,
placeholderText,
configuredServers,
batchToggleServers,
getConfigDialogProps,
getServerStatusIconProps,
} = mcpServerManager;
const renderSelectedValues = useCallback(
(values: string[], placeholder?: string) => {
if (values.length === 0) {
return placeholder || localize('com_ui_select_placeholder');
}
if (values.length === 1) {
return values[0];
}
return localize('com_ui_x_selected', { 0: values.length });
},
[localize],
);
const renderItemContent = useCallback(
(serverName: string, defaultContent: React.ReactNode) => {
const statusIconProps = getServerStatusIconProps(serverName);
const isServerInitializing = isInitializing(serverName);
/**
Common wrapper for the main content (check mark + text).
Ensures Check & Text are adjacent and the group takes available space.
*/
const mainContentWrapper = (
<button
type="button"
className={`flex flex-grow items-center rounded bg-transparent p-0 text-left transition-colors focus:outline-none ${
isServerInitializing ? 'opacity-50' : ''
}`}
tabIndex={0}
disabled={isServerInitializing}
>
{defaultContent}
</button>
);
const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />;
if (statusIcon) {
return (
<div className="flex w-full items-center justify-between">
{mainContentWrapper}
<div className="ml-2 flex items-center">{statusIcon}</div>
</div>
);
}
return mainContentWrapper;
},
[getServerStatusIconProps, isInitializing],
);
if (!isPinned && mcpValues?.length === 0) {
return null;
}
const configDialogProps = getConfigDialogProps();
return (
<>
<MultiSelect
items={configuredServers}
selectedValues={mcpValues ?? []}
setSelectedValues={batchToggleServers}
renderSelectedValues={renderSelectedValues}
renderItemContent={renderItemContent}
placeholder={placeholderText}
popoverClassName="min-w-fit"
className="badge-icon min-w-fit"
selectIcon={<MCPIcon className="icon-md text-text-primary" />}
selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
/>
{configDialogProps && (
<MCPConfigDialog {...configDialogProps} conversationId={conversationId} />
)}
</>
);
}
function MCPSelect() {
const { mcpServerManager } = useBadgeRowContext();
const { configuredServers } = mcpServerManager;
if (!configuredServers || configuredServers.length === 0) {
return null;
}
return <MCPSelectContent />;
}
export default memo(MCPSelect);