From c8cd8852513313e3f08eb713d3a9ebddc55314b3 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:10:50 +0000 Subject: [PATCH] feat(ui): add System One (Jev) tab to the playground (#44043) * feat(ui): add System One (Jev) playground tab Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): validate System One inputs and refresh request context Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): validate System One response payloads Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): fall back to requested model for System One Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(ui): use useMutation and zod schemas for the System One tab, mark it Beta Replace the hand-written request and response guards with zod schemas, which also provide the types. Send requests through useMutation instead of manual loading, error and race-guard state. Unknown spec fields now pass through to the upstream model, and validation errors point at the exact offending key * feat(ui): flag the System One tab as a TypeSafe-only beta * feat(ui): highlighted JSON editor for the System One tab Line numbers, JSON syntax highlighting through the existing react-syntax-highlighter dependency, a valid or issue-count status badge, and a compact path plus message issue list replace the bare textarea and stacked alerts. Answer card type badges now sit on the header row * fix(ui): let the System One results scroll to the bottom The tab panel was viewport height but sat below the tab bar, so its bottom was cut off. On wide screens the editor and results now scroll independently, answers render above the question breakdown, and the raw response no longer nests its own scroll area * feat(ui): color the model, state and questions blocks in the System One editor Tints each top-level request block in the JSON editor and marks the matching breakdown sections with the same color, so it is clear which part of the payload feeds which panel * feat(ui): wrap long lines in the System One JSON editor Long state strings no longer need horizontal scrolling. Each line renders as its own row with its number and block color, so wrapped lines keep their line number and the caret stays aligned * fix(ui): remove horizontal scrolling from the System One tab Long unbroken text in the state, question ids, choice labels and the raw response now wraps instead of widening its box * refactor(ui): replace System One presets with one example and a reset button The tab now starts with a single product review example that uses all three question types, and Reset example restores it after editing * refactor(ui): use an issue triage request as the System One example * fix(ui): type the System One line renderer from exported props rendererProps is not exported by the react-syntax-highlighter types, which broke the dashboard build * fix(ui): address System One review feedback Highlights the score level nearest a fractional calibrated score, keeps extra noul criteria fields in the sent payload, and clears an answer when the request key changes * refactor(ui): parse System One root blocks without mutation and preview all noul criteria The root-block finder is now a tokenizer plus a pure reduce, and the question preview lists every noul criterion that will be sent * refactor(ui): group System One playground files into components and lib Drops the repeated SystemOne prefix from the inner component files and moves the pure logic (schemas, example, payload validation, root block parsing) into lib/. Tests stay colocated with their files, matching the rest of the dashboard. No behavior change * feat(ui): link the decision models discussion from the System One beta notice * feat(ui): ask for decision model feedback in the System One beta notice * feat(ui): make the decision model feedback text the discussion link --------- Co-authored-by: ryan Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../systemOneUI/JsonEditor.test.tsx | 70 +++++ .../components/systemOneUI/JsonEditor.tsx | 167 ++++++++++++ .../systemOneUI/QuestionBreakdown.test.tsx | 33 +++ .../systemOneUI/QuestionBreakdown.tsx | 108 ++++++++ .../systemOneUI/ResponseView.test.tsx | 67 +++++ .../components/systemOneUI/ResponseView.tsx | 196 ++++++++++++++ .../SystemOneUI.integration.test.tsx | 248 +++++++++++++++++ .../components/systemOneUI/SystemOneUI.tsx | 181 +++++++++++++ .../components/systemOneUI/lib/example.ts | 38 +++ .../systemOneUI/lib/rootBlocks.test.ts | 53 ++++ .../components/systemOneUI/lib/rootBlocks.ts | 65 +++++ .../components/systemOneUI/lib/schemas.ts | 120 +++++++++ .../systemOneUI/lib/validatePayload.test.ts | 250 ++++++++++++++++++ .../systemOneUI/lib/validatePayload.ts | 62 +++++ .../playground/llm_calls/system_one.test.ts | 107 ++++++++ .../playground/llm_calls/system_one.ts | 38 +++ .../app/(dashboard)/playground/page.test.tsx | 14 + .../src/app/(dashboard)/playground/page.tsx | 10 +- 18 files changed, 1826 insertions(+), 1 deletion(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/QuestionBreakdown.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/ResponseView.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/ResponseView.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/example.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/rootBlocks.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/rootBlocks.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/schemas.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/validatePayload.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/validatePayload.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx new file mode 100644 index 00000000000..c861c1d466d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx @@ -0,0 +1,70 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import JsonEditor from "./JsonEditor"; +import { validateSystemOnePayload } from "./lib/validatePayload"; + +const validPayload = JSON.stringify( + { state: "Hi", questions: { escalate: { type: "noul", instructions: "Escalate?" } } }, + null, + 2, +); + +describe("JsonEditor", () => { + it("marks a valid payload as ready to send and counts its lines", () => { + render(); + + expect(screen.getByText("Valid payload")).toBeInTheDocument(); + expect(screen.getByRole("status")).toHaveTextContent("Ready to send"); + expect(screen.getByText(`${validPayload.split("\n").length} lines`)).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "System One JSON payload" })).toHaveAttribute("aria-invalid", "false"); + }); + + it("lists each issue with its path and counts only errors in the status badge", () => { + const payload = JSON.stringify({ + state: "Hi", + questions: { + category: { type: "choice", instructions: 1, criteria: { support: "Help" } }, + urgency: { type: "score", instructions: "Rate", criteria: ["Low"] }, + }, + }); + render(); + + expect(screen.getByText("2 issues")).toBeInTheDocument(); + const issues = screen.getByRole("list", { name: "Payload validation issues" }); + expect(issues).toHaveTextContent("questions.category.instructionsInstructions must be a string."); + expect(issues).toHaveTextContent("questions.urgency.criteriaScore criteria must contain at least 2 levels."); + expect(screen.getByRole("textbox", { name: "System One JSON payload" })).toHaveAttribute("aria-invalid", "true"); + }); + + it("shows warnings without counting them as issues", () => { + const payload = JSON.stringify({ + state: "Hi", + questions: { urgency: { type: "score", instructions: "Rate", criteria: Array.from({ length: 11 }, () => "L") } }, + }); + render(); + + expect(screen.getByText("Valid payload")).toBeInTheDocument(); + expect(screen.getByRole("list", { name: "Payload validation issues" })).toHaveTextContent( + "More than 10 score levels may reduce result quality.", + ); + }); + + it("numbers every line, including a trailing empty one, so wrapped lines keep their number", () => { + const value = `${validPayload}\n`; + render(); + + const lineCount = value.split("\n").length; + expect(screen.getByText(`${lineCount} lines`)).toBeInTheDocument(); + expect(screen.getByText(String(lineCount))).toBeInTheDocument(); + expect(screen.queryByText(String(lineCount + 1))).not.toBeInTheDocument(); + }); + + it("reports edits to the caller", () => { + const onChange = vi.fn(); + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "System One JSON payload" }), { target: { value: "{}" } }); + + expect(onChange).toHaveBeenCalledWith("{}"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx new file mode 100644 index 00000000000..1effe55ef41 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx @@ -0,0 +1,167 @@ +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/cva.config"; +import { CircleAlert, CircleCheck, TriangleAlert } from "lucide-react"; +import { useId, useMemo, useRef } from "react"; +import { createElement, PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; +import type { SyntaxHighlighterProps } from "react-syntax-highlighter"; +import json from "react-syntax-highlighter/dist/esm/languages/prism/json"; +import { findRootBlocks, ROOT_BLOCK_STYLES, type RootBlock } from "./lib/rootBlocks"; +import type { SystemOnePayloadValidation } from "./lib/validatePayload"; + +SyntaxHighlighter.registerLanguage("json", json); + +const EDITOR_TEXT = "m-0 whitespace-pre-wrap wrap-anywhere py-3 font-mono text-xs leading-5 [scrollbar-gutter:stable]"; +const GUTTER_WIDTH = "w-11"; +type LineRendererProps = Parameters>[0]; +const CONTENT_INSET = "pl-14 pr-3"; +const CODE_TAG_PROPS = { className: "language-json", style: { whiteSpace: "pre-wrap" } } as const; + +const TOKEN_COLORS = [ + "[&_.token.property]:text-sky-700 dark:[&_.token.property]:text-sky-300", + "[&_.token.string]:text-emerald-700 dark:[&_.token.string]:text-emerald-300", + "[&_.token.number]:text-amber-700 dark:[&_.token.number]:text-amber-300", + "[&_.token.boolean]:text-violet-700 dark:[&_.token.boolean]:text-violet-300", + "[&_.token.null]:text-violet-700 dark:[&_.token.null]:text-violet-300", + "[&_.token.punctuation]:text-muted-foreground [&_.token.operator]:text-muted-foreground", +].join(" "); + +interface JsonEditorProps { + value: string; + onChange: (value: string) => void; + validation: SystemOnePayloadValidation; +} + +function ValidationStatus({ validation }: { validation: SystemOnePayloadValidation }) { + const errorCount = validation.issues.filter((issue) => issue.severity === "error").length; + if (errorCount > 0) { + return ( + + {errorCount} {errorCount === 1 ? "issue" : "issues"} + + ); + } + return Valid payload; +} + +function IssueList({ id, validation }: { id: string; validation: SystemOnePayloadValidation }) { + if (validation.issues.length === 0) { + return ( +

+ + Ready to send +

+ ); + } + return ( +
    + {validation.issues.map((issue, index) => ( +
  • + {issue.severity === "error" ? ( + + ) : ( + + )} + {issue.path} + {issue.message} +
  • + ))} +
+ ); +} + +function renderLines(rootBlocks: readonly RootBlock[]) { + return function LineRows({ rows, stylesheet, useInlineStyles }: LineRendererProps) { + return rows.map((row, line) => { + const lineElement = { node: row, stylesheet, useInlineStyles, key: line }; + const block = rootBlocks.find(({ startLine, endLine }) => line >= startLine && line <= endLine); + return ( +
+ + {line + 1} + + + {createElement(lineElement)} + +
+ ); + }); + }; +} + +export default function JsonEditor({ value, onChange, validation }: JsonEditorProps) { + const issuesId = useId(); + const highlightRef = useRef(null); + const renderer = useMemo(() => renderLines(findRootBlocks(value)), [value]); + const lineCount = value.split("\n").length; + const hasErrors = !validation.isValid; + + return ( +
+
+
+ Request JSON + +
+ + {lineCount} {lineCount === 1 ? "line" : "lines"} + +
+
+