mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): playground page tabs + multi_export_dropdown + conversation_panel + TableHeaderSortDropdown
- app/playground/page: @tremor TabGroup/TabList/Tab/TabPanel \u2192 shadcn Tabs/TabsList/TabsTrigger/TabsContent (default=chat). h-full routing preserved via h-full class on each TabsContent. - multi_export_dropdown: @tremor Button + @ant-design Download/ FilePdf/FileExcel \u2192 shadcn DropdownMenu + Button + lucide Download/FileText/FileSpreadsheet. Click-outside handler removed \u2014 Radix DropdownMenu handles dismissal. - conversation_panel/index: @tremor Button + @ant-design ClearOutlined \u2192 shadcn Button variant=outline + lucide Eraser. Semantic tokens bg-background/border-border. - TableHeaderSortDropdown: antd Dropdown + @heroicons SwitchVertical/ ChevronUp/ChevronDown/X \u2192 shadcn DropdownMenu + lucide ArrowUpDown/ ChevronUp/ChevronDown/X. Current sort state drives icon choice. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
2d876bfb8f
commit
2980e65157
4 changed files with 146 additions and 165 deletions
|
|
@ -5,7 +5,12 @@ import AgentBuilderView from "@/components/playground/chat_ui/AgentBuilderView";
|
|||
import ChatUI from "@/components/playground/chat_ui/ChatUI";
|
||||
import CompareUI from "@/components/playground/compareUI/CompareUI";
|
||||
import ComplianceUI from "@/components/playground/complianceUI/ComplianceUI";
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { fetchProxySettings } from "@/utils/proxyUtils";
|
||||
|
||||
|
|
@ -15,8 +20,16 @@ interface ProxySettings {
|
|||
}
|
||||
|
||||
export default function PlaygroundPage() {
|
||||
const { accessToken, userRole, userId, disabledPersonalKeyCreation, token } = useAuthorized();
|
||||
const [proxySettings, setProxySettings] = useState<ProxySettings | undefined>(undefined);
|
||||
const {
|
||||
accessToken,
|
||||
userRole,
|
||||
userId,
|
||||
disabledPersonalKeyCreation,
|
||||
token,
|
||||
} = useAuthorized();
|
||||
const [proxySettings, setProxySettings] = useState<ProxySettings | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeProxySettings = async () => {
|
||||
|
|
@ -36,15 +49,19 @@ export default function PlaygroundPage() {
|
|||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<TabGroup className="w-full" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
|
||||
<TabList className="mb-0">
|
||||
<Tab>Chat</Tab>
|
||||
<Tab>Compare</Tab>
|
||||
<Tab>Compliance</Tab>
|
||||
<Tab>Agent Builder (Experimental)</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="h-full">
|
||||
<TabPanel className="h-full">
|
||||
<Tabs
|
||||
defaultValue="chat"
|
||||
className="w-full flex-1 flex flex-col min-h-0"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="chat">Chat</TabsTrigger>
|
||||
<TabsTrigger value="compare">Compare</TabsTrigger>
|
||||
<TabsTrigger value="compliance">Compliance</TabsTrigger>
|
||||
<TabsTrigger value="agent-builder">
|
||||
Agent Builder (Experimental)
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="chat" className="h-full">
|
||||
<ChatUI
|
||||
accessToken={accessToken}
|
||||
token={token}
|
||||
|
|
@ -53,14 +70,20 @@ export default function PlaygroundPage() {
|
|||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="h-full">
|
||||
<CompareUI accessToken={accessToken} disabledPersonalKeyCreation={disabledPersonalKeyCreation} />
|
||||
</TabPanel>
|
||||
<TabPanel className="h-full">
|
||||
<ComplianceUI accessToken={accessToken} disabledPersonalKeyCreation={disabledPersonalKeyCreation} />
|
||||
</TabPanel>
|
||||
<TabPanel className="h-full">
|
||||
</TabsContent>
|
||||
<TabsContent value="compare" className="h-full">
|
||||
<CompareUI
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="compliance" className="h-full">
|
||||
<ComplianceUI
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="agent-builder" className="h-full">
|
||||
<AgentBuilderView
|
||||
accessToken={accessToken}
|
||||
token={token}
|
||||
|
|
@ -68,11 +91,13 @@ export default function PlaygroundPage() {
|
|||
userRole={userRole}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
proxySettings={proxySettings}
|
||||
customProxyBaseUrl={proxySettings?.LITELLM_UI_API_DOC_BASE_URL ?? proxySettings?.PROXY_BASE_URL}
|
||||
customProxyBaseUrl={
|
||||
proxySettings?.LITELLM_UI_API_DOC_BASE_URL ??
|
||||
proxySettings?.PROXY_BASE_URL
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "@tremor/react";
|
||||
import { DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Download, FileSpreadsheet, FileText } from "lucide-react";
|
||||
import { MultiModelResult } from "./types";
|
||||
import { exportMultiToPDF, exportMultiToCSV } from "./multi_export_utils";
|
||||
|
||||
|
|
@ -8,70 +14,35 @@ interface MultiExportDropdownProps {
|
|||
multiResult: MultiModelResult;
|
||||
}
|
||||
|
||||
const MultiExportDropdown: React.FC<MultiExportDropdownProps> = ({ multiResult }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const MultiExportDropdown: React.FC<MultiExportDropdownProps> = ({
|
||||
multiResult,
|
||||
}) => {
|
||||
const hasResults = multiResult.entries.some((e) => e.result !== null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!hasResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative inline-block" ref={menuRef}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
icon={DownloadOutlined}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
||||
<button
|
||||
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
onClick={() => {
|
||||
exportMultiToPDF(multiResult);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<FilePdfOutlined className="mr-3 text-red-500" />
|
||||
Export as PDF
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
onClick={() => {
|
||||
exportMultiToCSV(multiResult);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<FileExcelOutlined className="mr-3 text-green-600" />
|
||||
Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="sm">
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
Export
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuItem onClick={() => exportMultiToPDF(multiResult)}>
|
||||
<FileText className="mr-3 h-4 w-4 text-destructive" />
|
||||
Export as PDF
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => exportMultiToCSV(multiResult)}>
|
||||
<FileSpreadsheet className="mr-3 h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||
Export as CSV
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiExportDropdown;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,85 +1,62 @@
|
|||
import React from "react";
|
||||
import { Button, Dropdown, MenuProps } from "antd";
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, XIcon } from "@heroicons/react/outline";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ArrowUpDown, ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type SortState = "asc" | "desc" | false;
|
||||
|
||||
interface TableHeaderSortDropdownProps {
|
||||
/**
|
||||
* Current sort state: "asc", "desc", or false for neutral
|
||||
*/
|
||||
sortState: SortState;
|
||||
/**
|
||||
* Callback when sort state changes
|
||||
* @param newState - The new sort state: "asc", "desc", or false
|
||||
*/
|
||||
onSortChange: (newState: SortState) => void;
|
||||
/**
|
||||
* Optional column ID for identification
|
||||
*/
|
||||
columnId?: string;
|
||||
}
|
||||
|
||||
export const TableHeaderSortDropdown: React.FC<TableHeaderSortDropdownProps> = ({
|
||||
sortState,
|
||||
onSortChange,
|
||||
}) => {
|
||||
const handleMenuClick: MenuProps["onClick"] = ({ key }) => {
|
||||
if (key === "asc") {
|
||||
onSortChange("asc");
|
||||
} else if (key === "desc") {
|
||||
onSortChange("desc");
|
||||
} else if (key === "reset") {
|
||||
onSortChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems: MenuProps["items"] = [
|
||||
{
|
||||
key: "asc",
|
||||
label: "Ascending",
|
||||
icon: <ChevronUpIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
label: "Descending",
|
||||
icon: <ChevronDownIcon className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
key: "reset",
|
||||
label: "Reset",
|
||||
icon: <XIcon className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
// Determine which icon to display based on current sort state
|
||||
export const TableHeaderSortDropdown: React.FC<
|
||||
TableHeaderSortDropdownProps
|
||||
> = ({ sortState, onSortChange }) => {
|
||||
const renderIcon = () => {
|
||||
if (sortState === "asc") {
|
||||
return <ChevronUpIcon className="h-4 w-4" />;
|
||||
} else if (sortState === "desc") {
|
||||
return <ChevronDownIcon className="h-4 w-4" />;
|
||||
} else {
|
||||
return <SwitchVerticalIcon className="h-4 w-4" />;
|
||||
}
|
||||
if (sortState === "asc") return <ChevronUp className="h-4 w-4" />;
|
||||
if (sortState === "desc") return <ChevronDown className="h-4 w-4" />;
|
||||
return <ArrowUpDown className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems,
|
||||
onClick: handleMenuClick,
|
||||
selectable: true,
|
||||
selectedKeys: sortState ? [sortState] : [],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
icon={renderIcon()}
|
||||
className={sortState ? "text-blue-500 hover:text-blue-600" : "text-gray-400 hover:text-blue-500"}
|
||||
/>
|
||||
</Dropdown>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
sortState
|
||||
? "text-primary hover:text-primary"
|
||||
: "text-muted-foreground hover:text-primary",
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onSortChange("asc")}>
|
||||
<ChevronUp className="mr-2 h-4 w-4" />
|
||||
Ascending
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSortChange("desc")}>
|
||||
<ChevronDown className="mr-2 h-4 w-4" />
|
||||
Descending
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSortChange(false)}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { ClearOutlined } from "@ant-design/icons";
|
||||
import { Button as TremorButton } from "@tremor/react";
|
||||
import { Eraser } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ConversationPanelProps } from "./types";
|
||||
import { useConversation } from "./useConversation";
|
||||
import VariableInput from "./VariableInput";
|
||||
|
|
@ -8,7 +8,10 @@ import MessageList from "./MessageList";
|
|||
import VariableWarning from "./VariableWarning";
|
||||
import MessageInput from "./MessageInput";
|
||||
|
||||
const ConversationPanel: React.FC<ConversationPanelProps> = ({ prompt, accessToken }) => {
|
||||
const ConversationPanel: React.FC<ConversationPanelProps> = ({
|
||||
prompt,
|
||||
accessToken,
|
||||
}) => {
|
||||
const {
|
||||
isLoading,
|
||||
messages,
|
||||
|
|
@ -27,7 +30,7 @@ const ConversationPanel: React.FC<ConversationPanelProps> = ({ prompt, accessTok
|
|||
} = useConversation(prompt, accessToken);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
{!variablesFilled && (
|
||||
<VariableInput
|
||||
extractedVariables={extractedVariables}
|
||||
|
|
@ -37,14 +40,15 @@ const ConversationPanel: React.FC<ConversationPanelProps> = ({ prompt, accessTok
|
|||
)}
|
||||
|
||||
{messages.length > 0 && (
|
||||
<div className="p-3 border-b border-gray-200 bg-white flex justify-end">
|
||||
<TremorButton
|
||||
<div className="p-3 border-b border-border bg-background flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClearConversation}
|
||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300"
|
||||
icon={ClearOutlined}
|
||||
>
|
||||
<Eraser className="h-3.5 w-3.5" />
|
||||
Clear Chat
|
||||
</TremorButton>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -55,14 +59,19 @@ const ConversationPanel: React.FC<ConversationPanelProps> = ({ prompt, accessTok
|
|||
messagesEndRef={messagesEndRef}
|
||||
/>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 bg-white">
|
||||
<VariableWarning extractedVariables={extractedVariables} variables={variables} />
|
||||
<div className="p-4 border-t border-border bg-background">
|
||||
<VariableWarning
|
||||
extractedVariables={extractedVariables}
|
||||
variables={variables}
|
||||
/>
|
||||
|
||||
<MessageInput
|
||||
inputMessage={inputMessage}
|
||||
isLoading={isLoading}
|
||||
isDisabled={
|
||||
isLoading || !inputMessage.trim() || (extractedVariables.length > 0 && !allVariablesFilled)
|
||||
isLoading ||
|
||||
!inputMessage.trim() ||
|
||||
(extractedVariables.length > 0 && !allVariablesFilled)
|
||||
}
|
||||
onInputChange={setInputMessage}
|
||||
onSend={handleSendMessage}
|
||||
|
|
@ -75,4 +84,3 @@ const ConversationPanel: React.FC<ConversationPanelProps> = ({ prompt, accessTok
|
|||
};
|
||||
|
||||
export default ConversationPanel;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue