Skip to content

Commit e685ed0

Browse files
authored
feat(ui): add full control JSON editing and create-from-JSON (#147)
## Summary - add `Full JSON` mode for editing the entire control definition from the control modal - add `From JSON` in the create-control flow so a control can be created directly from JSON without picking an evaluator first - reuse a shared JSON editor for both full control definitions and evaluator config JSON - add helper text clarifying that the control JSON editor expects the raw control definition, not `data` / `name` / `id` wrapper objects - add Playwright coverage for the JSON create/edit flows ## Testing - `make ui-typecheck` - `make ui-lint` *(passes; existing `no-img-element` warnings remain in `ui/src/core/layouts/app-layout.tsx`)* ## Screenshots New control UI now has `From JSON` button` <img width="841" height="613" alt="image" src="https://github.com/user-attachments/assets/0db00e89-5fd0-4cca-bbee-ff2378a1fb87" /> When you click on it you get this <img width="794" height="916" alt="Screenshot 2026-03-24 at 9 11 12 PM" src="https://github.com/user-attachments/assets/f487e36a-b635-4fbf-b528-e83cb1404f9c" /> you can also edit full json in control now <img width="802" height="917" alt="Screenshot 2026-03-24 at 9 11 38 PM" src="https://github.com/user-attachments/assets/7207ce13-ec49-4137-9928-7bcdcd27eb64" />
1 parent 5d70c7d commit e685ed0

8 files changed

Lines changed: 682 additions & 151 deletions

File tree

‎ui/src/core/page-components/agent-detail/modals/add-new-control/index.tsx‎

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,33 @@ function getDefaultConfigForEvaluator(
5151
return DEFAULT_EVALUATOR_CONFIGS[evaluatorId] ?? {};
5252
}
5353

54+
function buildJsonDraftControl(agentName: string) {
55+
return {
56+
id: 0,
57+
name: `json-control-for-${sanitizeControlNamePart(agentName)}`,
58+
control: {
59+
description: '',
60+
enabled: true,
61+
execution: 'server' as const,
62+
scope: {
63+
step_types: ['llm'],
64+
stages: ['post'] as ('post' | 'pre')[],
65+
},
66+
condition: {
67+
selector: {
68+
path: '*',
69+
},
70+
evaluator: {
71+
name: 'regex',
72+
config: getDefaultConfigForEvaluator('regex'),
73+
},
74+
},
75+
action: { decision: 'deny' as const },
76+
tags: [],
77+
},
78+
};
79+
}
80+
5481
type AddNewControlModalProps = {
5582
opened: boolean;
5683
onClose: () => void;
@@ -90,6 +117,12 @@ export function AddNewControlModal({
90117
});
91118
};
92119

120+
const handleFromJsonClick = () => {
121+
openModal(MODAL_NAMES.CONTROL_STORE, {
122+
submodal: SUBMODAL_NAMES.CREATE,
123+
});
124+
};
125+
93126
const handleEditModalClose = () => {
94127
closeSubmodal();
95128
};
@@ -110,31 +143,34 @@ export function AddNewControlModal({
110143
}, [evaluatorsData]);
111144

112145
const draftControl = useMemo(() => {
113-
if (!selectedEvaluator) return null;
114-
const name = `${sanitizeControlNamePart(selectedEvaluator.name)}-control-for-${sanitizeControlNamePart(agentName)}`;
115-
return {
116-
id: 0,
117-
name,
118-
control: {
119-
description: selectedEvaluator.description,
120-
enabled: true,
121-
execution: 'server' as const,
122-
scope: {
123-
step_types: ['llm'],
124-
stages: ['post'] as ('post' | 'pre')[],
125-
},
126-
condition: {
127-
selector: {
128-
path: '*',
146+
if (selectedEvaluator) {
147+
const name = `${sanitizeControlNamePart(selectedEvaluator.name)}-control-for-${sanitizeControlNamePart(agentName)}`;
148+
return {
149+
id: 0,
150+
name,
151+
control: {
152+
description: selectedEvaluator.description,
153+
enabled: true,
154+
execution: 'server' as const,
155+
scope: {
156+
step_types: ['llm'],
157+
stages: ['post'] as ('post' | 'pre')[],
129158
},
130-
evaluator: {
131-
name: selectedEvaluator.id,
132-
config: getDefaultConfigForEvaluator(selectedEvaluator.id),
159+
condition: {
160+
selector: {
161+
path: '*',
162+
},
163+
evaluator: {
164+
name: selectedEvaluator.id,
165+
config: getDefaultConfigForEvaluator(selectedEvaluator.id),
166+
},
133167
},
168+
action: { decision: 'deny' as const },
134169
},
135-
action: { decision: 'deny' as const },
136-
},
137-
};
170+
};
171+
}
172+
173+
return buildJsonDraftControl(agentName);
138174
}, [selectedEvaluator, agentName]);
139175

140176
const columns: ColumnDef<EvaluatorInfo & { id: string }>[] = [
@@ -219,7 +255,7 @@ export function AddNewControlModal({
219255
</Button>
220256
</Group>
221257
<Text size="sm" c="dimmed">
222-
Select an evaluator to create a new control
258+
Select an evaluator to create a new control or start from JSON
223259
</Text>
224260
</Box>
225261
<Divider />
@@ -240,20 +276,30 @@ export function AddNewControlModal({
240276
value={searchQuery}
241277
onChange={(e) => setSearchQuery(e.target.value)}
242278
/>
243-
<Text size="sm" c="dimmed">
244-
Learn here on how to add new type of evaluator.{' '}
245-
<Text
246-
component="a"
247-
href="https://github.com/agentcontrol/agent-control/blob/main/README.md"
248-
c="blue"
279+
<Group gap="md" wrap="nowrap">
280+
<Button
281+
variant="outline"
249282
size="sm"
250-
td="none"
251-
target="_blank"
252-
rel="noreferrer"
283+
data-testid="from-json-button"
284+
onClick={handleFromJsonClick}
253285
>
254-
Docs ↗
286+
From JSON
287+
</Button>
288+
<Text size="sm" c="dimmed">
289+
Learn here on how to add new type of evaluator.{' '}
290+
<Text
291+
component="a"
292+
href="https://github.com/agentcontrol/agent-control/blob/main/README.md"
293+
c="blue"
294+
size="sm"
295+
td="none"
296+
target="_blank"
297+
rel="noreferrer"
298+
>
299+
Docs ↗
300+
</Text>
255301
</Text>
256-
</Text>
302+
</Group>
257303
</Group>
258304

259305
{/* Table or Empty State */}
@@ -307,6 +353,7 @@ export function AddNewControlModal({
307353
control={draftControl}
308354
agentId={agentId}
309355
mode="create"
356+
initialEditorMode={selectedEvaluator ? 'form' : 'json'}
310357
onClose={handleEditModalClose}
311358
onSuccess={handleEditModalSuccess}
312359
/>

‎ui/src/core/page-components/agent-detail/modals/edit-control/control-condition.ts‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import type { ControlDefinition } from '@/core/api/types';
22
import type { AnyEvaluatorDefinition } from '@/core/evaluators';
33
import { getEvaluator } from '@/core/evaluators';
44

5-
const COMPOSITE_CONDITION_EDITING_MESSAGE =
6-
'This control uses a composite condition tree. This PR keeps the old single-condition UI, so saving will preserve the existing tree without editing it.';
7-
85
export type LeafConditionDetails = {
96
selectorPath: string;
107
evaluatorName: string;
@@ -16,7 +13,6 @@ export type ControlConditionState = {
1613
evaluatorId: string;
1714
evaluator: AnyEvaluatorDefinition | undefined;
1815
canEditLeafCondition: boolean;
19-
conditionEditingMessage: string | null;
2016
};
2117

2218
function getLeafConditionDetails(
@@ -46,9 +42,6 @@ export function getControlConditionState(
4642
evaluatorId,
4743
evaluator,
4844
canEditLeafCondition: Boolean(leafCondition),
49-
conditionEditingMessage: leafCondition
50-
? null
51-
: COMPOSITE_CONDITION_EDITING_MESSAGE,
5245
};
5346
}
5447

0 commit comments

Comments
 (0)