@@ -162,8 +176,8 @@ export function ComparisonPanel({
min={0}
max={2}
step={0.01}
- value={comparison.temperature}
- onChange={(value) => {
+ value={[comparison.temperature]}
+ onValueChange={(value) => {
const nextValue = Array.isArray(value) ? value[0] : value;
const clamped = Math.min(2, Math.max(0, Number(nextValue.toFixed(2))));
handleSettingChange("temperature", clamped);
@@ -180,8 +194,8 @@ export function ComparisonPanel({
min={1}
max={32768}
step={1}
- value={comparison.maxTokens}
- onChange={(value) => {
+ value={[comparison.maxTokens]}
+ onValueChange={(value) => {
const nextValue = Array.isArray(value) ? value[0] : value;
const clamped = Math.min(32768, Math.max(1, Math.round(nextValue)));
handleSettingChange("maxTokens", clamped);
@@ -209,26 +223,29 @@ export function ComparisonPanel({
/>
{
// Prevent automatic closing - we control it manually
}}
- placement="bottomRight"
- destroyTooltipOnHide={false}
>
-
+ {
+ event.stopPropagation();
+ handleTogglePopover();
+ }}
+ className={`p-2 rounded-lg transition-colors ${
+ popoverVisible ? "bg-gray-200 text-gray-700" : "hover:bg-gray-100 text-gray-600"
+ }`}
+ >
+
+
+ }
+ />
+
+ {settingsContent}
+
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.test.tsx
index eb157904165..049b70b982c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.test.tsx
@@ -1,42 +1,95 @@
-import { render } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { MessageInput } from "./MessageInput";
+const PLACEHOLDER = "Type your message... (Shift+Enter for new line)";
+
describe("MessageInput", () => {
- it("should render", () => {
- const onChange = vi.fn();
- const onSend = vi.fn();
- const { container } = render(
);
- const textarea = container.querySelector("textarea");
- const button = container.querySelector("button");
- expect(textarea).toBeInTheDocument();
- expect(button).toBeInTheDocument();
+ it("renders a message box and a send control", () => {
+ render(
);
+
+ expect(screen.getByPlaceholderText(PLACEHOLDER)).toBeInTheDocument();
+ expect(screen.getByRole("button")).toBeInTheDocument();
});
- it("should disable send button initially", () => {
+ it("reports every keystroke through onChange", async () => {
+ const user = userEvent.setup();
const onChange = vi.fn();
- const onSend = vi.fn();
- const { container } = render(
);
- const button = container.querySelector("button") as HTMLButtonElement;
+ render(
);
- expect(button).toBeDisabled();
+ await user.type(screen.getByPlaceholderText(PLACEHOLDER), "hi");
+
+ expect(onChange).toHaveBeenCalledWith("h");
});
- it("should enable send button when hasAttachment is true even with empty value", () => {
- const onChange = vi.fn();
+ it("refuses to send while the box is empty and nothing is attached", async () => {
+ const user = userEvent.setup();
const onSend = vi.fn();
- const uploadComponent =
Upload
;
- const { container, getByTestId } = render(
+ render(
);
+
+ const send = screen.getByRole("button");
+ expect(send).toBeDisabled();
+
+ await user.click(send);
+ expect(onSend).not.toHaveBeenCalled();
+ });
+
+ it("sends once the box holds text", async () => {
+ const user = userEvent.setup();
+ const onSend = vi.fn();
+ render(
);
+
+ const send = screen.getByRole("button");
+ expect(send).not.toBeDisabled();
+
+ await user.click(send);
+ expect(onSend).toHaveBeenCalledTimes(1);
+ });
+
+ it("sends on an attachment alone and surfaces the upload control", async () => {
+ const user = userEvent.setup();
+ const onSend = vi.fn();
+ render(
Upload}
/>,
);
- const button = container.querySelector("button") as HTMLButtonElement;
- expect(getByTestId("upload-component")).toBeInTheDocument();
- expect(button).not.toBeDisabled();
+
+ expect(screen.getByTestId("upload-component")).toBeInTheDocument();
+
+ const send = screen.getByRole("button");
+ expect(send).not.toBeDisabled();
+
+ await user.click(send);
+ expect(onSend).toHaveBeenCalledTimes(1);
+ });
+
+ it("stays inert while disabled even with text present", async () => {
+ const user = userEvent.setup();
+ const onSend = vi.fn();
+ render();
+
+ await user.click(screen.getByRole("button"));
+ expect(onSend).not.toHaveBeenCalled();
+ });
+
+ it("sends on Enter but adds a newline on Shift+Enter", async () => {
+ const user = userEvent.setup();
+ const onSend = vi.fn();
+ render();
+
+ const box = screen.getByPlaceholderText(PLACEHOLDER);
+ await user.click(box);
+
+ await user.keyboard("{Shift>}{Enter}{/Shift}");
+ expect(onSend).not.toHaveBeenCalled();
+
+ await user.keyboard("{Enter}");
+ expect(onSend).toHaveBeenCalledTimes(1);
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx
index fae964b372a..b27341ff7a5 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx
@@ -1,8 +1,7 @@
import React from "react";
-import { Input, Button } from "antd";
-import { ArrowUpOutlined } from "@ant-design/icons";
-
-const { TextArea } = Input;
+import { ArrowUp } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
interface MessageInputProps {
value: string;
@@ -29,25 +28,25 @@ export function MessageInput({ value, onChange, onSend, disabled, hasAttachment,
{uploadComponent &&
{uploadComponent}
}
-
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx
index 1c6951fc421..29a8092e2b0 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx
@@ -4,158 +4,118 @@ import { describe, expect, it, vi } from "vitest";
import { UnifiedSelector } from "./UnifiedSelector";
import { EndpointId, ENDPOINT_CONFIGS } from "../endpoint_config";
+const CHAT = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
+const AGENTS = ENDPOINT_CONFIGS[EndpointId.A2A_AGENTS];
+
+const promptIsVisible = (text: string) =>
+ screen.queryByText(text) !== null || screen.queryByPlaceholderText(text) !== null;
+
+const openList = async (user: ReturnType) => {
+ await user.click(screen.getByRole("combobox"));
+};
+
describe("UnifiedSelector", () => {
- it("should render", () => {
- const onChange = vi.fn();
- const options = [
- { value: "option1", label: "Option 1" },
- { value: "option2", label: "Option 2" },
- ];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
-
- render();
-
- const select = screen.getByRole("combobox");
- expect(select).toBeInTheDocument();
- });
-
- it("should display placeholder when not loading", () => {
- const onChange = vi.fn();
- const options = [{ value: "option1", label: "Option 1" }];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
-
- const { container } = render(
- ,
+ it("renders a combobox", () => {
+ render(
+ ,
);
- const placeholder = container.querySelector(".ant-select-selection-placeholder");
- expect(placeholder).toHaveTextContent(config.selectorPlaceholder);
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
});
- it("should display loading placeholder when loading", () => {
- const onChange = vi.fn();
- const options = [{ value: "option1", label: "Option 1" }];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
+ it("prompts with the endpoint's own selector copy", () => {
+ render();
- const { container } = render(
- ,
- );
-
- const placeholder = container.querySelector(".ant-select-selection-placeholder");
- expect(placeholder).toHaveTextContent(`Loading ${config.selectorLabel.toLowerCase()}s...`);
+ expect(promptIsVisible(CHAT.selectorPlaceholder)).toBe(true);
});
- it("should call onChange when option is selected", async () => {
+ it("prompts with the agent endpoint's copy when configured for agents", () => {
+ render();
+
+ expect(promptIsVisible(AGENTS.selectorPlaceholder)).toBe(true);
+ });
+
+ it("swaps the prompt for a loading message while options are in flight", () => {
+ render();
+
+ expect(promptIsVisible(`Loading ${CHAT.selectorLabel.toLowerCase()}s...`)).toBe(true);
+ });
+
+ it("reports the chosen option's value, not its label", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
- const options = [
- { value: "option1", label: "Option 1" },
- { value: "option2", label: "Option 2" },
- ];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
-
- render();
-
- const select = screen.getByRole("combobox");
- await user.click(select);
-
- await waitFor(() => {
- const option = screen.getByText("Option 1");
- expect(option).toBeInTheDocument();
- });
-
- const option = screen.getByText("Option 1");
- await user.click(option);
-
- await waitFor(() => {
- expect(onChange).toHaveBeenCalled();
- });
- const callArgs = onChange.mock.calls[0];
- expect(callArgs[0]).toBe("option1");
- });
-
- it("should display selected value", () => {
- const onChange = vi.fn();
- const options = [
- { value: "option1", label: "Option 1" },
- { value: "option2", label: "Option 2" },
- ];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
-
- const { container } = render(
- ,
+ render(
+ ,
);
- const selectedValue = container.querySelector(".ant-select-selection-item");
- expect(selectedValue).toHaveTextContent("Option 1");
+ await openList(user);
+
+ const matches = await screen.findAllByText("Option Two");
+ await user.click(matches[matches.length - 1]);
+
+ await waitFor(() => expect(onChange).toHaveBeenCalled());
+ expect(onChange.mock.calls[0][0]).toBe("option2");
});
- it("should filter options by search input", async () => {
+ it("narrows the list as the user searches", async () => {
const user = userEvent.setup();
- const onChange = vi.fn();
- const options = [
- { value: "option1", label: "Option One" },
- { value: "option2", label: "Option Two" },
- { value: "option3", label: "Different" },
- ];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
+ render(
+ ,
+ );
- render();
-
- const select = screen.getByRole("combobox");
- await user.click(select);
- await user.type(select, "One");
+ const combobox = screen.getByRole("combobox");
+ await user.click(combobox);
+ await user.type(combobox, "One");
await waitFor(() => {
- expect(screen.getByText("Option One")).toBeInTheDocument();
+ expect(screen.getAllByText("Option One").length).toBeGreaterThan(0);
expect(screen.queryByText("Option Two")).not.toBeInTheDocument();
expect(screen.queryByText("Different")).not.toBeInTheDocument();
});
});
- it("should show loading spinner in notFoundContent when loading", async () => {
+ it("shows a busy indicator in the empty list while loading", async () => {
const user = userEvent.setup();
- const onChange = vi.fn();
- const options: { value: string; label: string }[] = [];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
+ render();
- render();
-
- const select = screen.getByRole("combobox");
- await user.click(select);
+ await openList(user);
await waitFor(() => {
- const spin = document.querySelector(".ant-spin");
- expect(spin).toBeInTheDocument();
+ expect(document.querySelector('[aria-busy="true"]')).toBeInTheDocument();
});
});
- it("should show no options message when not loading and no options", async () => {
+ it("says so when there is nothing to pick and nothing is loading", async () => {
const user = userEvent.setup();
- const onChange = vi.fn();
- const options: { value: string; label: string }[] = [];
- const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
+ render();
- render();
+ await openList(user);
- const select = screen.getByRole("combobox");
- await user.click(select);
-
- await waitFor(() => {
- expect(screen.getByText(`No ${config.selectorLabel.toLowerCase()}s available`)).toBeInTheDocument();
- });
- });
-
- it("should work with agent endpoint config", () => {
- const onChange = vi.fn();
- const options = [{ value: "agent1", label: "Agent One" }];
- const config = ENDPOINT_CONFIGS[EndpointId.A2A_AGENTS];
-
- const { container } = render(
- ,
- );
-
- const placeholder = container.querySelector(".ant-select-selection-placeholder");
- expect(placeholder).toHaveTextContent(config.selectorPlaceholder);
+ expect(await screen.findByText(`No ${CHAT.selectorLabel.toLowerCase()}s available`)).toBeInTheDocument();
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx
index c37580efd39..915f0dbb320 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx
@@ -3,7 +3,15 @@
* based on the current endpoint configuration.
*/
-import { Select, Spin } from "antd";
+import {
+ Combobox,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxInput,
+ ComboboxItem,
+ ComboboxList,
+} from "@/components/ui/combobox";
+import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { SelectorOption, EndpointConfig } from "../endpoint_config";
interface UnifiedSelectorProps {
@@ -14,26 +22,44 @@ interface UnifiedSelectorProps {
onChange: (value: string) => void;
}
+const matchesQuery = (option: SelectorOption, query: string): boolean =>
+ option.label.toLowerCase().includes(query.trim().toLowerCase());
+
export function UnifiedSelector({ value, options, loading, config, onChange }: UnifiedSelectorProps) {
+ const selected = options.find((option) => option.value === value) ?? null;
+ const noun = config.selectorLabel.toLowerCase();
+
return (
-