feat(ui): migrate chat chrome to shadcn

- ChatMessages: chrome-only migration; antd Collapse/Panel -> shadcn Accordion for tool call card; antd Tooltip -> shadcn Tooltip for edit/copy action buttons. Message rendering, streaming state, markdown/syntax-highlighter internals untouched.
- ChatPage: antd Tooltip/Skeleton/Popover -> shadcn equivalents for sidebar nav items, model selector, MCP connect picker, and top-bar actions. Sidebar layout, storage banner, and chat input textarea preserved.
- ConversationList: antd Button/Input/Modal/Popconfirm/Tooltip/Avatar/Typography -> shadcn equivalents. Modal (Cmd+K search) -> Dialog; Popconfirm -> custom Popover with Cancel/Delete buttons; Avatar -> shadcn Avatar+Fallback.
- MCPAppsPanel: antd Spin/Input/Button/Skeleton -> shadcn Button/Input/Skeleton + LoaderCircle spinner shim; Input with prefix icon rebuilt as absolute-positioned icon + padded input.
- Add ChatMessages.tsx to no-raw-tailwind-colors override (inline code styling in markdown renderer uses raw palette).

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 12:13:02 +00:00
parent a5b2a32b4a
commit 75e172b0e0
No known key found for this signature in database
5 changed files with 470 additions and 271 deletions

View file

@ -152,6 +152,7 @@
"src/components/playground/chat_ui/AgentBuilderView.tsx",
"src/components/playground/chat_ui/RealtimePlayground.tsx",
"src/components/playground/compareUI/CompareUI.tsx",
"src/components/chat/ChatMessages.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx",
"src/components/molecules/models/columns.tsx",

View file

@ -1,7 +1,23 @@
"use client";
import { Wrench as ToolOutlined, Copy as CopyOutlined, Check as CheckOutlined, Pencil as EditOutlined } from "lucide-react";
import { Collapse, Tooltip } from "antd";
import {
Wrench as ToolOutlined,
Copy as CopyOutlined,
Check as CheckOutlined,
Pencil as EditOutlined,
} from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import React, { useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@ -11,8 +27,6 @@ import ReasoningContent from "../playground/chat_ui/ReasoningContent";
import MCPEventsDisplay from "../playground/chat_ui/MCPEventsDisplay";
import { ChatMessage } from "./types";
const { Panel } = Collapse;
// Keys whose values must be redacted in tool args display
const REDACTED_KEY_PATTERNS = /token|key|secret|password|auth/i;
@ -194,22 +208,41 @@ function UserBubble({ message, onEdit, isStreaming }: UserBubbleProps) {
<div style={{ display: "flex", alignItems: "flex-end", gap: 6, maxWidth: "72%" }}>
{/* Edit button — appears on hover, to the left of the bubble */}
{hovered && !isStreaming && onEdit && (
<Tooltip title="Edit message">
<button
onClick={() => { setEditValue(message.content); setEditing(true); }}
style={{
background: "none", border: "none", cursor: "pointer",
padding: "4px 6px", borderRadius: 5,
color: "#9ca3af", fontSize: 13, flexShrink: 0,
display: "flex", alignItems: "center",
transition: "color 0.15s",
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.color = "#6b7280"; }}
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.color = "#9ca3af"; }}
>
<EditOutlined />
</button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => {
setEditValue(message.content);
setEditing(true);
}}
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: "4px 6px",
borderRadius: 5,
color: "#9ca3af",
fontSize: 13,
flexShrink: 0,
display: "flex",
alignItems: "center",
transition: "color 0.15s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLButtonElement).style.color = "#6b7280";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLButtonElement).style.color = "#9ca3af";
}}
aria-label="Edit message"
>
<EditOutlined className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent>Edit message</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<div
style={{
@ -348,32 +381,40 @@ function CopyButton({ text }: { text: string }) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 6 }}>
<Tooltip title={copied ? "Copied!" : "Copy"}>
<button
onClick={handleCopy}
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: "4px 6px",
borderRadius: 5,
color: copied ? "#52c41a" : "#9ca3af",
fontSize: 13,
display: "flex",
alignItems: "center",
gap: 4,
transition: "color 0.15s",
}}
onMouseEnter={(e) => {
if (!copied) (e.currentTarget as HTMLButtonElement).style.color = "#6b7280";
}}
onMouseLeave={(e) => {
if (!copied) (e.currentTarget as HTMLButtonElement).style.color = "#9ca3af";
}}
>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleCopy}
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: "4px 6px",
borderRadius: 5,
color: copied ? "#52c41a" : "#9ca3af",
fontSize: 13,
display: "flex",
alignItems: "center",
gap: 4,
transition: "color 0.15s",
}}
onMouseEnter={(e) => {
if (!copied)
(e.currentTarget as HTMLButtonElement).style.color = "#6b7280";
}}
onMouseLeave={(e) => {
if (!copied)
(e.currentTarget as HTMLButtonElement).style.color = "#9ca3af";
}}
aria-label={copied ? "Copied!" : "Copy"}
>
{copied ? <CheckOutlined className="h-3.5 w-3.5" /> : <CopyOutlined className="h-3.5 w-3.5" />}
</button>
</TooltipTrigger>
<TooltipContent>{copied ? "Copied!" : "Copy"}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
@ -445,25 +486,32 @@ function ToolCard({ message }: ToolCardProps) {
return (
<div style={{ maxWidth: "80%" }}>
<Collapse
size="small"
<Accordion
type="single"
collapsible
style={{
backgroundColor: "#fafafa",
border: "1px solid #e5e7eb",
borderRadius: 8,
}}
>
<Panel
header={
<span style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 13 }}>
<ToolOutlined style={{ color: "#6b7280" }} />
<AccordionItem value="tool" className="border-0">
<AccordionTrigger className="px-3 py-2 hover:no-underline">
<span
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 13,
}}
>
<ToolOutlined className="h-3.5 w-3.5 text-muted-foreground" />
<span style={{ color: "#374151", fontWeight: 500 }}>
{message.toolName ?? "Tool call"}
</span>
</span>
}
key="tool"
>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3">
{redactedArgs !== undefined && (
<div style={{ marginBottom: message.toolResult ? 12 : 0 }}>
<div
@ -525,8 +573,9 @@ function ToolCard({ message }: ToolCardProps) {
</div>
</div>
)}
</Panel>
</Collapse>
</AccordionContent>
</AccordionItem>
</Accordion>
<div style={{ fontSize: 11, color: "#9ca3af", marginTop: 4 }}>
{formatTimestamp(message.timestamp)}
</div>

View file

@ -1,9 +1,20 @@
"use client";
import React, { useCallback, useEffect, useRef, useState, useLayoutEffect } from "react";
import { Tooltip, Skeleton, Popover } from "antd";
import { Skeleton } from "@/components/ui/skeleton";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import MessageManager from "@/components/molecules/message_manager";
import { Settings as SettingOutlined, Plus as PlusOutlined, Pencil as EditOutlined, PanelLeftClose as MenuFoldOutlined, PanelLeft as MenuUnfoldOutlined, Search as SearchOutlined, MessageSquare as MessageOutlined, LayoutGrid as AppstoreOutlined, Key as KeyOutlined, ArrowLeft as ArrowLeftOutlined, ChevronDown as DownOutlined, X as CloseOutlined, Check as CheckOutlined } from "lucide-react";
import { Settings as SettingOutlined, Plus as PlusOutlined, Pencil as EditOutlined, PanelLeftClose as MenuFoldOutlined, PanelLeft as MenuUnfoldOutlined, Search as SearchOutlined, MessageSquare as MessageOutlined, LayoutGrid as AppstoreOutlined, Key as KeyOutlined, ArrowLeft as ArrowLeftOutlined, ChevronDown as DownOutlined, Check as CheckOutlined } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { useRouter, useSearchParams } from "next/navigation";
@ -583,7 +594,9 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
active = false,
kbd?: string,
) => (
<Tooltip title={sidebarCollapsed ? label : undefined} placement="right" key={label}>
<TooltipProvider key={label}>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={onClick}
style={{
@ -617,20 +630,23 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
</>
)}
</button>
</Tooltip>
</TooltipTrigger>
{sidebarCollapsed && (
<TooltipContent side="right">{label}</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
);
// ---- Model selector trigger button ----
const modelSelectorTrigger = isLoadingModels ? (
<Skeleton.Input active style={{ width: 160, height: 28 }} />
<Skeleton className="w-40 h-7" />
) : (
<Popover
open={modelSelectorOpen}
onOpenChange={(open) => { setModelSelectorOpen(open); if (!open) setModelSearchText(""); }}
content={modelSelectorContent}
trigger="click"
placement="bottomLeft"
>
<PopoverTrigger asChild>
<button
style={{
display: "flex",
@ -683,6 +699,10 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
)}
<DownOutlined style={{ fontSize: 10, color: "#9ca3af", flexShrink: 0, marginLeft: 2 }} />
</button>
</PopoverTrigger>
<PopoverContent align="start" className="p-0 w-auto">
{modelSelectorContent}
</PopoverContent>
</Popover>
);
@ -722,32 +742,29 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
padding: inConversation ? "4px 12px 10px" : "8px 12px 12px",
borderTop: "1px solid #f3f4f6",
}}>
<Popover
open={mcpPopoverOpen}
onOpenChange={setMcpPopoverOpen}
content={
<Popover open={mcpPopoverOpen} onOpenChange={setMcpPopoverOpen}>
<PopoverTrigger asChild>
<button style={{
background: "none", border: "1px solid #d1d5db",
borderRadius: 6, padding: "5px 10px",
cursor: "pointer", fontSize: 14, color: "#6b7280",
display: "flex", alignItems: "center", gap: 4,
}}>
<PlusOutlined />
{selectedMCPServers.length > 0 && (
<span style={{ fontSize: 12, color: "#1677ff", fontWeight: 500 }}>
{selectedMCPServers.length}
</span>
)}
</button>
</PopoverTrigger>
<PopoverContent side="top" align="start" className="p-0 w-auto">
<MCPConnectPicker
accessToken={accessToken}
selectedServers={selectedMCPServers}
onChange={setSelectedMCPServers}
/>
}
trigger="click"
placement="topLeft"
>
<button style={{
background: "none", border: "1px solid #d1d5db",
borderRadius: 6, padding: "5px 10px",
cursor: "pointer", fontSize: 14, color: "#6b7280",
display: "flex", alignItems: "center", gap: 4,
}}>
<PlusOutlined />
{selectedMCPServers.length > 0 && (
<span style={{ fontSize: 12, color: "#1677ff", fontWeight: 500 }}>
{selectedMCPServers.length}
</span>
)}
</button>
</PopoverContent>
</Popover>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
@ -834,18 +851,26 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
</span>
</div>
)}
<Tooltip title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} placement="right">
<button
onClick={() => setSidebarCollapsed((v) => !v)}
style={{
background: "none", border: "none", cursor: "pointer",
padding: 6, borderRadius: 7, color: "#6b7280", fontSize: 16,
display: "flex", alignItems: "center",
}}
>
{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setSidebarCollapsed((v) => !v)}
style={{
background: "none", border: "none", cursor: "pointer",
padding: 6, borderRadius: 7, color: "#6b7280", fontSize: 16,
display: "flex", alignItems: "center",
}}
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button>
</TooltipTrigger>
<TooltipContent side="right">
{sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Sidebar nav buttons */}
@ -861,35 +886,44 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
{sidebarNavItem(<MessageOutlined />, "Chats", () => setSidebarView("chats"), sidebarView === "chats")}
{sidebarNavItem(<AppstoreOutlined />, "Apps", () => setSidebarView("apps"), sidebarView === "apps")}
{sidebarNavItem(<KeyOutlined />, "Credentials", () => setSidebarView("credentials"), sidebarView === "credentials")}
<Tooltip title={sidebarCollapsed ? "Back to Developer Console UI" : undefined} placement="right">
<a
href={dashboardUrl}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 10px",
width: "100%",
borderRadius: 7,
color: "#6b7280",
textDecoration: "none",
fontSize: 14,
justifyContent: sidebarCollapsed ? "center" : "flex-start",
boxSizing: "border-box",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background = "#f5f5f5";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background = "transparent";
}}
>
<ArrowLeftOutlined style={{ fontSize: 16, flexShrink: 0 }} />
{!sidebarCollapsed && (
<span>Back to Developer Console UI</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<a
href={dashboardUrl}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 10px",
width: "100%",
borderRadius: 7,
color: "#6b7280",
textDecoration: "none",
fontSize: 14,
justifyContent: sidebarCollapsed ? "center" : "flex-start",
boxSizing: "border-box",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background = "#f5f5f5";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background = "transparent";
}}
>
<ArrowLeftOutlined style={{ fontSize: 16, flexShrink: 0 }} />
{!sidebarCollapsed && (
<span>Back to Developer Console UI</span>
)}
</a>
</TooltipTrigger>
{sidebarCollapsed && (
<TooltipContent side="right">
Back to Developer Console UI
</TooltipContent>
)}
</a>
</Tooltip>
</Tooltip>
</TooltipProvider>
</div>
<div style={{ height: 1, background: "#e5e7eb", margin: "4px 8px", flexShrink: 0 }} />
@ -931,15 +965,23 @@ const ChatPage: React.FC<ChatPageProps> = ({ accessToken, userRole, userId, user
{/* Right: settings */}
<div style={{ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }}>
<Tooltip title="Settings">
<button style={{
background: "none", border: "none", cursor: "pointer",
padding: 7, borderRadius: 7, color: "#6b7280", fontSize: 16,
display: "flex", alignItems: "center",
}}>
<SettingOutlined />
</button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
style={{
background: "none", border: "none", cursor: "pointer",
padding: 7, borderRadius: 7, color: "#6b7280", fontSize: 16,
display: "flex", alignItems: "center",
}}
aria-label="Settings"
>
<SettingOutlined />
</button>
</TooltipTrigger>
<TooltipContent>Settings</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>

View file

@ -1,20 +1,52 @@
"use client";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Button,
Input,
Modal,
Popconfirm,
Tooltip,
Avatar,
Typography,
} from "antd";
import { Pencil as EditOutlined, Trash2 as DeleteOutlined, Plus as PlusOutlined, Search as SearchOutlined, User as UserOutlined, MessageSquare as MessageOutlined } from "lucide-react";
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Pencil as EditOutlined,
Trash2 as DeleteOutlined,
Plus as PlusOutlined,
Search as SearchOutlined,
User as UserOutlined,
MessageSquare as MessageOutlined,
} from "lucide-react";
import dayjs from "dayjs";
import { Conversation } from "./types";
const { Text } = Typography;
const Text = ({
className,
style,
children,
title,
}: {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
title?: string;
}) => (
<span className={className} style={style} title={title}>
{children}
</span>
);
interface Props {
conversations: Conversation[];
@ -61,6 +93,51 @@ const groupConversations = (conversations: Conversation[]): GroupedConversations
}));
};
// ---- Delete confirmation popover (replaces antd Popconfirm) ----
const DeleteConfirmPopover: React.FC<{ onConfirm: () => void }> = ({
onConfirm,
}) => {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
aria-label="Delete"
className="h-[22px] w-[22px] p-0 min-w-[22px] text-destructive hover:text-destructive"
onClick={(e) => e.stopPropagation()}
>
<DeleteOutlined className="h-3 w-3" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-56"
align="end"
onClick={(e) => e.stopPropagation()}
>
<p className="text-sm mb-3">Delete this conversation?</p>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => {
setOpen(false);
onConfirm();
}}
>
Delete
</Button>
</div>
</PopoverContent>
</Popover>
);
};
// ---- Single conversation row ----
interface ConversationRowProps {
@ -149,16 +226,13 @@ const ConversationRow: React.FC<ConversationRowProps> = ({
>
{editing ? (
<Input
ref={(node) => {
inputRef.current = node?.input ?? null;
}}
size="small"
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={commitRename}
onClick={(e) => e.stopPropagation()}
style={{ flex: 1, fontSize: 13 }}
className="flex-1 h-7 text-[13px]"
/>
) : (
<>
@ -189,32 +263,23 @@ const ConversationRow: React.FC<ConversationRowProps> = ({
}}
onClick={(e) => e.stopPropagation()}
>
<Tooltip title="Rename">
<Button
type="text"
size="small"
icon={<EditOutlined style={{ fontSize: 12 }} />}
onClick={startEditing}
style={{ width: 22, height: 22, padding: 0, minWidth: 22 }}
/>
</Tooltip>
<Popconfirm
title="Delete this conversation?"
onConfirm={() => onDelete(conv.id)}
okText="Delete"
cancelText="Cancel"
okButtonProps={{ danger: true }}
>
<Tooltip title="Delete">
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined style={{ fontSize: 12 }} />}
style={{ width: 22, height: 22, padding: 0, minWidth: 22 }}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={startEditing}
aria-label="Rename"
className="h-[22px] w-[22px] p-0 min-w-[22px]"
>
<EditOutlined className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Rename</TooltipContent>
</Tooltip>
</Popconfirm>
</TooltipProvider>
<DeleteConfirmPopover onConfirm={() => onDelete(conv.id)} />
</div>
</>
)}
@ -255,67 +320,51 @@ const SearchModal: React.FC<SearchModalProps> = ({
};
return (
<Modal
open={open}
onCancel={onClose}
footer={null}
title={null}
width={480}
styles={{ body: { padding: "16px 16px 8px" } }}
>
<Input
autoFocus
prefix={<SearchOutlined style={{ color: "#bbb" }} />}
placeholder="Search conversations…"
value={query}
onChange={(e) => setQuery(e.target.value)}
style={{ marginBottom: 12 }}
allowClear
/>
<Dialog open={open} onOpenChange={(o) => (!o ? onClose() : undefined)}>
<DialogContent className="sm:max-w-[480px] p-4">
<DialogHeader className="sr-only">
<DialogTitle>Search conversations</DialogTitle>
</DialogHeader>
<div className="relative mb-3">
<SearchOutlined className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
autoFocus
placeholder="Search conversations…"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9"
/>
</div>
<div style={{ maxHeight: 320, overflowY: "auto" }}>
{filtered.length === 0 ? (
<div style={{ textAlign: "center", padding: "24px 0", color: "#999" }}>
No conversations found
</div>
) : (
filtered.map((conv) => {
const truncated =
conv.title.length > 55 ? conv.title.slice(0, 55) + "…" : conv.title;
return (
<div
key={conv.id}
onClick={() => handleSelect(conv.id)}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "8px 10px",
borderRadius: 6,
cursor: "pointer",
transition: "background-color 0.1s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLDivElement).style.backgroundColor = "#f0f5ff";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLDivElement).style.backgroundColor = "transparent";
}}
>
<MessageOutlined style={{ color: "#999", flexShrink: 0 }} />
<Text style={{ fontSize: 13 }}>{truncated}</Text>
<Text
type="secondary"
style={{ fontSize: 11, marginLeft: "auto", flexShrink: 0 }}
<div style={{ maxHeight: 320, overflowY: "auto" }}>
{filtered.length === 0 ? (
<div className="text-center py-6 text-muted-foreground">
No conversations found
</div>
) : (
filtered.map((conv) => {
const truncated =
conv.title.length > 55
? conv.title.slice(0, 55) + "…"
: conv.title;
return (
<div
key={conv.id}
onClick={() => handleSelect(conv.id)}
className="flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer hover:bg-accent"
>
{dayjs(conv.updatedAt).format("MMM D")}
</Text>
</div>
);
})
)}
</div>
</Modal>
<MessageOutlined className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<Text style={{ fontSize: 13 }}>{truncated}</Text>
<span className="ml-auto flex-shrink-0 text-xs text-muted-foreground">
{dayjs(conv.updatedAt).format("MMM D")}
</span>
</div>
);
})
)}
</div>
</DialogContent>
</Dialog>
);
};
@ -366,19 +415,23 @@ const ConversationList: React.FC<Props> = ({
>
{/* Top: New Chat button */}
<div style={{ padding: "12px 10px 8px" }}>
<Tooltip
title="Chats are saved locally in this browser. All requests are logged in Spend → Logs."
placement="right"
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onNewChat}
style={{ width: "100%" }}
>
New Chat
</Button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={onNewChat}
className="w-full"
>
<PlusOutlined className="mr-2 h-4 w-4" />
New Chat
</Button>
</TooltipTrigger>
<TooltipContent side="right">
Chats are saved locally in this browser. All requests are
logged in Spend → Logs.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Conversation list (scrollable) */}
@ -443,11 +496,14 @@ const ConversationList: React.FC<Props> = ({
gap: 8,
}}
>
<Avatar
size={28}
icon={<UserOutlined />}
style={{ backgroundColor: "#e0e7ff", color: "#4f46e5", flexShrink: 0 }}
/>
<Avatar className="h-7 w-7 flex-shrink-0">
<AvatarFallback
// eslint-disable-next-line litellm-ui/no-raw-tailwind-colors
className="bg-indigo-100 text-indigo-600"
>
<UserOutlined className="h-3.5 w-3.5" />
</AvatarFallback>
</Avatar>
<Text
style={{
fontSize: 13,

View file

@ -1,8 +1,32 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Spin, Input, Button, Skeleton } from "antd";
import { Search as SearchOutlined, ArrowLeft as ArrowLeftOutlined, ChevronRight as RightOutlined, Wrench as ToolOutlined, CheckCircle2 as CheckCircleOutlined } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import {
Search as SearchOutlined,
ArrowLeft as ArrowLeftOutlined,
ChevronRight as RightOutlined,
Wrench as ToolOutlined,
CheckCircle2 as CheckCircleOutlined,
LoaderCircle,
} from "lucide-react";
const Spin = ({
size,
style,
}: {
size?: "small" | "default" | "large";
style?: React.CSSProperties;
}) => (
<LoaderCircle
style={style}
className={`animate-spin ${
size === "small" ? "w-3 h-3" : size === "large" ? "w-6 h-6" : "w-4 h-4"
} text-muted-foreground`}
/>
);
import { deleteMCPOAuthUserCredential, fetchMCPServers, getMCPOAuthUserCredentialStatus, listMCPTools } from "../networking";
import { AUTH_TYPE, MCPServer, MCPTool, handleTransport } from "../mcp_tools/types";
import MessageManager from "@/components/molecules/message_manager";
@ -37,12 +61,18 @@ const OAuth2ConnectButton: React.FC<OAuth2ConnectButtonProps> = ({
if (variant === "button") {
return (
<Button
type="primary"
loading={loading}
onClick={startOAuthFlow}
disabled={loading}
style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }}
>
{loading ? "Connecting…" : "Connect"}
{loading ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
Connecting…
</>
) : (
"Connect"
)}
</Button>
);
}
@ -310,8 +340,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
{detailServer.auth_type === AUTH_TYPE.OAUTH2 ? (
oauthConnected.has(detailServer.server_id) ? (
<Button
type="default"
danger
variant="destructive"
onClick={async () => {
try {
await deleteMCPOAuthUserCredential(accessToken, detailServer.server_id);
@ -337,12 +366,21 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
)
) : (
<Button
type={isConnected ? "default" : "primary"}
loading={isTogglingOn}
variant={isConnected ? "outline" : "default"}
disabled={isTogglingOn}
onClick={() => handleToggle(name, !isConnected, detailServer.server_id)}
style={{ borderRadius: 8, fontWeight: 600, height: 38, minWidth: 110 }}
>
{isConnected ? "Disconnect" : "Connect"}
{isTogglingOn ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
{isConnected ? "Disconnecting…" : "Connecting…"}
</>
) : isConnected ? (
"Disconnect"
) : (
"Connect"
)}
</Button>
)}
</div>
@ -439,15 +477,28 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
) : null}
</div>
</div>
<Input
prefix={<SearchOutlined style={{ color: "#9ca3af", fontSize: 13 }} />}
placeholder="Search servers..."
value={query}
onChange={(e) => setQuery(e.target.value)}
allowClear
style={{ width: 220, borderRadius: 8, fontSize: 13 }}
size="middle"
/>
<div className="relative w-[220px]">
<SearchOutlined
// eslint-disable-next-line litellm-ui/no-raw-tailwind-colors
className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-400"
/>
<Input
placeholder="Search servers..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 pr-8 h-9 text-[13px] rounded-lg"
/>
{query && (
<button
type="button"
onClick={() => setQuery("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground text-xs"
aria-label="Clear"
>
×
</button>
)}
</div>
</div>
{/* Tabs */}
@ -545,7 +596,7 @@ const MCPAppsPanel: React.FC<Props> = ({ accessToken, selectedServers, onChange
</span>
) : null
) : loadingCounts ? (
<Skeleton.Input active size="small" style={{ width: 28, height: 12, minWidth: 28, flexShrink: 0 }} />
<Skeleton className="w-7 h-3 flex-shrink-0" />
) : null}
</div>
</div>