diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.test.tsx deleted file mode 100644 index 289ce8a53f5..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import QuestionBreakdown from "./QuestionBreakdown"; -import { validateSystemOnePayload } from "./lib/validatePayload"; - -const previewOf = (questions: object) => validateSystemOnePayload(JSON.stringify({ state: "Hi", questions })).payload; - -describe("QuestionBreakdown", () => { - it("previews every noul criterion that will be sent, not only true and false", () => { - render( - , - ); - - expect(screen.getByText("Asks for a person")).toBeInTheDocument(); - expect(screen.getByText("unsure")).toBeInTheDocument(); - expect(screen.getByText("Maybe")).toBeInTheDocument(); - expect(screen.queryByText("No criteria defined")).not.toBeInTheDocument(); - }); - - it("says when a noul question has no criteria", () => { - render(); - - expect(screen.getByText("No criteria defined")).toBeInTheDocument(); - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.tsx deleted file mode 100644 index 8fc06101783..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import type { PlaygroundQuestion, PlaygroundRequest } from "./lib/schemas"; - -function formatState(state: unknown): string { - if (typeof state === "string") { - return state; - } - return JSON.stringify(state, null, 2) ?? String(state); -} - -function QuestionCriteria({ question }: { question: PlaygroundQuestion }) { - if (question.type === "choice") { - return ( -
- {Object.entries(question.criteria).map(([label, description]) => ( -
-
{label}
-
{formatState(description)}
-
- ))} -
- ); - } - - if (question.type === "noul") { - const criteria = Object.entries(question.criteria ?? {}).filter(([, description]) => description !== undefined); - return criteria.length > 0 ? ( -
- {criteria.map(([label, description]) => ( -
-
{label}
-
- {typeof description === "string" ? description : JSON.stringify(description)} -
-
- ))} -
- ) : ( -

No criteria defined

- ); - } - - return ( -
    - {question.criteria.map((description, index) => ( -
  1. - {index} - {formatState(description)} -
  2. - ))} -
- ); -} - -export default function QuestionBreakdown({ payload }: { payload?: PlaygroundRequest }) { - if (!payload) { - return ( - - - Question breakdown - Enter a valid request to preview its state and questions. - - - ); - } - - return ( - - - Question breakdown - Review the state and criteria that will be sent. - - -
-

- State -

-
-            {formatState(payload.state)}
-          
-
-
- {Object.entries(payload.questions).map(([id, question]) => ( -
-
-

- {id} -

- {question.type} -
- {question.instructions != null &&

{formatState(question.instructions)}

} -
-

Criteria

- -
-
- ))} -
-
-
- ); -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx index eb75c836b2b..470a2495d4f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx @@ -11,7 +11,7 @@ import { import type { ReactElement } from "react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import SystemOneUI, { NOUL_HELP } from "./SystemOneUI"; +import SystemOneUI from "./SystemOneUI"; import type { SystemOneResponse } from "./lib/schemas"; import type { OpenAIDecisionsResponse } from "./lib/openAIDecisions"; @@ -194,17 +194,11 @@ describe("SystemOneUI integration", () => { expect(screen.queryByText(/TypeSafe/)).not.toBeInTheDocument(); await userEvent.keyboard("{Escape}"); - expect(screen.getByRole("note", { name: "Decision endpoint notice" })).toHaveTextContent( - "omit model to use the proxy's configured default.", - ); expect(screen.getByRole("link", { name: "How to call /v1/decisions and /v1/systemone" })).toHaveAttribute( "href", "https://docs.litellm.ai/docs/decisions", ); - expect(screen.getByRole("link", { name: "Give us feedback on what you want for decision models" })).toHaveAttribute( - "href", - "https://github.com/BerriAI/litellm/discussions/44231", - ); + expect(screen.queryByRole("note")).not.toBeInTheDocument(); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); @@ -653,6 +647,27 @@ describe("SystemOneUI integration", () => { expect(screen.getByRole("button", { name: "Send" })).toBeDisabled(); }); + it("keeps Send below the form and shows results only after a request", async () => { + const user = userEvent.setup(); + render(); + + const send = screen.getByRole("button", { name: "Send" }); + const editor = screen.getByRole("region", { name: "Decisions request editor" }); + const form = within(editor).getByRole("textbox", { name: "Input" }); + expect(form.compareDocumentPosition(send) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(screen.queryByRole("region", { name: "Decisions results" })).not.toBeInTheDocument(); + expect(screen.queryByRole("note")).not.toBeInTheDocument(); + + await user.click(send); + + const results = await screen.findByRole("region", { name: "Decisions results" }); + expect(within(results).getByText("Calibrated probabilities")).toBeInTheDocument(); + expect(editor.compareDocumentPosition(results) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + + fireEvent.change(within(editor).getByRole("textbox", { name: "Input" }), { target: { value: "Edited state" } }); + expect(screen.queryByRole("region", { name: "Decisions results" })).not.toBeInTheDocument(); + }); + it("sends to /v1/systemone even without discovered models and renders calibrated answers", async () => { const user = userEvent.setup(); sessionStorage.setItem("customProxyBaseUrl", "https://stale.example.com/"); @@ -712,7 +727,8 @@ describe("SystemOneUI integration", () => { expect(screen.getByRole("button", { name: "Send" })).toBeDisabled(); await userEvent.click(screen.getByRole("tab", { name: "Form" })); - expect(screen.getByText("Enter a valid request to preview its state and questions.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Send" })).toBeDisabled(); + expect(screen.queryByRole("region", { name: "Decisions results" })).not.toBeInTheDocument(); }); it("sends structured native questions without changing their payload", async () => { @@ -729,7 +745,6 @@ describe("SystemOneUI integration", () => { fireEvent.change(editor, { target: { value: draft } }); expect(screen.getByRole("button", { name: "Send" })).toBeEnabled(); await user.click(screen.getByRole("tab", { name: "Form" })); - expect(screen.getByText(/"text": "Help"/)).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Send" })); expect(await screen.findByText("Selected choice")).toBeInTheDocument(); @@ -920,13 +935,10 @@ describe("SystemOneUI integration", () => { fireEvent.blur(field); } - it("explains noul on hover in the banner and on the answer type option", async () => { + it("explains noul on the answer type option", async () => { const user = userEvent.setup(); render(); - await user.hover(screen.getByText("noul", { selector: "span.cursor-help" })); - expect(await screen.findByText(NOUL_HELP)).toBeInTheDocument(); - question("has_repro_steps").getByRole("combobox", { name: "Answer type" }).focus(); await user.keyboard("{ArrowDown}"); expect(await screen.findByRole("option", { name: "noul" })).toHaveAttribute( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx index 3d2d4c0f1a0..716f5c78b08 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx @@ -1,22 +1,21 @@ "use client"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { SearchSelect } from "@/components/shared/SearchSelect"; +import { cn } from "@/lib/cva.config"; import { DECISIONS_DOCS_URL } from "@/lib/decisionModels"; import { uiHref } from "@/utils/uiHref"; import { useMutation } from "@tanstack/react-query"; -import { Code, Eraser, Info, LoaderCircle, Send } from "lucide-react"; +import { BookOpen, Code, Eraser, LoaderCircle, Send } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { makePlaygroundDecisionRequest, type PlaygroundDecisionRequest } from "../../llm_calls/system_one"; import { DECISION_PRESETS, emptyPayload, matchPreset, presetPayload, type DecisionPreset } from "./lib/example"; import { payloadModel, withPayloadModel } from "./lib/payloadModel"; import JsonEditor from "./JsonEditor"; -import QuestionBreakdown from "./QuestionBreakdown"; import ResponseView from "./ResponseView"; import SystemOneForm from "./SystemOneForm"; import OpenAIDecisionsForm from "./OpenAIDecisionsForm"; @@ -38,7 +37,6 @@ interface SystemOneSendVariables { export const NOUL_HELP = "noul is a yes / no question. The model returns the probability that the answer is yes."; const CUSTOM_PRESET = "custom"; -const DECISION_MODELS_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/44231"; interface PresetPickerProps { activePreset: DecisionPreset | undefined; @@ -103,6 +101,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = makePlaygroundDecisionRequest(request, apiKey, getCustomProxyBaseUrl(), { signal }), }); const isLoading = systemOne.isPending; + const hasResult = isLoading || systemOne.data !== undefined || systemOne.error !== null; const { reset: resetSystemOne } = systemOne; useEffect(() => () => activeController.current?.abort(), []); @@ -163,49 +162,49 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = } return ( -
-
-
- Endpoint - - Model -
- ({ label: model, value: model }))} - value={payloadModel(rawPayload) ?? null} - onValueChange={handleModelPick} - placeholder={decisionModels.length > 0 ? "Pick a decision model" : "No decision models yet"} - emptyText="No decision models found" - disabled={hasSyntaxError} - allowClear={false} - /> -
- {decisionModelsLoaded && decisionModels.length === 0 && ( - - Add a decision model - - )} -
-
+
+
+
- Virtual Key Source + +
+ ({ label: model, value: model }))} + value={payloadModel(rawPayload) ?? null} + onValueChange={handleModelPick} + placeholder={decisionModels.length > 0 ? "Pick a decision model" : "No decision models yet"} + emptyText="No decision models found" + disabled={hasSyntaxError} + allowClear={false} + /> +
+ {decisionModelsLoaded && decisionModels.length === 0 && ( + + Add a decision model + + )}