Skip to content

Commit f0d60d1

Browse files
committed
feat(frontend): add Run-on mode selector to the evaluator playground
Adds a 'Run on' control to the evaluator (LLM-as-a-judge) playground header so the first/empty state explains itself instead of leaving the user with two disconnected loaders. Three modes, each drawing its own data-flow: - Run directly on a test case (Data -> Evaluator -> Score) - Run on an app output (Data -> App -> Output -> Evaluator -> Score) - default - Run on a trace (Trace -> Evaluator -> Score) - disabled for now The mode is persisted per project; a connected app forces effective 'app' mode. In app mode with no app connected, the run panel hides the testcases and shows a centered 'Select an app' empty state (shared with the evaluator-creation drawer). All colors come from the antd theme token so it follows light/dark mode. Prompt playground is intentionally untouched.
1 parent de548da commit f0d60d1

6 files changed

Lines changed: 486 additions & 25 deletions

File tree

web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorPlaygroundHeader.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ import {Button, Tooltip, Typography} from "antd"
2020
import {useAtomValue, useSetAtom} from "jotai"
2121
import dynamic from "next/dynamic"
2222

23-
import {disconnectAppFromEvaluatorAtom, selectedAppLabelAtom} from "./atoms"
23+
import {
24+
disconnectAppFromEvaluatorAtom,
25+
effectiveRunOnModeAtom,
26+
runOnModeAtom,
27+
selectedAppLabelAtom,
28+
type RunOnMode,
29+
} from "./atoms"
30+
import RunOnSelector from "./RunOnSelector"
2431

2532
const TestsetDropdown = dynamic(
2633
() => import("@/oss/components/Playground/Components/TestsetDropdown"),
@@ -77,6 +84,23 @@ const EvaluatorPlaygroundHeader: React.FC<EvaluatorPlaygroundHeaderProps> = ({
7784
disconnectApp()
7885
}, [disconnectApp])
7986

87+
// Run-on mode — drives which loaders are surfaced. A connected app forces
88+
// "app" mode (see effectiveRunOnModeAtom); the stored mode only matters when
89+
// nothing is connected.
90+
const runOnMode = useAtomValue(effectiveRunOnModeAtom)
91+
const setRunOnMode = useSetAtom(runOnModeAtom)
92+
const handlePickRunOn = useCallback(
93+
(next: RunOnMode) => {
94+
if (next === "trace") return // disabled, not selectable
95+
// Leaving "app" mode means dropping the connected app so the graph
96+
// returns to standalone-evaluator shape.
97+
if (next === "data") disconnectApp()
98+
setRunOnMode(next)
99+
},
100+
[disconnectApp, setRunOnMode],
101+
)
102+
const isAppMode = runOnMode === "app"
103+
80104
// Check if we have an app node (depth-0 with a different entity than evaluator)
81105
const hasAppSelected = nodes.some((n) => n.depth === 0 && n.entityId !== evaluatorEntityId)
82106

@@ -100,15 +124,18 @@ const EvaluatorPlaygroundHeader: React.FC<EvaluatorPlaygroundHeaderProps> = ({
100124
</div>
101125

102126
<div className="flex min-w-0 flex-1 items-center justify-end gap-1">
103-
<EntityPicker<WorkflowRevisionSelectionResult>
104-
variant="popover-cascader"
105-
adapter={appWorkflowAdapter}
106-
onSelect={onAppSelect}
107-
size="small"
108-
placeholder={selectedAppLabel ?? "Select app"}
109-
popupFooter={popupFooter}
110-
/>
111-
{hasAppSelected && (
127+
<RunOnSelector mode={runOnMode} onPick={handlePickRunOn} />
128+
{isAppMode && (
129+
<EntityPicker<WorkflowRevisionSelectionResult>
130+
variant="popover-cascader"
131+
adapter={appWorkflowAdapter}
132+
onSelect={onAppSelect}
133+
size="small"
134+
placeholder={selectedAppLabel ?? "Select app"}
135+
popupFooter={popupFooter}
136+
/>
137+
)}
138+
{isAppMode && hasAppSelected && (
112139
<Tooltip title="Disconnect app">
113140
<Button
114141
type="text"
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/**
2+
* RunOnSelector
3+
*
4+
* The "Run on" control for the evaluator playground header. A leading dropdown
5+
* that names the data source the evaluator runs against and draws the resulting
6+
* data-flow, so the empty/first state explains itself instead of leaving the
7+
* user with two disconnected loaders.
8+
*
9+
* Three modes:
10+
* - Run directly on data (Data → Evaluator → Score)
11+
* - Run on an app (Data → App → Output → Evaluator → Score) — default
12+
* - Run on a trace (Trace → Evaluator → Score) — disabled for now
13+
*
14+
* All colors come from the live antd token (`theme.useToken()`) so the control
15+
* follows light/dark mode automatically.
16+
*/
17+
18+
import {useState} from "react"
19+
20+
import {AppstoreOutlined} from "@ant-design/icons"
21+
import {
22+
CaretDownIcon,
23+
CheckIcon,
24+
DatabaseIcon,
25+
GavelIcon,
26+
TreeViewIcon,
27+
} from "@phosphor-icons/react"
28+
import {Button, Dropdown, theme} from "antd"
29+
import type {GlobalToken} from "antd"
30+
import clsx from "clsx"
31+
32+
import type {RunOnMode} from "./atoms"
33+
34+
// The app icon used across the product (the sidebar "Prompts" item). Wrapped so
35+
// it accepts the same `size`/`style` props as the phosphor icons it sits beside.
36+
const AppIcon = ({size = 16, style}: {size?: number; style?: React.CSSProperties}) => (
37+
<AppstoreOutlined style={{fontSize: size, ...style}} />
38+
)
39+
40+
// ── flow pills ──────────────────────────────────────────────────────────────
41+
42+
type FlowVariant = "data" | "app" | "out" | "eval" | "trace"
43+
44+
interface FlowNode {
45+
label: string
46+
variant: FlowVariant
47+
}
48+
49+
const flowStyle = (token: GlobalToken, variant: FlowVariant): React.CSSProperties => {
50+
switch (variant) {
51+
case "data":
52+
return {background: token.blue1, color: token.blue7, borderColor: token.blue2}
53+
case "app":
54+
return {
55+
background: token.colorPrimaryBg,
56+
color: token.colorText,
57+
borderColor: token.colorPrimaryBorder,
58+
}
59+
case "out":
60+
return {background: token.green1, color: token.green7, borderColor: token.green3}
61+
case "eval":
62+
// index 7 (not 6) so the text brightens under the dark algorithm —
63+
// purple6 lands dark-on-dark and disappears on a dark background.
64+
return {background: token.purple1, color: token.purple7, borderColor: token.purple3}
65+
case "trace":
66+
return {background: token.cyan1, color: token.cyan7, borderColor: token.cyan3}
67+
}
68+
}
69+
70+
const FlowIcon = ({variant}: {variant: FlowVariant}) => {
71+
switch (variant) {
72+
case "data":
73+
return <DatabaseIcon size={12} />
74+
case "app":
75+
return <AppIcon size={12} />
76+
case "eval":
77+
return <GavelIcon size={12} />
78+
case "trace":
79+
return <TreeViewIcon size={12} />
80+
default:
81+
return null
82+
}
83+
}
84+
85+
const FlowPills = ({steps, token}: {steps: FlowNode[]; token: GlobalToken}) => (
86+
<div className="flex flex-wrap items-center gap-y-1">
87+
{steps.map((step, i) => (
88+
<span key={`${step.label}-${i}`} className="flex items-center">
89+
{i > 0 && (
90+
<span className="px-1.5 text-[12px]" style={{color: token.colorTextQuaternary}}>
91+
92+
</span>
93+
)}
94+
<span
95+
className="inline-flex items-center gap-1 whitespace-nowrap rounded-full border border-solid px-2 py-[3px] text-[11px] leading-none"
96+
style={flowStyle(token, step.variant)}
97+
>
98+
<FlowIcon variant={step.variant} />
99+
{step.label}
100+
</span>
101+
</span>
102+
))}
103+
</div>
104+
)
105+
106+
// ── modes ───────────────────────────────────────────────────────────────────
107+
108+
interface ModeDef {
109+
key: RunOnMode
110+
/** Full label shown in the dropdown option. */
111+
label: string
112+
/** Short label shown after "Run on:" in the trigger button. */
113+
shortLabel: string
114+
Icon: React.ComponentType<{size?: number; style?: React.CSSProperties}>
115+
desc: string
116+
flow: FlowNode[]
117+
badge?: "default" | "soon"
118+
disabled?: boolean
119+
}
120+
121+
const MODES: ModeDef[] = [
122+
{
123+
key: "data",
124+
label: "Run directly on a test case",
125+
shortLabel: "Test case",
126+
Icon: DatabaseIcon,
127+
desc: "Evaluate data you provide. Connect a test set, or type the input and output in by hand.",
128+
flow: [
129+
{label: "Data", variant: "data"},
130+
{label: "Evaluator", variant: "eval"},
131+
{label: "Score", variant: "out"},
132+
],
133+
},
134+
{
135+
key: "app",
136+
label: "Run on an app output",
137+
shortLabel: "App output",
138+
Icon: AppIcon,
139+
badge: "default",
140+
desc: "Run an app over your data, then the evaluator grades its output. The usual evaluation flow.",
141+
flow: [
142+
{label: "Data", variant: "data"},
143+
{label: "App", variant: "app"},
144+
{label: "Output", variant: "out"},
145+
{label: "Evaluator", variant: "eval"},
146+
{label: "Score", variant: "out"},
147+
],
148+
},
149+
{
150+
key: "trace",
151+
label: "Run on a trace",
152+
shortLabel: "Trace",
153+
Icon: TreeViewIcon,
154+
badge: "soon",
155+
disabled: true,
156+
desc: "Pull the input and output straight from a logged trace in Observability.",
157+
flow: [
158+
{label: "Trace", variant: "trace"},
159+
{label: "Evaluator", variant: "eval"},
160+
{label: "Score", variant: "out"},
161+
],
162+
},
163+
]
164+
165+
// ── component ───────────────────────────────────────────────────────────────
166+
167+
interface RunOnSelectorProps {
168+
mode: RunOnMode
169+
onPick: (mode: RunOnMode) => void
170+
}
171+
172+
const RunOnSelector = ({mode, onPick}: RunOnSelectorProps) => {
173+
const {token} = theme.useToken()
174+
const [open, setOpen] = useState(false)
175+
const [hovered, setHovered] = useState<RunOnMode | null>(null)
176+
const current = MODES.find((m) => m.key === mode) ?? MODES.find((m) => m.key === "app")!
177+
178+
const overlay = (
179+
<div
180+
className="w-[460px] rounded-lg border border-solid p-1.5"
181+
style={{
182+
background: token.colorBgElevated,
183+
borderColor: token.colorBorderSecondary,
184+
boxShadow: token.boxShadowSecondary,
185+
}}
186+
>
187+
<div
188+
className="px-2.5 pb-1.5 pt-1 text-[11px] font-semibold uppercase tracking-[0.04em]"
189+
style={{color: token.colorTextQuaternary}}
190+
>
191+
What should the evaluator run on?
192+
</div>
193+
{MODES.map((m) => {
194+
const selected = m.key === mode
195+
const isHovered = hovered === m.key
196+
const background = selected
197+
? token.controlItemBgActive
198+
: isHovered && !m.disabled
199+
? token.colorFillTertiary
200+
: "transparent"
201+
return (
202+
<div
203+
key={m.key}
204+
role="button"
205+
aria-disabled={m.disabled}
206+
onMouseEnter={() => setHovered(m.key)}
207+
onMouseLeave={() => setHovered((h) => (h === m.key ? null : h))}
208+
onClick={() => {
209+
if (m.disabled) return
210+
onPick(m.key)
211+
setOpen(false)
212+
}}
213+
className={clsx(
214+
"flex items-start gap-3 rounded-md p-2.5",
215+
m.disabled ? "cursor-default opacity-55" : "cursor-pointer",
216+
)}
217+
style={{background}}
218+
>
219+
<span
220+
className="mt-0.5 flex w-[18px] shrink-0 justify-center"
221+
style={{color: token.colorPrimary}}
222+
>
223+
{selected && <CheckIcon size={16} />}
224+
</span>
225+
<div className="min-w-0 flex-1">
226+
<div
227+
className="flex items-center gap-2 text-[14px] font-medium"
228+
style={{color: token.colorText}}
229+
>
230+
<m.Icon size={15} />
231+
{m.label}
232+
{m.badge === "default" && (
233+
<span
234+
className="rounded-full px-[7px] py-px text-[10.5px] font-semibold"
235+
style={{
236+
background: token.colorPrimary,
237+
color: token.colorTextLightSolid,
238+
}}
239+
>
240+
default
241+
</span>
242+
)}
243+
{m.badge === "soon" && (
244+
<span
245+
className="rounded-full px-[7px] py-px text-[10.5px] font-semibold"
246+
style={{background: token.gold1, color: token.gold8}}
247+
>
248+
soon
249+
</span>
250+
)}
251+
</div>
252+
<div
253+
className="mt-0.5 text-[12.5px] leading-snug"
254+
style={{color: token.colorTextTertiary}}
255+
>
256+
{m.desc}
257+
</div>
258+
<div className="mt-2">
259+
<FlowPills steps={m.flow} token={token} />
260+
</div>
261+
</div>
262+
</div>
263+
)
264+
})}
265+
</div>
266+
)
267+
268+
return (
269+
<Dropdown
270+
open={open}
271+
onOpenChange={setOpen}
272+
trigger={["click"]}
273+
placement="bottomLeft"
274+
popupRender={() => overlay}
275+
>
276+
<Button
277+
size="small"
278+
className="flex items-center gap-1.5 font-medium"
279+
style={{
280+
background: token.colorPrimaryBg,
281+
borderColor: token.colorPrimaryBorder,
282+
}}
283+
>
284+
<span className="font-normal" style={{color: token.colorTextTertiary}}>
285+
Run on:
286+
</span>
287+
<current.Icon size={14} style={{color: token.colorText}} />
288+
<span className="truncate">{current.shortLabel}</span>
289+
<CaretDownIcon size={12} style={{color: token.colorTextTertiary}} />
290+
</Button>
291+
</Dropdown>
292+
)
293+
}
294+
295+
export default RunOnSelector

0 commit comments

Comments
 (0)