Skip to content

Commit 030aedf

Browse files
committed
feat(ui): Action playground config and parameter forms
Signed-off-by: Richard Palethorpe <io@richiejp.com>
1 parent cb15f92 commit 030aedf

File tree

5 files changed

+170
-28
lines changed

5 files changed

+170
-28
lines changed

webui/app.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,30 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
429429
}
430430
}
431431

432+
func (a *App) GetActionDefinition(pool *state.AgentPool) func(c *fiber.Ctx) error {
433+
return func(c *fiber.Ctx) error {
434+
payload := struct {
435+
Config map[string]string `json:"config"`
436+
}{}
437+
438+
if err := c.BodyParser(&payload); err != nil {
439+
xlog.Error("Error parsing action payload", "error", err)
440+
return errorJSONMessage(c, err.Error())
441+
}
442+
443+
actionName := c.Params("name")
444+
445+
xlog.Debug("Executing action", "action", actionName, "config", payload.Config)
446+
a, err := services.Action(actionName, "", payload.Config, pool)
447+
if err != nil {
448+
xlog.Error("Error creating action", "error", err)
449+
return errorJSONMessage(c, err.Error())
450+
}
451+
452+
return c.JSON(a.Definition())
453+
}
454+
}
455+
432456
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
433457
return func(c *fiber.Ctx) error {
434458
payload := struct {

webui/react-ui/src/pages/ActionsPlayground.jsx

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useEffect } from 'react';
22
import { useOutletContext, useNavigate } from 'react-router-dom';
3-
import { actionApi } from '../utils/api';
3+
import { actionApi, agentApi } from '../utils/api';
4+
import FormFieldDefinition from '../components/common/FormFieldDefinition';
45

56
function ActionsPlayground() {
67
const { showToast } = useOutletContext();
@@ -12,6 +13,10 @@ function ActionsPlayground() {
1213
const [result, setResult] = useState(null);
1314
const [loading, setLoading] = useState(false);
1415
const [loadingActions, setLoadingActions] = useState(true);
16+
const [actionMetadata, setActionMetadata] = useState(null);
17+
const [agentMetadata, setAgentMetadata] = useState(null);
18+
const [configFields, setConfigFields] = useState([]);
19+
const [paramFields, setParamFields] = useState([]);
1520

1621
// Update document title
1722
useEffect(() => {
@@ -36,21 +41,86 @@ function ActionsPlayground() {
3641
};
3742

3843
fetchActions();
39-
}, [showToast]);
44+
}, []);
45+
46+
// Fetch agent metadata on mount
47+
useEffect(() => {
48+
const fetchAgentMetadata = async () => {
49+
try {
50+
const metadata = await agentApi.getAgentConfigMetadata();
51+
setAgentMetadata(metadata);
52+
} catch (err) {
53+
console.error('Error fetching agent metadata:', err);
54+
showToast('Failed to load agent metadata', 'error');
55+
}
56+
};
57+
58+
fetchAgentMetadata();
59+
}, []);
60+
61+
// Fetch action definition when action is selected or config changes
62+
useEffect(() => {
63+
if (!selectedAction) return;
64+
65+
const fetchActionDefinition = async () => {
66+
try {
67+
// Get config fields from agent metadata
68+
const actionMeta = agentMetadata?.actions?.find(action => action.name === selectedAction);
69+
const configFields = actionMeta?.fields || [];
70+
setConfigFields(configFields);
71+
72+
// Parse current config to pass to action definition
73+
let currentConfig = {};
74+
try {
75+
currentConfig = JSON.parse(configJson);
76+
} catch (err) {
77+
console.error('Error parsing current config:', err);
78+
}
79+
80+
// Get parameter fields from action definition
81+
const paramFields = await actionApi.getActionDefinition(selectedAction, currentConfig);
82+
setParamFields(paramFields);
83+
84+
// Reset JSON to match the new fields
85+
setConfigJson(JSON.stringify(currentConfig, null, 2));
86+
setParamsJson(JSON.stringify({}, null, 2));
87+
setResult(null);
88+
} catch (err) {
89+
console.error('Error fetching action definition:', err);
90+
showToast('Failed to load action definition', 'error');
91+
}
92+
};
93+
94+
fetchActionDefinition();
95+
}, [selectedAction, configJson, agentMetadata]);
4096

4197
// Handle action selection
4298
const handleActionChange = (e) => {
4399
setSelectedAction(e.target.value);
100+
setConfigJson('{}');
101+
setParamsJson('{}');
44102
setResult(null);
45103
};
46104

47-
// Handle JSON input changes
48-
const handleConfigChange = (e) => {
49-
setConfigJson(e.target.value);
105+
// Handle form field changes
106+
const handleConfigChange = (field, value) => {
107+
try {
108+
const config = JSON.parse(configJson);
109+
config[field] = value;
110+
setConfigJson(JSON.stringify(config, null, 2));
111+
} catch (err) {
112+
console.error('Error updating config:', err);
113+
}
50114
};
51115

52-
const handleParamsChange = (e) => {
53-
setParamsJson(e.target.value);
116+
const handleParamsChange = (field, value) => {
117+
try {
118+
const params = JSON.parse(paramsJson);
119+
params[field] = value;
120+
setParamsJson(JSON.stringify(params, null, 2));
121+
} catch (err) {
122+
console.error('Error updating params:', err);
123+
}
54124
};
55125

56126
// Execute the selected action
@@ -138,31 +208,23 @@ function ActionsPlayground() {
138208
<h2>Action Configuration</h2>
139209

140210
<form onSubmit={handleExecuteAction}>
141-
<div className="form-group mb-6">
142-
<label htmlFor="config-json">Configuration (JSON):</label>
143-
<textarea
144-
id="config-json"
145-
value={configJson}
211+
{configFields.length > 0 && (
212+
<FormFieldDefinition
213+
fields={configFields}
214+
values={JSON.parse(configJson)}
146215
onChange={handleConfigChange}
147-
className="form-control"
148-
rows="5"
149-
placeholder='{"key": "value"}'
216+
idPrefix="config_"
150217
/>
151-
<p className="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
152-
</div>
218+
)}
153219

154-
<div className="form-group mb-6">
155-
<label htmlFor="params-json">Parameters (JSON):</label>
156-
<textarea
157-
id="params-json"
158-
value={paramsJson}
220+
{paramFields.length > 0 && (
221+
<FormFieldDefinition
222+
fields={paramFields}
223+
values={JSON.parse(paramsJson)}
159224
onChange={handleParamsChange}
160-
className="form-control"
161-
rows="5"
162-
placeholder='{"key": "value"}'
225+
idPrefix="param_"
163226
/>
164-
<p className="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
165-
</div>
227+
)}
166228

167229
<div className="flex justify-end">
168230
<button

webui/react-ui/src/utils/api.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,49 @@ const buildUrl = (endpoint) => {
2424
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
2525
};
2626

27+
// Helper function to convert ActionDefinition to FormFieldDefinition format
28+
const convertActionDefinitionToFields = (definition) => {
29+
if (!definition || !definition.Properties) {
30+
return [];
31+
}
32+
33+
const fields = [];
34+
const required = definition.Required || [];
35+
36+
Object.entries(definition.Properties).forEach(([name, property]) => {
37+
const field = {
38+
name,
39+
label: name.charAt(0).toUpperCase() + name.slice(1),
40+
type: 'text', // Default to text, we'll enhance this later
41+
required: required.includes(name),
42+
helpText: property.Description || '',
43+
defaultValue: property.Default,
44+
};
45+
46+
if (property.Enum && property.Enum.length > 0) {
47+
field.type = 'select';
48+
field.options = property.Enum;
49+
} else {
50+
// Handle different property types
51+
switch (property.Type) {
52+
case 'integer':
53+
field.type = 'number';
54+
field.min = property.Minimum;
55+
field.max = property.Maximum;
56+
break;
57+
case 'boolean':
58+
field.type = 'checkbox';
59+
break;
60+
}
61+
// TODO: Handle Object and Array types which require nested fields
62+
}
63+
64+
fields.push(field);
65+
});
66+
67+
return fields;
68+
};
69+
2770
// Agent-related API calls
2871
export const agentApi = {
2972
// Get list of all agents
@@ -215,7 +258,18 @@ export const actionApi = {
215258
});
216259
return handleResponse(response);
217260
},
218-
261+
262+
// Get action definition
263+
getActionDefinition: async (name, config = {}) => {
264+
const response = await fetch(buildUrl(API_CONFIG.endpoints.actionDefinition(name)), {
265+
method: 'POST',
266+
headers: API_CONFIG.headers,
267+
body: JSON.stringify(config),
268+
});
269+
const definition = await handleResponse(response);
270+
return convertActionDefinitionToFields(definition);
271+
},
272+
219273
// Execute an action for an agent
220274
executeAction: async (name, actionData) => {
221275
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {

webui/react-ui/src/utils/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const API_CONFIG = {
4343

4444
// Action endpoints
4545
listActions: '/api/actions',
46+
actionDefinition: (name) => `/api/actions/${name}/definition`,
4647
executeAction: (name) => `/api/action/${name}/run`,
4748

4849
// Status endpoint

webui/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
187187
// Add endpoint for getting agent config metadata
188188
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta())
189189

190+
webapp.Post("/api/action/:name/definition", app.GetActionDefinition(pool))
190191
webapp.Post("/api/action/:name/run", app.ExecuteAction(pool))
191192
webapp.Get("/api/actions", app.ListActions())
192193

0 commit comments

Comments
 (0)