feat(ui): rename the Playground System One tab to Decisions and give the JSON editor VS Code light and dark themes (#45729)

* fix(ui): render the System One JSON editor with the stock One Dark theme

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(ui): rename the Playground System One tab to Decisions and use the VS Code Dark+ JSON theme

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep the JSON editor header and issue list on the light surface

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(ui): drop the root block scanner test and the unused band styles

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(ui): follow the dashboard theme in the JSON editor and drop the breakdown accents

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): round the Decisions score pill to two decimals

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): strip the Prism block backgrounds so the JSON editor theme swap stops the React style warning

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-09 23:30:22 -07:00 • committed by GitHub
parent 15154a40a8
commit 512692dd36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 85 additions and 176 deletions

View file

@ -1,11 +1,12 @@
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 { useTheme } from "next-themes";
import { useId, 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 { vs, vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import type { SystemOnePayloadValidation } from "./lib/validatePayload";
SyntaxHighlighter.registerLanguage("json", json);
@ -14,16 +15,44 @@ const EDITOR_TEXT = "m-0 whitespace-pre-wrap wrap-anywhere py-3 font-mono text-x
const GUTTER_WIDTH = "w-11";
type LineRendererProps = Parameters<NonNullable<SyntaxHighlighterProps["renderer"]>>[0];
const CONTENT_INSET = "pl-14 pr-3";
const CODE_TAG_PROPS = { className: "language-json", style: { whiteSpace: "pre-wrap" } } as const;
type SyntaxTheme = NonNullable<SyntaxHighlighterProps["style"]>;
const THEME_BLOCKS = ['pre[class*="language-"]', 'code[class*="language-"]'] 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(" ");
function withoutBlockBackground(theme: SyntaxTheme): SyntaxTheme {
const stripped = THEME_BLOCKS.map((block) => {
const { background: _background, backgroundColor: _backgroundColor, ...rest } = theme[block] ?? {};
return [block, rest] as const;
});
return { ...theme, ...Object.fromEntries(stripped) };
}
const VS_CODE_DARK_PLUS = withoutBlockBackground(vscDarkPlus);
const VS_CODE_LIGHT_PLUS = {
...withoutBlockBackground(vs),
property: { color: "#0451a5" },
string: { color: "#a31515" },
number: { color: "#098658" },
boolean: { color: "#0000ff" },
punctuation: { color: "#000000" },
operator: { color: "#000000" },
} as const;
const INHERIT_TEXT = {
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
textShadow: "none",
} as const;
const CODE_TAG_PROPS = { className: "language-json", style: INHERIT_TEXT } as const;
const TRANSPARENT_PRE = {
...INHERIT_TEXT,
background: "transparent",
border: 0,
margin: 0,
padding: 0,
overflow: "visible",
} as const;
interface JsonEditorProps {
value: string;
@ -69,39 +98,24 @@ export function IssueList({ id, validation }: { id: string; validation: SystemOn
);
}
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 (
<div key={line} className="flex">
<span className={cn(GUTTER_WIDTH, "shrink-0 select-none pr-3 text-right text-muted-foreground")}>
{line + 1}
</span>
<span
className={cn(
"relative min-h-5 min-w-0 flex-1 px-3",
block && [
"before:absolute before:inset-y-0 before:left-0 before:w-0.5",
ROOT_BLOCK_STYLES[block.key].band,
line === block.startLine && "rounded-t-sm before:rounded-t-sm",
line === block.endLine && "rounded-b-sm before:rounded-b-sm",
],
)}
>
{createElement(lineElement)}
</span>
</div>
);
});
};
function LineRows({ rows, stylesheet, useInlineStyles }: LineRendererProps) {
return rows.map((row, line) => {
const lineElement = { node: row, stylesheet, useInlineStyles, key: line };
return (
<div key={line} className="flex">
<span className={cn(GUTTER_WIDTH, "shrink-0 select-none pr-3 text-right text-zinc-400 dark:text-zinc-500")}>
{line + 1}
</span>
<span className="min-h-5 min-w-0 flex-1 px-3">{createElement(lineElement)}</span>
</div>
);
});
}
export default function JsonEditor({ value, onChange, validation }: JsonEditorProps) {
const issuesId = useId();
const highlightRef = useRef<HTMLDivElement>(null);
const renderer = useMemo(() => renderLines(findRootBlocks(value)), [value]);
const syntaxTheme = useTheme().resolvedTheme === "dark" ? VS_CODE_DARK_PLUS : VS_CODE_LIGHT_PLUS;
const lineCount = value.split("\n").length;
const hasErrors = !validation.isValid;
@ -121,20 +135,22 @@ export default function JsonEditor({ value, onChange, validation }: JsonEditorPr
{lineCount} {lineCount === 1 ? "line" : "lines"}
</span>
</div>
<div className="relative min-h-80 flex-1">
<div aria-hidden="true" className={cn(GUTTER_WIDTH, "absolute inset-y-0 left-0 border-r bg-muted/50")} />
<div className="relative min-h-80 flex-1 bg-background dark:bg-[#1e1e1e]">
<div
ref={highlightRef}
aria-hidden="true"
className={cn("absolute inset-0 overflow-hidden", EDITOR_TEXT, TOKEN_COLORS)}
>
className={cn(
GUTTER_WIDTH,
"absolute inset-y-0 left-0 border-r bg-muted/50 dark:border-zinc-700 dark:bg-zinc-800/60",
)}
/>
<div ref={highlightRef} aria-hidden="true" className={cn("absolute inset-0 overflow-hidden", EDITOR_TEXT)}>
<SyntaxHighlighter
language="json"
style={{}}
useInlineStyles={false}
style={syntaxTheme}
customStyle={TRANSPARENT_PRE}
PreTag="div"
codeTagProps={CODE_TAG_PROPS}
renderer={renderer}
renderer={LineRows}
>
{value}
</SyntaxHighlighter>

View file

@ -1,7 +1,5 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/cva.config";
import { ROOT_BLOCK_STYLES } from "./lib/rootBlocks";
import type { PlaygroundQuestion, PlaygroundRequest } from "./lib/schemas";
function formatState(state: unknown): string {
@ -74,10 +72,7 @@ export default function QuestionBreakdown({ payload }: { payload?: PlaygroundReq
<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={cn("grid gap-2 border-l-2 pl-3", ROOT_BLOCK_STYLES.state.accent)}
>
<section aria-labelledby="system-one-state-heading" className="grid gap-2">
<h3 id="system-one-state-heading" className="text-sm font-medium">
State
</h3>
@ -85,7 +80,7 @@ export default function QuestionBreakdown({ payload }: { payload?: PlaygroundReq
{formatState(payload.state)}
</pre>
</section>
<div className={cn("grid gap-3 border-l-2 pl-3", ROOT_BLOCK_STYLES.questions.accent)}>
<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">

View file

@ -59,6 +59,20 @@ describe("ResponseView", () => {
expect(screen.getByRole("meter", { name: "1 probability" })).toHaveAttribute("aria-valuetext", "0%");
});
it("rounds the calibrated score pill to two decimals and leaves integers bare", () => {
const precise: SystemOneResponse = {
answers: {
severity: { type: "score", score: 3.9809950180008897, probabilities: { "3": 0.02, "4": 0.98 } },
clarity: { type: "score", score: 2, probabilities: { "2": 1 }, legend: { "2": "Clear enough" } },
},
};
render(<ResponseView response={precise} isLoading={false} />);
expect(screen.getByText("3.98")).toBeInTheDocument();
expect(screen.queryByText("3.9809950180008897")).not.toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
});
it("renders structured decision score legends without coercing objects to strings", () => {
const response: SystemOneResponse = {
answers: {

View file

@ -7,6 +7,8 @@ import { ChevronDown, LoaderCircle } from "lucide-react";
import { useState } from "react";
import type { SystemOneAnswer, SystemOneResponse } from "./lib/schemas";
const SCORE_FORMAT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
interface ResponseViewProps {
response?: SystemOneResponse;
fallbackModel?: string;
@ -85,7 +87,7 @@ function AnswerDetails({ answer }: { answer: SystemOneAnswer }) {
<div className="grid gap-4">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-muted-foreground">Score</span>
<Badge>{answer.score}</Badge>
<Badge>{SCORE_FORMAT.format(answer.score)}</Badge>
{answer.confidence !== undefined && (
<Badge variant="outline">{Math.round(answer.confidence * 100)}% confidence</Badge>
)}

View file

@ -1,53 +0,0 @@
import { describe, expect, it } from "vitest";
import { findRootBlocks } from "./rootBlocks";
describe("findRootBlocks", () => {
it("maps each top-level request key to the lines its value spans", () => {
const text = JSON.stringify(
{
model: "jev-latest",
state: { message: "Hi" },
questions: { escalate: { type: "noul", instructions: "Escalate?" } },
},
null,
2,
);
expect(findRootBlocks(text)).toEqual([
{ key: "model", startLine: 1, endLine: 1 },
{ key: "state", startLine: 2, endLine: 4 },
{ key: "questions", startLine: 5, endLine: 10 },
]);
});
it("ignores nested keys, unrelated root keys and braces or quotes inside strings", () => {
const text = [
"{",
' "temperature": 0,',
' "state": "a } tricky \\" { string",',
' "questions": {',
' "model": { "state": "nested" }',
" }",
"}",
].join("\n");
expect(findRootBlocks(text)).toEqual([
{ key: "state", startLine: 2, endLine: 2 },
{ key: "questions", startLine: 3, endLine: 5 },
]);
});
it("finds every block on a single minified line", () => {
expect(findRootBlocks('{"state":"Hi","questions":{"a":{"type":"noul"}}}')).toEqual([
{ key: "state", startLine: 0, endLine: 0 },
{ key: "questions", startLine: 0, endLine: 0 },
]);
});
it("keeps highlighting an unfinished block while the user is typing", () => {
expect(findRootBlocks('{\n "state": "Hi",\n "questions": {\n "a": {')).toEqual([
{ key: "state", startLine: 1, endLine: 1 },
{ key: "questions", startLine: 2, endLine: 3 },
]);
});
});

View file

@ -1,65 +0,0 @@
export type RootBlockKey = "model" | "state" | "questions";
export interface RootBlock {
readonly key: RootBlockKey;
readonly startLine: number;
readonly endLine: number;
}
export const ROOT_BLOCK_STYLES: Readonly<Record<RootBlockKey, { band: string; accent: string }>> = {
model: { band: "bg-blue-500/10 before:bg-blue-500/60", accent: "border-l-blue-500/60" },
state: { band: "bg-green-500/10 before:bg-green-500/60", accent: "border-l-green-500/60" },
questions: { band: "bg-orange-500/10 before:bg-orange-500/60", accent: "border-l-orange-500/60" },
};
interface Token {
readonly text: string;
readonly line: number;
}
interface ScanState {
readonly depth: number;
readonly previous?: Token;
readonly open?: { readonly key: RootBlockKey; readonly startLine: number };
readonly blocks: readonly RootBlock[];
}
const JSON_TOKEN = /"(?:[^"\\\n]|\\.)*"?|[{}[\],:]|[^\s{}[\],:"]+/g;
const isRootBlockKey = (key: string): key is RootBlockKey => Object.hasOwn(ROOT_BLOCK_STYLES, key);
const tokenize = (text: string): Token[] =>
text
.split("\n")
.flatMap((lineText, line) => Array.from(lineText.matchAll(JSON_TOKEN), ([match]) => ({ text: match, line })));
const closeOpenBlock = ({ open, previous, blocks }: ScanState): readonly RootBlock[] =>
open && previous ? [...blocks, { ...open, endLine: previous.line }] : blocks;
function step(state: ScanState, token: Token): ScanState {
const next = { ...state, previous: token };
switch (token.text) {
case "{":
case "[":
return { ...next, depth: state.depth + 1 };
case "}":
case "]":
return state.depth === 1
? { ...next, depth: 0, open: undefined, blocks: closeOpenBlock(state) }
: { ...next, depth: state.depth - 1 };
case ",":
return state.depth === 1 ? { ...next, open: undefined, blocks: closeOpenBlock(state) } : next;
case ":": {
const key = state.previous?.text.slice(1, -1) ?? "";
return state.depth === 1 && state.previous?.text.startsWith('"') && isRootBlockKey(key)
? { ...next, open: { key, startLine: state.previous.line } }
: next;
}
default:
return next;
}
}
export function findRootBlocks(text: string): readonly RootBlock[] {
return closeOpenBlock(tokenize(text).reduce(step, { depth: 0, blocks: [] }));
}

View file

@ -87,10 +87,10 @@ describe("PlaygroundPage ?tab= deep link", () => {
expect(screen.getByRole("tab", { name: "Chat" })).toHaveAttribute("aria-selected", "false");
});
it("activates System One from ?tab=system-one", () => {
it("activates Decisions from ?tab=system-one", () => {
renderWithProviders(<PlaygroundPage />, { searchParams: { tab: "system-one" } });
const tab = screen.getByRole("tab", { name: /^System One/ });
const tab = screen.getByRole("tab", { name: /^Decisions/ });
expect(tab).toHaveAttribute("aria-selected", "true");
expect(tab).toHaveTextContent("Beta");
expect(screen.getByTestId("system-one-ui")).toBeInTheDocument();

View file

@ -70,7 +70,7 @@ export default function PlaygroundPage() {
Compliance
</TabsTrigger>
<TabsTrigger value="system-one" className="flex-none">
<BetaBadge>System One</BetaBadge>
<BetaBadge>Decisions</BetaBadge>
</TabsTrigger>
<TabsTrigger value="agent-builder" className="flex-none">
Agent Builder (Experimental)