Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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}
Comment thread
cursor[bot] marked this conversation as resolved.
PreTag="div"
codeTagProps={CODE_TAG_PROPS}
renderer={renderer}
renderer={LineRows}
>
{value}
</SyntaxHighlighter>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -74,18 +72,15 @@ 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">
Comment thread
greptile-apps[bot] marked this conversation as resolved.
<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={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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
)}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading