feat(ui): MessageBubble + SessionManagement to shadcn

- conversation_panel/MessageBubble: @ant-design UserOutlined/RobotOutlined
  \u2192 lucide User/Bot. Inline style hex colors replaced with categorical
  blue (user) / muted (assistant) Tailwind classes. File added to
  overrides.
- SessionManagement: antd Switch/Tooltip + @ant-design InfoCircle/
  CopyOutlined \u2192 shadcn Switch/Tooltip + lucide Info/Copy. UI/API
  label pair flanks the Switch since shadcn Switch has no checkedChildren/
  unCheckedChildren slots. Session-active banner uses categorical
  emerald/blue palette.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 12:24:52 +00:00
parent 381cb8a614
commit c1d0451c9b
No known key found for this signature in database
3 changed files with 127 additions and 61 deletions

View file

@ -60,6 +60,8 @@
"src/components/guardrails/content_filter/ContentFilterDisplay.tsx",
"src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx",
"src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx",
"src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx",
"src/components/playground/chat_ui/SessionManagement.tsx",
"src/components/policies/impact_preview_alert.tsx",
"src/components/policies/impact_popover.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",

View file

@ -1,6 +1,13 @@
import React from "react";
import { Switch, Tooltip } from "antd";
import { InfoCircleOutlined, CopyOutlined } from "@ant-design/icons";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Copy, Info } from "lucide-react";
import { cn } from "@/lib/utils";
import { EndpointType } from "./mode_endpoint_mapping";
import NotificationsManager from "../../molecules/notifications_manager";
@ -30,10 +37,14 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
const getSessionDisplay = () => {
if (!responsesSessionId) {
return useApiSessionManagement ? "API Session: Ready" : "UI Session: Ready";
return useApiSessionManagement
? "API Session: Ready"
: "UI Session: Ready";
}
const sessionPrefix = useApiSessionManagement ? "Response ID" : "UI Session";
const sessionPrefix = useApiSessionManagement
? "Response ID"
: "UI Session";
const truncatedId = responsesSessionId.slice(0, 10);
return `${sessionPrefix}: ${truncatedId}...`;
};
@ -52,43 +63,83 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
return (
<div className="mb-4">
{/* Session Management Toggle */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700">Session Management</span>
<Tooltip title="Choose between LiteLLM API session management (using previous_response_id) or UI-based session management (using chat history)">
<InfoCircleOutlined className="text-gray-400" style={{ fontSize: "12px" }} />
</Tooltip>
<span className="text-sm font-medium text-foreground">
Session Management
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Choose between LiteLLM API session management (using
previous_response_id) or UI-based session management (using
chat history)
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex items-center gap-1.5 text-xs">
<span
className={cn(
useApiSessionManagement
? "text-muted-foreground"
: "text-foreground",
)}
>
UI
</span>
<Switch
checked={useApiSessionManagement}
onCheckedChange={onToggleSessionManagement}
/>
<span
className={cn(
useApiSessionManagement
? "text-foreground"
: "text-muted-foreground",
)}
>
API
</span>
</div>
<Switch
checked={useApiSessionManagement}
onChange={onToggleSessionManagement}
checkedChildren="API"
unCheckedChildren="UI"
size="small"
/>
</div>
{/* Session Status Indicator */}
<div
className={`text-xs p-2 rounded-md ${
className={cn(
"text-xs p-2 rounded-md border",
responsesSessionId
? "bg-green-50 text-green-700 border border-green-200"
: "bg-blue-50 text-blue-700 border border-blue-200"
}`}
? "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900 dark:text-emerald-300"
: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900 dark:text-blue-300",
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<InfoCircleOutlined style={{ fontSize: "12px" }} />
<Info className="h-3 w-3" />
{getSessionDisplay()}
</div>
{responsesSessionId && (
<Tooltip
title={
<div className="text-xs">
<div className="mb-1">Copy response ID to continue session:</div>
<div className="bg-gray-800 text-gray-100 p-2 rounded font-mono text-xs whitespace-pre-wrap">
{`curl -X POST "your-proxy-url/v1/responses" \\
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleCopySessionId}
className="ml-2 p-1 hover:bg-emerald-100 dark:hover:bg-emerald-900 rounded transition-colors"
aria-label="Copy response ID"
>
<Copy className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-[500px]">
<div className="text-xs">
<div className="mb-1">
Copy response ID to continue session:
</div>
<div className="bg-gray-800 text-gray-100 p-2 rounded font-mono text-xs whitespace-pre-wrap">
{`curl -X POST "your-proxy-url/v1/responses" \\
-H "Authorization: Bearer your-api-key" \\
-H "Content-Type: application/json" \\
-d '{
@ -97,18 +148,16 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
"previous_response_id": "${responsesSessionId}",
"stream": true
}'`}
</div>
</div>
</div>
}
overlayStyle={{ maxWidth: "500px" }}
>
<button onClick={handleCopySessionId} className="ml-2 p-1 hover:bg-green-100 rounded transition-colors">
<CopyOutlined style={{ fontSize: "12px" }} />
</button>
</Tooltip>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="text-xs opacity-75 mt-1">{getSessionDescription()}</div>
<div className="text-xs opacity-75 mt-1">
{getSessionDescription()}
</div>
</div>
</div>
);

View file

@ -1,41 +1,51 @@
import React from "react";
import { RobotOutlined, UserOutlined } from "@ant-design/icons";
import { Bot, User } from "lucide-react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
import ResponseMetrics from "../../../playground/chat_ui/ResponseMetrics";
import { Message } from "./types";
import { cn } from "@/lib/utils";
interface MessageBubbleProps {
message: Message;
}
const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
const isUser = message.role === "user";
return (
<div className={`mb-4 flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
<div
className={cn(
"mb-4 flex",
isUser ? "justify-end" : "justify-start",
)}
>
<div
className="max-w-[85%] rounded-lg shadow-sm p-3.5 px-4"
style={{
backgroundColor: message.role === "user" ? "#f0f8ff" : "#ffffff",
border: message.role === "user" ? "1px solid #e6f0fa" : "1px solid #f0f0f0",
}}
className={cn(
"max-w-[85%] rounded-lg shadow-sm p-3.5 px-4 border",
isUser
? "bg-blue-50 border-blue-100 dark:bg-blue-950/40 dark:border-blue-900"
: "bg-background border-border",
)}
>
<div className="flex items-center gap-2 mb-1.5">
<div
className="flex items-center justify-center w-6 h-6 rounded-full mr-1"
style={{
backgroundColor: message.role === "user" ? "#e6f0fa" : "#f5f5f5",
}}
className={cn(
"flex items-center justify-center w-6 h-6 rounded-full mr-1",
isUser
? "bg-blue-100 dark:bg-blue-950"
: "bg-muted",
)}
>
{message.role === "user" ? (
<UserOutlined style={{ fontSize: "12px", color: "#2563eb" }} />
{isUser ? (
<User className="h-3 w-3 text-blue-600 dark:text-blue-400" />
) : (
<RobotOutlined style={{ fontSize: "12px", color: "#4b5563" }} />
<Bot className="h-3 w-3 text-muted-foreground" />
)}
</div>
<strong className="text-sm capitalize">{message.role}</strong>
{message.role === "assistant" && message.model && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-normal">
{!isUser && message.model && (
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground font-normal">
{message.model}
</span>
)}
@ -50,22 +60,23 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
hyphens: "auto",
}}
>
{message.role === "assistant" ? (
{!isUser ? (
<ReactMarkdown
components={{
code({
node,
inline,
className,
children,
...props
}: React.ComponentPropsWithoutRef<"code"> & {
inline?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
node?: any;
}) {
const match = /language-(\w+)/.exec(className || "");
return !inline && match ? (
<SyntaxHighlighter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
style={coy as any}
language={match[1]}
PreTag="div"
@ -78,7 +89,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
</SyntaxHighlighter>
) : (
<code
className={`${className} px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono`}
className={`${className} px-1.5 py-0.5 rounded bg-muted text-sm font-mono`}
style={{ wordBreak: "break-word" }}
{...props}
>
@ -86,8 +97,11 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
</code>
);
},
pre: ({ node, ...props }) => (
<pre style={{ overflowX: "auto", maxWidth: "100%" }} {...props} />
pre: ({ ...props }) => (
<pre
style={{ overflowX: "auto", maxWidth: "100%" }}
{...props}
/>
),
}}
>
@ -97,8 +111,10 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
<div className="whitespace-pre-wrap">{message.content}</div>
)}
{message.role === "assistant" &&
(message.timeToFirstToken || message.totalLatency || message.usage) && (
{!isUser &&
(message.timeToFirstToken ||
message.totalLatency ||
message.usage) && (
<ResponseMetrics
timeToFirstToken={message.timeToFirstToken}
totalLatency={message.totalLatency}
@ -112,4 +128,3 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
};
export default MessageBubble;