mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): chat/MCPConnectPicker to shadcn
- antd Switch/Spin \u2192 shadcn Switch + lucide Loader2 (replacing the Switch's loading prop with a separate inline spinner since shadcn Switch has no loading slot). - Inline style attributes replaced with Tailwind utilities + semantic tokens (text-muted-foreground / text-foreground). Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
e1c0b5291b
commit
4d9072bf3e
1 changed files with 34 additions and 62 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Switch, Spin } from "antd";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { fetchMCPServers, listMCPTools } from "../networking";
|
||||
import { MCPServer } from "../mcp_tools/types";
|
||||
|
|
@ -10,10 +11,13 @@ interface Props {
|
|||
onChange: (servers: string[]) => void;
|
||||
}
|
||||
|
||||
const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onChange }) => {
|
||||
const MCPConnectPicker: React.FC<Props> = ({
|
||||
accessToken,
|
||||
selectedServers,
|
||||
onChange,
|
||||
}) => {
|
||||
const [servers, setServers] = useState<MCPServer[]>([]);
|
||||
const [loadingServers, setLoadingServers] = useState(true);
|
||||
// Track which individual servers are being toggled on (verifying tools)
|
||||
const [togglingOn, setTogglingOn] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -24,8 +28,9 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
try {
|
||||
const data = await fetchMCPServers(accessToken);
|
||||
if (cancelled) return;
|
||||
// API returns { data: MCPServer[] } or MCPServer[]
|
||||
const list: MCPServer[] = Array.isArray(data) ? data : (data?.data ?? []);
|
||||
const list: MCPServer[] = Array.isArray(data)
|
||||
? data
|
||||
: (data?.data ?? []);
|
||||
setServers(list);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
|
|
@ -47,29 +52,24 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
|
||||
const handleToggle = async (serverName: string, checked: boolean) => {
|
||||
if (!checked) {
|
||||
// Toggle OFF — remove immediately, no tool fetch needed
|
||||
onChange(selectedServers.filter((s) => s !== serverName));
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle ON — verify tools are reachable first
|
||||
setTogglingOn((prev) => new Set(prev).add(serverName));
|
||||
try {
|
||||
const result = await listMCPTools(accessToken, serverName);
|
||||
// listMCPTools never throws; it returns { tools, error, message } on failure
|
||||
if (result?.error) {
|
||||
MessageManager.warning(
|
||||
`Could not load tools for ${serverName} — it will be excluded from this message.`
|
||||
`Could not load tools for ${serverName} — it will be excluded from this message.`,
|
||||
);
|
||||
// Do not add to selectedServers
|
||||
return;
|
||||
}
|
||||
onChange([...selectedServers, serverName]);
|
||||
} catch {
|
||||
MessageManager.warning(
|
||||
`Could not load tools for ${serverName} — it will be excluded from this message.`
|
||||
`Could not load tools for ${serverName} — it will be excluded from this message.`,
|
||||
);
|
||||
// Do not add to selectedServers
|
||||
} finally {
|
||||
setTogglingOn((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -80,20 +80,13 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 320,
|
||||
maxHeight: 400,
|
||||
overflowY: "auto",
|
||||
padding: "8px 0",
|
||||
}}
|
||||
>
|
||||
<div className="max-w-[320px] max-h-[400px] overflow-y-auto py-2">
|
||||
{loadingServers ? (
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: "24px 0" }}>
|
||||
<Spin />
|
||||
<div className="flex justify-center py-6">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : servers.length === 0 ? (
|
||||
<div style={{ padding: "16px 12px", color: "#8c8c8c", fontSize: 13, textAlign: "center" }}>
|
||||
<div className="px-3 py-4 text-muted-foreground text-sm text-center">
|
||||
No MCP servers configured
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -105,60 +98,39 @@ const MCPConnectPicker: React.FC<Props> = ({ accessToken, selectedServers, onCha
|
|||
return (
|
||||
<div
|
||||
key={server.server_id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 12px",
|
||||
gap: 12,
|
||||
}}
|
||||
className="flex items-start justify-between px-3 py-2 gap-3"
|
||||
>
|
||||
{server.mcp_info?.logo_url && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={server.mcp_info.logo_url}
|
||||
alt={`${name} logo`}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 6,
|
||||
objectFit: "contain", flexShrink: 0,
|
||||
marginTop: 1,
|
||||
className="w-6 h-6 rounded-md object-contain shrink-0 mt-0.5"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
color: "#1f1f1f",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-foreground truncate">
|
||||
{name}
|
||||
</div>
|
||||
{server.description && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "#8c8c8c",
|
||||
marginTop: 2,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<div className="text-xs text-muted-foreground mt-0.5 truncate">
|
||||
{server.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isSelected}
|
||||
loading={isTogglingOn}
|
||||
onChange={(checked) => handleToggle(name, checked)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
{isTogglingOn && (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground mr-1" />
|
||||
)}
|
||||
<Switch
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => handleToggle(name, checked)}
|
||||
disabled={isTogglingOn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue