From 77ea70480aa050317303fb5125f2a9501e88d77b Mon Sep 17 00:00:00 2001 From: kerry Date: Sat, 10 Oct 2026 16:57:21 +0000 Subject: [PATCH 1/4] fix(ui): simplify playground decision endpoint Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../SystemOneUI.integration.test.tsx | 97 ++++++++--------- .../components/systemOneUI/SystemOneUI.tsx | 101 ++++++------------ .../playground/llm_calls/system_one.test.ts | 4 +- .../playground/llm_calls/system_one.ts | 5 +- 4 files changed, 82 insertions(+), 125 deletions(-) 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 55f007372d75..9fbaa4bd2c9e 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 @@ -108,12 +108,13 @@ describe("SystemOneUI integration", () => { expect(resetButton).toBeDisabled(); }); - it("explains the native decision endpoint after opting in without announcing it as an alert", async () => { - const user = userEvent.setup(); + it("shows only the literal proxy endpoint and explains it without announcing it as an alert", () => { render(); - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" })); + + expect(screen.getByRole("textbox", { name: "Decision endpoint" })).toHaveValue("/v1/systemone"); + expect(screen.getByRole("textbox", { name: "Decision endpoint" })).toHaveAttribute("readonly"); + expect(screen.queryByRole("combobox", { name: "Decision endpoint" })).not.toBeInTheDocument(); + expect(screen.queryByText(/TypeSafe/)).not.toBeInTheDocument(); expect(screen.getByRole("note", { name: "Decision endpoint notice" })).toHaveTextContent( "omit model to use the proxy's configured default.", @@ -134,7 +135,7 @@ describe("SystemOneUI integration", () => { mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS)); render(); - expect(await screen.findByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"); + await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest")); expect(lookupAuthHeaders(mockModelLookup.mock.calls)).toEqual([expect.arrayContaining(["Bearer session-key"])]); await user.click(screen.getByRole("button", { name: "Send" })); @@ -169,12 +170,26 @@ describe("SystemOneUI integration", () => { expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string).model).toBe("pplx-decider"); }); - it("links to Add Model when the key cannot call any decision model", async () => { + it("routes a configured TypeSafe model through the same proxy endpoint", async () => { const user = userEvent.setup(); + mockModelLookup.mockResolvedValue( + modelGroupInfoResponse([{ model_group: "typesafe/jev-latest", mode: "evaluation" }]), + ); + render(); + await waitFor(() => + expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("typesafe/jev-latest"), + ); + + await user.click(screen.getByRole("button", { name: "Send" })); + + expect(await screen.findByText("Selected choice")).toBeInTheDocument(); + expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/); + expect(mockFetch.mock.calls[0]?.[0]).not.toMatch(/\/typesafe\//); + expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string).model).toBe("typesafe/jev-latest"); + }); + + it("links to Add Model when the key cannot call any decision model", async () => { render(); - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" })); expect(await screen.findByRole("link", { name: "Add a decision model" })).toHaveAttribute( "href", @@ -193,7 +208,7 @@ describe("SystemOneUI integration", () => { await user.type(screen.getByLabelText("Virtual Key", { exact: true }), "sk-typed-key"); - expect(await screen.findByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"); + await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest")); expect(lookupAuthHeaders(mockModelLookup.mock.calls)).toEqual([expect.arrayContaining(["Bearer sk-typed-key"])]); }); @@ -209,10 +224,10 @@ describe("SystemOneUI integration", () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); expect(document.cookie).toContain("token=ui-session"); - expect(screen.getByRole("combobox", { name: "Decision endpoint" })).toHaveTextContent("/typesafe/v1/systemone"); + expect(screen.getByRole("textbox", { name: "Decision endpoint" })).toHaveValue("/v1/systemone"); }); - it("keeps an edited TypeSafe request in place when decision models load afterwards", async () => { + it("keeps an edited request in place when decision models load afterwards", async () => { const models = Promise.withResolvers(); mockModelLookup.mockReturnValue(models.promise); render(); @@ -226,7 +241,7 @@ describe("SystemOneUI integration", () => { }); expect(editor).toHaveValue(draft); - expect(screen.queryByRole("combobox", { name: "Decision model" })).not.toBeInTheDocument(); + expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue(""); }); it("shows invalid JSON and disables Send", async () => { @@ -240,7 +255,7 @@ describe("SystemOneUI integration", () => { expect(screen.getByRole("button", { name: "Send" })).toBeDisabled(); }); - it("preserves the untouched TypeSafe default and renders calibrated answers", async () => { + 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/"); render(); @@ -251,12 +266,12 @@ describe("SystemOneUI integration", () => { expect(await screen.findAllByText("technical")).toHaveLength(2); expect(mockFetch).toHaveBeenCalledTimes(1); const [url, options] = mockFetch.mock.calls[0] ?? []; - expect(url).toBe("https://tenant.example.com/typesafe/v1/systemone"); + expect(url).toBe("https://tenant.example.com/v1/systemone"); expect(options?.method).toBe("POST"); expect(options?.headers).toMatchObject({ "Content-Type": "application/json" }); expect(Object.values(options?.headers as Record)).toContain("Bearer session-key"); expect(JSON.parse(options?.body as string)).toMatchObject({ - model: "jev-latest", + model: "your-decision-model", questions: { has_repro_steps: { type: "noul" } }, }); }); @@ -272,11 +287,11 @@ describe("SystemOneUI integration", () => { await user.click(screen.getByRole("button", { name: "Send" })); - expect(await screen.findByText("jev-latest")).toBeInTheDocument(); + expect(await screen.findByText("your-decision-model")).toBeInTheDocument(); expect(screen.getByText("Selected choice")).toBeInTheDocument(); }); - it("keeps legacy validation when question values have invalid types", async () => { + it("validates native question values before sending", async () => { render(); fireEvent.change(await openJsonEditor(), { @@ -286,25 +301,21 @@ describe("SystemOneUI integration", () => { questions: { category: { type: "choice", - instructions: { text: "Route the message" }, - criteria: { support: { text: "Help" } }, + instructions: 1, + criteria: { support: true }, }, }, }), }, }); - expect(screen.getByText("Instructions must be a string.")).toBeInTheDocument(); - expect(screen.getByText("Choice descriptions must be strings.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Send" })).toBeDisabled(); expect(screen.getByText("Enter a valid request to preview its state and questions.")).toBeInTheDocument(); }); - it("preserves each endpoint draft and sends legacy requests to TypeSafe", async () => { + it("sends structured native questions without changing their payload", async () => { const user = userEvent.setup(); render(); - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" })); const editor = await openJsonEditor(); const draft = JSON.stringify({ model: "my-decider", @@ -317,31 +328,15 @@ describe("SystemOneUI integration", () => { expect(screen.getByRole("button", { name: "Send" })).toBeEnabled(); expect(screen.getByText(/"text": "Help"/)).toBeInTheDocument(); - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "TypeSafe · /typesafe/v1/systemone" })); - await user.click(screen.getByRole("button", { name: "Send" })); - expect(await screen.findByText("Selected choice")).toBeInTheDocument(); - expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/typesafe\/v1\/systemone$/); - expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string).model).toBe("jev-latest"); - - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" })); - expect(editor).toHaveValue(draft); - expect(screen.queryByText("Selected choice")).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Send" })); expect(await screen.findByText("Selected choice")).toBeInTheDocument(); - expect(mockFetch.mock.calls[1]?.[0]).toMatch(/\/v1\/systemone$/); - expect(JSON.parse(mockFetch.mock.calls[1]?.[1]?.body as string)).toEqual(JSON.parse(draft)); + expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/); + expect(JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string)).toEqual(JSON.parse(draft)); }); it("sends a native request without model so the proxy can select its default", async () => { const user = userEvent.setup(); render(); - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" })); const payload = { state: "An outage", questions: { urgent: { type: "noul", instructions: "Is this urgent?" } } }; fireEvent.change(await openJsonEditor(), { target: { value: JSON.stringify(payload) }, @@ -355,7 +350,7 @@ describe("SystemOneUI integration", () => { expect(body).not.toHaveProperty("model"); }); - it.each(["success", "error"])("ignores a late %s after switching endpoints", async (outcome) => { + it.each(["success", "error"])("ignores a late %s after editing the request", async (outcome) => { const user = userEvent.setup(); const pending = Promise.withResolvers(); mockFetch.mockReturnValueOnce(pending.promise); @@ -367,9 +362,7 @@ describe("SystemOneUI integration", () => { ); await user.click(screen.getByRole("button", { name: "Send" })); await screen.findByRole("button", { name: "Cancel request" }); - screen.getByRole("combobox", { name: "Decision endpoint" }).focus(); - await user.keyboard("{ArrowDown}"); - await user.click(await screen.findByRole("option", { name: "Decisions · /v1/systemone" })); + fireEvent.change(screen.getByRole("textbox", { name: "Input" }), { target: { value: "A new request" } }); expect(mockFetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); expect(screen.getByRole("button", { name: "Send" })).toBeEnabled(); @@ -554,9 +547,9 @@ describe("SystemOneUI integration", () => { expect(await screen.findByText("Selected choice")).toBeInTheDocument(); expect(screen.queryByRole("textbox", { name: "System One JSON payload" })).not.toBeInTheDocument(); - expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/typesafe\/v1\/systemone$/); + expect(mockFetch.mock.calls[0]?.[0]).toMatch(/\/v1\/systemone$/); expect(sentBody()).toEqual({ - model: "jev-latest", + model: "your-decision-model", state: "The login page is blank after the upgrade", questions: { team: { @@ -575,7 +568,7 @@ describe("SystemOneUI integration", () => { const user = userEvent.setup(); mockModelLookup.mockResolvedValue(modelGroupInfoResponse(DECISION_AND_CHAT_MODELS)); render(); - expect(await screen.findByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest"); + await waitFor(() => expect(screen.getByRole("combobox", { name: "Decision model" })).toHaveValue("jev-latest")); const level = question("severity").getByRole("textbox", { name: "Level 0" }); fill(level, "Typo"); 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 0ac39cb5ede5..2752d79232f4 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 @@ -12,9 +12,9 @@ import { useMutation } from "@tanstack/react-query"; import { Code, Info, LoaderCircle, RotateCcw, Send } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { makeSystemOneRequest } from "../../llm_calls/system_one"; -import { PLACEHOLDER_DECISION_MODEL, SYSTEM_ONE_EXAMPLE, decisionsExample } from "./lib/example"; +import { PLACEHOLDER_DECISION_MODEL, decisionsExample } from "./lib/example"; import { payloadModel, withPayloadModel } from "./lib/payloadModel"; -import type { DecisionEndpoint, PlaygroundRequest } from "./lib/schemas"; +import type { PlaygroundRequest } from "./lib/schemas"; import JsonEditor from "./JsonEditor"; import QuestionBreakdown from "./QuestionBreakdown"; import ResponseView from "./ResponseView"; @@ -31,12 +31,10 @@ type EditorView = "form" | "json"; interface SystemOneSendVariables { payload: PlaygroundRequest; - endpoint: DecisionEndpoint; apiKey: string; signal: AbortSignal; } -const EXAMPLE_PAYLOAD = JSON.stringify(SYSTEM_ONE_EXAMPLE, null, 2); const DECISION_MODELS_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/44231"; function getCustomProxyBaseUrl(): string | undefined { @@ -52,23 +50,17 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = effectiveApiKey, getCustomProxyBaseUrl(), ); - const [chosenEndpoint, setChosenEndpoint] = useState(null); - const endpoint: DecisionEndpoint = - chosenEndpoint ?? (decisionModels.length > 0 ? "/v1/systemone" : "/typesafe/v1/systemone"); - const [drafts, setDrafts] = useState>>({}); + const [draft, setDraft] = useState(null); const [view, setView] = useState("form"); - const examplePayload = - endpoint === "/v1/systemone" - ? JSON.stringify(decisionsExample(decisionModels[0] ?? PLACEHOLDER_DECISION_MODEL), null, 2) - : EXAMPLE_PAYLOAD; - const rawPayload = drafts[endpoint] ?? examplePayload; + const examplePayload = JSON.stringify(decisionsExample(decisionModels[0] ?? PLACEHOLDER_DECISION_MODEL), null, 2); + const rawPayload = draft ?? examplePayload; const activeController = useRef(null); - const validation = useMemo(() => validateSystemOnePayload(rawPayload, endpoint), [rawPayload, endpoint]); + const validation = useMemo(() => validateSystemOnePayload(rawPayload, "/v1/systemone"), [rawPayload]); const hasSyntaxError = validation.issues.some((issue) => issue.path === "syntax"); const systemOne = useMutation({ - mutationFn: ({ payload, apiKey, signal, endpoint }: SystemOneSendVariables) => - makeSystemOneRequest(payload, apiKey, getCustomProxyBaseUrl(), { signal, endpoint }), + mutationFn: ({ payload, apiKey, signal }: SystemOneSendVariables) => + makeSystemOneRequest(payload, apiKey, getCustomProxyBaseUrl(), { signal }), }); const isLoading = systemOne.isPending; const { reset: resetSystemOne } = systemOne; @@ -90,15 +82,13 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = function handlePayloadChange(value: string) { if (value !== rawPayload) { clearRequestState(); - setChosenEndpoint(endpoint); - setDrafts((current) => ({ ...current, [endpoint]: value })); + setDraft(value); } } function handleResetExample() { clearRequestState(); - setChosenEndpoint(endpoint); - setDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== endpoint))); + setDraft(null); } function handleModelPick(model: string | null) { @@ -119,10 +109,10 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = return; } clearRequestState(); - setChosenEndpoint(endpoint); + setDraft(rawPayload); const controller = new AbortController(); activeController.current = controller; - const variables = { payload: validation.payload, apiKey: effectiveApiKey, signal: controller.signal, endpoint }; + const variables = { payload: validation.payload, apiKey: effectiveApiKey, signal: controller.signal }; systemOne.mutate(variables); } @@ -131,46 +121,24 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
Endpoint - - {endpoint === "/v1/systemone" && ( - <> - 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 - - )} - + + 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 + )}
@@ -230,13 +198,10 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
- - {endpoint === "/v1/systemone" ? "Decision models · /v1/systemone" : "TypeSafe Jev · /typesafe/v1/systemone"} - + /v1/systemone - {endpoint === "/v1/systemone" - ? "Sends choice, noul, and score questions through /v1/systemone to a decision model on your proxy. Pick one under Model, or omit model to use the proxy's configured default." - : "Sends requests through /typesafe/v1/systemone and requires TYPESAFE_API_KEY on the proxy."}{" "} + Sends choice, noul, and score questions through /v1/systemone to a decision model on your proxy. Pick one + under Model, or omit model to use the proxy's configured default.{" "} How to call /v1/decisions and /v1/systemone diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.test.ts index e097f2d3d26f..b594941d0779 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.test.ts @@ -72,7 +72,7 @@ describe("makeSystemOneRequest", () => { expect(mockFetch).toHaveBeenCalledTimes(1); expect(mockFetch).toHaveBeenCalledWith( - "https://custom.example.com/typesafe/v1/systemone", + "https://custom.example.com/v1/systemone", expect.objectContaining(expectedRequest), ); expect(result.response).toEqual(responseBody); @@ -82,7 +82,7 @@ describe("makeSystemOneRequest", () => { it("uses the proxy base URL when no custom base is provided", async () => { await makeSystemOneRequest(payload, "session-key"); - expect(mockFetch.mock.calls[0]?.[0]).toBe("https://proxy.example.com/typesafe/v1/systemone"); + expect(mockFetch.mock.calls[0]?.[0]).toBe("https://proxy.example.com/v1/systemone"); }); it("throws the upstream error text for failed requests", async () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.ts index 6c29d9110ac8..b688038210df 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/system_one.ts @@ -5,7 +5,6 @@ import { createApiClient } from "@/lib/http/client"; import { isDecisionMode } from "@/lib/decisionModels"; import { systemOneResponseSchema, - type DecisionEndpoint, type PlaygroundRequest, type SystemOneResponse, } from "../components/systemOneUI/lib/schemas"; @@ -55,10 +54,10 @@ export async function makeSystemOneRequest( payload: PlaygroundRequest, accessToken: string, customBaseUrl?: string, - { signal, endpoint = "/typesafe/v1/systemone" }: { signal?: AbortSignal; endpoint?: DecisionEndpoint } = {}, + { signal }: { signal?: AbortSignal } = {}, ): Promise { const startedAt = performance.now(); - const body = await proxyClient(customBaseUrl).post(endpoint, { + const body = await proxyClient(customBaseUrl).post("/v1/systemone", { body: payload, headers: authHeaders(accessToken), signal, From ecc9eec65ac23acc8e70e0d277685e6c4794337f Mon Sep 17 00:00:00 2001 From: kerry Date: Sat, 10 Oct 2026 17:05:33 +0000 Subject: [PATCH 2/4] fix(ui): expose both decision API shapes in playground Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../components/systemOneUI/JsonEditor.tsx | 22 ++- .../components/systemOneUI/ResponseView.tsx | 64 +++++---- .../SystemOneUI.integration.test.tsx | 125 ++++++++++++++++- .../components/systemOneUI/SystemOneUI.tsx | 126 ++++++++++++------ .../systemOneUI/lib/decisions.test.ts | 53 +++++++- .../components/systemOneUI/lib/example.ts | 31 +++++ .../systemOneUI/lib/openAIDecisions.ts | 98 ++++++++++++++ .../systemOneUI/lib/validatePayload.ts | 33 +++-- .../playground/llm_calls/system_one.test.ts | 90 ++++++++++++- .../playground/llm_calls/system_one.ts | 57 ++++++-- 10 files changed, 593 insertions(+), 106 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/lib/openAIDecisions.ts 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 index cc7bebb0bb82..7311c3a055a1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx @@ -7,7 +7,7 @@ import { createElement, PrismLight as SyntaxHighlighter } from "react-syntax-hig import type { SyntaxHighlighterProps } from "react-syntax-highlighter"; import json from "react-syntax-highlighter/dist/esm/languages/prism/json"; import { vs, vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; -import type { SystemOnePayloadValidation } from "./lib/validatePayload"; +import type { PayloadValidation } from "./lib/validatePayload"; SyntaxHighlighter.registerLanguage("json", json); @@ -57,10 +57,12 @@ const TRANSPARENT_PRE = { interface JsonEditorProps { value: string; onChange: (value: string) => void; - validation: SystemOnePayloadValidation; + validation: PayloadValidation; + label?: string; + placeholder?: string; } -export function ValidationStatus({ validation }: { validation: SystemOnePayloadValidation }) { +export function ValidationStatus({ validation }: { validation: PayloadValidation }) { const errorCount = validation.issues.filter((issue) => issue.severity === "error").length; if (errorCount > 0) { return ( @@ -72,7 +74,7 @@ export function ValidationStatus({ validation }: { validation: SystemOnePayloadV return Valid payload; } -export function IssueList({ id, validation }: { id: string; validation: SystemOnePayloadValidation }) { +export function IssueList({ id, validation }: { id: string; validation: PayloadValidation }) { if (validation.issues.length === 0) { return (

@@ -112,7 +114,13 @@ function LineRows({ rows, stylesheet, useInlineStyles }: LineRendererProps) { }); } -export default function JsonEditor({ value, onChange, validation }: JsonEditorProps) { +export default function JsonEditor({ + value, + onChange, + validation, + label = "System One JSON payload", + placeholder = "Paste or write a System One request", +}: JsonEditorProps) { const issuesId = useId(); const highlightRef = useRef(null); const syntaxTheme = useTheme().resolvedTheme === "dark" ? VS_CODE_DARK_PLUS : VS_CODE_LIGHT_PLUS; @@ -156,7 +164,7 @@ export default function JsonEditor({ value, onChange, validation }: JsonEditorPr