diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 09d0032e6c9..9df325e9910 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -336,9 +336,6 @@ } }, "src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -351,9 +348,6 @@ "src/app/(dashboard)/guardrails-monitor/_components/GuardrailDetail.tsx": { "no-nested-ternary": { "count": 3 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx": { @@ -1486,16 +1480,10 @@ }, "src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx": { "no-nested-ternary": { - "count": 3 - }, - "no-restricted-imports": { - "count": 1 + "count": 2 } }, "src/app/(dashboard)/projects/_components/ProjectKeysSection.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -1526,11 +1514,6 @@ "count": 2 } }, - "src/app/(dashboard)/projects/_components/ProjectsPage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/prompts/_components/add_prompt_form.tsx": { "local/filename-pascal-case": { "count": 1 @@ -2109,16 +2092,6 @@ "count": 1 } }, - "src/components/DeletedKeysPage/DeletedKeysPage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/DeletedTeamsPage/DeletedTeamsPage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/DeprecationBanner.tsx": { "no-restricted-imports": { "count": 1 @@ -3827,11 +3800,6 @@ "count": 1 } }, - "src/components/ui/AntDLoadingSpinner.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/ui/alert-dialog.tsx": { "local/filename-pascal-case": { "count": 1 @@ -4040,11 +4008,6 @@ "count": 1 } }, - "src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/view_logs/CostBreakdownViewer.tsx": { "no-restricted-imports": { "count": 1 @@ -4207,9 +4170,6 @@ "src/components/view_logs/index.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/view_logs/log_filter_logic.tsx": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.test.tsx new file mode 100644 index 00000000000..41aa1087782 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.test.tsx @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { render, screen, waitFor } from "@testing-library/react"; +import { EvaluationSettingsModal } from "./EvaluationSettingsModal"; + +const mockFetchAvailableModels = vi.fn(); +vi.mock("@/components/llm_calls/fetch_models", () => ({ + fetchAvailableModels: (...args: unknown[]) => mockFetchAvailableModels(...args), +})); + +const modelGroups = [{ model_group: "gpt-5.2" }, { model_group: "claude-sonnet-5" }]; + +const defaultProps = { + open: true, + onClose: vi.fn(), + guardrailName: "pii-detector", + accessToken: "test-token", + onRunEvaluation: vi.fn(), +}; + +async function selectModel(user: ReturnType, label: string) { + await user.click(screen.getByRole("combobox")); + const options = await screen.findAllByText(label); + await user.click(options[options.length - 1]); +} + +describe("EvaluationSettingsModal", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockFetchAvailableModels.mockResolvedValue(modelGroups); + }); + + it("should render nothing while closed", () => { + render(); + expect(screen.queryByText("Evaluation Settings")).not.toBeInTheDocument(); + }); + + it("should show the title and the guardrail-specific description when open", () => { + render(); + expect(screen.getByText("Evaluation Settings")).toBeInTheDocument(); + expect(screen.getByText("Configure AI evaluation for pii-detector")).toBeInTheDocument(); + }); + + it("should fall back to a generic description when no guardrail name is given", () => { + render(); + expect(screen.getByText("Configure AI evaluation for re-running on logs")).toBeInTheDocument(); + }); + + it("should prefill the prompt and the response schema with their defaults", () => { + render(); + expect(screen.getByDisplayValue(/Evaluate whether this guardrail's decision was correct/)).toBeInTheDocument(); + expect( + screen.getByDisplayValue(/"verdict": "correct" \| "false_positive" \| "false_negative"/), + ).toBeInTheDocument(); + }); + + it("should restore the default prompt when 'Reset to default' is clicked", async () => { + const user = userEvent.setup(); + render(); + + const promptBox = screen.getByDisplayValue(/Evaluate whether this guardrail's decision was correct/); + await user.clear(promptBox); + await user.type(promptBox, "custom prompt"); + expect(screen.getByDisplayValue("custom prompt")).toBeInTheDocument(); + + await user.click(screen.getByText("Reset to default")); + expect(screen.getByDisplayValue(/Evaluate whether this guardrail's decision was correct/)).toBeInTheDocument(); + }); + + it("should load the available models with the access token when opened", async () => { + render(); + await waitFor(() => expect(mockFetchAvailableModels).toHaveBeenCalledWith("test-token")); + }); + + it("should not load models when there is no access token", () => { + render(); + expect(mockFetchAvailableModels).not.toHaveBeenCalled(); + }); + + it("should not run an evaluation while no model is selected", async () => { + const user = userEvent.setup(); + const onRunEvaluation = vi.fn(); + const onClose = vi.fn(); + render(); + + await user.click(screen.getByRole("button", { name: /run evaluation/i })); + + expect(onRunEvaluation).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + }); + + it("should run the evaluation with the selected model and the current prompt and schema", async () => { + const user = userEvent.setup(); + const onRunEvaluation = vi.fn(); + const onClose = vi.fn(); + render(); + + await waitFor(() => expect(mockFetchAvailableModels).toHaveBeenCalled()); + await selectModel(user, "claude-sonnet-5"); + await user.click(screen.getByRole("button", { name: /run evaluation/i })); + + expect(onRunEvaluation).toHaveBeenCalledWith({ + model: "claude-sonnet-5", + prompt: expect.stringContaining("Evaluate whether this guardrail's decision was correct"), + schema: expect.stringContaining('"verdict"'), + }); + expect(onClose).toHaveBeenCalled(); + }); + + it("should close without running when 'Cancel' is clicked", async () => { + const user = userEvent.setup(); + const onRunEvaluation = vi.fn(); + const onClose = vi.fn(); + render(); + + await user.click(screen.getByRole("button", { name: /cancel/i })); + + expect(onClose).toHaveBeenCalled(); + expect(onRunEvaluation).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.tsx index 0edfa65dfe8..900a04e480d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/EvaluationSettingsModal.tsx @@ -1,7 +1,17 @@ -import { CloseOutlined, PlayCircleOutlined } from "@ant-design/icons"; -import { Button, Modal, Select, Input } from "antd"; -import React, { useEffect, useState } from "react"; +import { Play } from "lucide-react"; +import React, { useEffect, useMemo, useState } from "react"; import { fetchAvailableModels, type ModelGroup } from "@/components/llm_calls/fetch_models"; +import { SearchSelect } from "@/components/shared/SearchSelect"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; const DEFAULT_PROMPT = `Evaluate whether this guardrail's decision was correct. Analyze the user input, the guardrail action taken, and determine if it was appropriate. @@ -73,79 +83,81 @@ export function EvaluationSettingsModal({ } }; - const modelSelectOptions = modelOptions.map((m) => ({ - value: m.model_group, - label: m.model_group, - })); + const modelSelectOptions = useMemo( + () => modelOptions.map((m) => ({ value: m.model_group, label: m.model_group })), + [modelOptions], + ); return ( - } - destroyOnClose - > -

- {guardrailName - ? `Configure AI evaluation for ${guardrailName}` - : "Configure AI evaluation for re-running on logs"} -

+ !nextOpen && onClose()}> + + + Evaluation Settings + + {guardrailName + ? `Configure AI evaluation for ${guardrailName}` + : "Configure AI evaluation for re-running on logs"} + + -
-
-
- - +
+
+
+ + +
+