mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
a5b2a32b4a
commit
75e172b0e0
5 changed files with 470 additions and 271 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue