feat(ui): restyle Decisions playground after OpenRouter's model playground

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-10-11 00:36:44 +00:00
parent 9e2aae8490
commit 8fdc2150da
4 changed files with 139 additions and 270 deletions

View file

@ -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(
<QuestionBreakdown
payload={previewOf({
escalate: {
type: "noul",
instructions: "Escalate?",
criteria: { true: "Asks for a person", unsure: "Maybe" },
},
})}
/>,
);
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(<QuestionBreakdown payload={previewOf({ escalate: { type: "noul", instructions: "Escalate?" } })} />);
expect(screen.getByText("No criteria defined")).toBeInTheDocument();
});
});

View file

@ -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 (
<dl className="grid gap-2">
{Object.entries(question.criteria).map(([label, description]) => (
<div key={label} className="grid gap-0.5 sm:grid-cols-[minmax(7rem,auto)_1fr] sm:gap-3">
<dt className="font-mono text-xs font-medium">{label}</dt>
<dd className="text-xs text-muted-foreground">{formatState(description)}</dd>
</div>
))}
</dl>
);
}
if (question.type === "noul") {
const criteria = Object.entries(question.criteria ?? {}).filter(([, description]) => description !== undefined);
return criteria.length > 0 ? (
<dl className="grid gap-2">
{criteria.map(([label, description]) => (
<div key={label} className="grid gap-0.5 sm:grid-cols-[minmax(7rem,auto)_1fr] sm:gap-3">
<dt className="text-xs font-medium">{label}</dt>
<dd className="text-xs text-muted-foreground">
{typeof description === "string" ? description : JSON.stringify(description)}
</dd>
</div>
))}
</dl>
) : (
<p className="text-xs text-muted-foreground">No criteria defined</p>
);
}
return (
<ol className="grid gap-2">
{question.criteria.map((description, index) => (
<li key={`${index}-${description}`} className="grid gap-0.5 sm:grid-cols-[minmax(7rem,auto)_1fr] sm:gap-3">
<span className="font-mono text-xs font-medium">{index}</span>
<span className="text-xs text-muted-foreground">{formatState(description)}</span>
</li>
))}
</ol>
);
}
export default function QuestionBreakdown({ payload }: { payload?: PlaygroundRequest }) {
if (!payload) {
return (
<Card>
<CardHeader>
<CardTitle>Question breakdown</CardTitle>
<CardDescription>Enter a valid request to preview its state and questions.</CardDescription>
</CardHeader>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>Question breakdown</CardTitle>
<CardDescription>Review the state and criteria that will be sent.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 wrap-anywhere">
<section aria-labelledby="system-one-state-heading" className="grid gap-2">
<h3 id="system-one-state-heading" className="text-sm font-medium">
State
</h3>
<pre className="max-h-48 overflow-y-auto whitespace-pre-wrap rounded-md bg-muted p-3 font-mono text-xs">
{formatState(payload.state)}
</pre>
</section>
<div className="grid gap-3">
{Object.entries(payload.questions).map(([id, question]) => (
<section key={id} className="grid gap-3 rounded-md border p-3" aria-labelledby={`question-${id}`}>
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 id={`question-${id}`} className="font-mono text-sm font-medium">
{id}
</h3>
<Badge variant="secondary">{question.type}</Badge>
</div>
{question.instructions != null && <p className="text-sm">{formatState(question.instructions)}</p>}
<div className="grid gap-2">
<h4 className="text-xs font-medium text-muted-foreground">Criteria</h4>
<QuestionCriteria question={question} />
</div>
</section>
))}
</div>
</CardContent>
</Card>
);
}

View file

@ -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(<SystemOneUI accessToken="session-key" />);
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(<SystemOneUI accessToken="session-key" />);
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(

View file

@ -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 (
<div className="flex h-full min-h-0 flex-col gap-4 overflow-auto p-4 xl:overflow-hidden">
<section className="grid gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Endpoint</span>
<Select
value={endpoint}
onValueChange={(value) => {
if (value === "/v1/decisions" || value === "/v1/systemone") {
clearRequestState();
setEndpoint(value);
}
}}
>
<SelectTrigger className="w-80" aria-label="Decision endpoint">
<SelectValue>{endpoint}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="/v1/decisions">/v1/decisions</SelectItem>
<SelectItem value="/v1/systemone">/v1/systemone</SelectItem>
</SelectContent>
</Select>
<span className="ml-2 text-sm font-medium text-muted-foreground">Model</span>
<div className="w-80">
<SearchSelect
aria-label="Decision model"
options={decisionModels.map((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}
/>
</div>
{decisionModelsLoaded && decisionModels.length === 0 && (
<a href={uiHref("models-and-endpoints")} className="text-sm text-primary underline">
Add a decision model
</a>
)}
</div>
<div className="flex flex-wrap items-center gap-3">
<div className="flex h-full min-h-0 flex-col overflow-auto p-4 xl:overflow-hidden">
<div
className={cn(
"mx-auto grid w-full gap-4 xl:min-h-0 xl:flex-1",
hasResult ? "max-w-7xl xl:grid-cols-2" : "max-w-3xl",
)}
>
<section className="flex min-h-96 flex-col gap-3 xl:min-h-0" aria-label="Decisions request editor">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Virtual Key Source</span>
<Select
value={endpoint}
onValueChange={(value) => {
if (value === "/v1/decisions" || value === "/v1/systemone") {
clearRequestState();
setEndpoint(value);
}
}}
>
<SelectTrigger className="w-44" aria-label="Decision endpoint">
<SelectValue>{endpoint}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="/v1/decisions">/v1/decisions</SelectItem>
<SelectItem value="/v1/systemone">/v1/systemone</SelectItem>
</SelectContent>
</Select>
<div className="w-56">
<SearchSelect
aria-label="Decision model"
options={decisionModels.map((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}
/>
</div>
{decisionModelsLoaded && decisionModels.length === 0 && (
<a href={uiHref("models-and-endpoints")} className="text-sm text-primary underline">
Add a decision model
</a>
)}
<Select
value={apiKeySource}
onValueChange={(value) => {
@ -215,7 +214,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
}}
disabled={disabledPersonalKeyCreation}
>
<SelectTrigger className="w-48" aria-label="Virtual Key Source">
<SelectTrigger className="w-44" aria-label="Virtual Key Source">
<SelectValue>{apiKeySource === "custom" ? "Virtual Key" : "Current UI Session"}</SelectValue>
</SelectTrigger>
<SelectContent>
@ -236,71 +235,32 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
/>
)}
</div>
<div className="ml-auto flex items-center gap-2">
<PresetPicker activePreset={activePreset} onPick={handlePresetPick} />
<Button
variant="outline"
onClick={() => handlePayloadChange(clearedPayload)}
disabled={rawPayload === clearedPayload}
>
<Eraser />
Clear
</Button>
{view === "json" && (
<Button variant="outline" onClick={handleFormatJson} disabled={!rawPayload.trim() || hasSyntaxError}>
<Code />
Format JSON
</Button>
)}
{isLoading && (
<Button variant="outline" onClick={clearRequestState}>
Cancel request
</Button>
)}
<Button onClick={handleSend} disabled={!validation.isValid || isLoading || !effectiveApiKey}>
{isLoading ? <LoaderCircle className="animate-spin" /> : <Send />}
Send
</Button>
</div>
</div>
<Alert role="note" aria-label="Decision endpoint notice">
<Info />
<AlertTitle>{endpoint}</AlertTitle>
<AlertDescription>
{endpoint === "/v1/decisions" ? (
"Uses input and a questions array with predicate, choice, and score questions. Edit the form or JSON and inspect the response JSON in the OpenAI Decisions format."
) : (
<>
Uses state and a questions object with{" "}
<SimpleTooltip content={NOUL_HELP} className="underline decoration-dotted underline-offset-2">
noul
</SimpleTooltip>
, choice, and score questions in the System One format.
</>
)}{" "}
Pick a decision model under Model, or omit model to use the proxy&apos;s configured default.{" "}
<a href={DECISIONS_DOCS_URL} target="_blank" rel="noopener noreferrer" className="underline">
How to call /v1/decisions and /v1/systemone
</a>
{" · "}
<a href={DECISION_MODELS_DISCUSSION_URL} target="_blank" rel="noopener noreferrer" className="underline">
Give us feedback on what you want for decision models
</a>
</AlertDescription>
</Alert>
</section>
<div className="grid gap-4 xl:min-h-0 xl:flex-1 xl:grid-cols-2">
<section className="flex min-h-96 flex-col xl:min-h-0" aria-label="Decisions request editor">
<Tabs
value={view}
onValueChange={(value) => (value === "form" || value === "json") && setView(value)}
className="min-h-0 flex-1"
>
<TabsList aria-label="Request editor view">
<TabsTrigger value="form">Form</TabsTrigger>
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
<div className="flex flex-wrap items-center justify-between gap-2">
<TabsList aria-label="Request editor view">
<TabsTrigger value="form">Form</TabsTrigger>
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
<div className="flex items-center gap-1">
<PresetPicker activePreset={activePreset} onPick={handlePresetPick} />
<SimpleTooltip content="How to call /v1/decisions and /v1/systemone">
<a
href={DECISIONS_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="How to call /v1/decisions and /v1/systemone"
className={cn(buttonVariants({ variant: "ghost", size: "icon" }), "shrink-0")}
>
<BookOpen />
</a>
</SimpleTooltip>
</div>
</div>
<TabsContent value="form" className="flex min-h-0 flex-col">
{validated.endpoint === "/v1/decisions" ? (
<OpenAIDecisionsForm
@ -328,20 +288,53 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
/>
</TabsContent>
</Tabs>
<div className="flex flex-wrap items-center gap-2">
<Button
variant="ghost"
onClick={() => handlePayloadChange(clearedPayload)}
disabled={rawPayload === clearedPayload}
>
<Eraser />
Clear
</Button>
{view === "json" && (
<Button variant="ghost" onClick={handleFormatJson} disabled={!rawPayload.trim() || hasSyntaxError}>
<Code />
Format JSON
</Button>
)}
<div className="ml-auto flex items-center gap-2">
{isLoading && (
<Button variant="outline" onClick={clearRequestState}>
Cancel request
</Button>
)}
<Button
size="lg"
className="min-w-32"
onClick={handleSend}
disabled={!validation.isValid || isLoading || !effectiveApiKey}
>
{isLoading ? <LoaderCircle className="animate-spin" /> : <Send />}
Send
</Button>
</div>
</div>
</section>
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="Decisions results">
<ResponseView
response={systemOne.data?.response}
view={view}
fallbackModel={validation.payload?.model}
latencyMs={systemOne.data?.latencyMs}
error={systemOne.error?.message}
isLoading={isLoading}
/>
{view === "form" && validated.endpoint === "/v1/systemone" && (
<QuestionBreakdown payload={validated.validation.payload} />
)}
</section>
{hasResult && (
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="Decisions results">
<ResponseView
response={systemOne.data?.response}
view={view}
fallbackModel={validation.payload?.model}
latencyMs={systemOne.data?.latencyMs}
error={systemOne.error?.message}
isLoading={isLoading}
/>
</section>
)}
</div>
</div>
);