Skip to content

Commit 68680ba

Browse files
jedrazbkibanamachineelasticmachine
authored
[OneChat] Create / Edit / Delete Agent profile UX (#226602)
## Summary Create / delete / edit agents UX Ability to associate tools with agents ### Demo https://github.com/user-attachments/assets/917ba9d2-e787-4cea-8e7f-ac77a863ac87 --------- Co-authored-by: kibanamachine <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
1 parent a120ce5 commit 68680ba

File tree

21 files changed

+1285
-70
lines changed

21 files changed

+1285
-70
lines changed

x-pack/platform/plugins/shared/onechat/public/application/components/agents/agents_list.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { useEffect } from 'react';
9+
import {
10+
EuiForm,
11+
EuiFormRow,
12+
EuiFieldText,
13+
EuiTextArea,
14+
EuiButton,
15+
EuiSpacer,
16+
EuiFlexGroup,
17+
EuiFlexItem,
18+
EuiTitle,
19+
EuiCallOut,
20+
EuiLoadingSpinner,
21+
} from '@elastic/eui';
22+
import { i18n } from '@kbn/i18n';
23+
import { AgentProfile } from '@kbn/onechat-common';
24+
import { useForm, Controller, FormProvider } from 'react-hook-form';
25+
import { useAgentEdit } from '../../../hooks/agents/use_agent_edit';
26+
import { useKibana } from '../../../hooks/use_kibana';
27+
import { useNavigation } from '../../../hooks/use_navigation';
28+
import { appPaths } from '../../../utils/app_paths';
29+
import { useAgentDelete } from '../../../hooks/agents/use_agent_delete';
30+
import { ToolsSelection } from './tools_selection';
31+
32+
export interface AgentFormProps {
33+
agentId?: string;
34+
}
35+
36+
type AgentFormData = Omit<AgentProfile, 'createdAt' | 'updatedAt'>;
37+
38+
export const AgentForm: React.FC<AgentFormProps> = ({ agentId }) => {
39+
const { navigateToOnechatUrl } = useNavigation();
40+
const {
41+
services: { notifications },
42+
} = useKibana();
43+
44+
const isCreateMode = !agentId;
45+
46+
const onSaveSuccess = () => {
47+
notifications.toasts.addSuccess(
48+
isCreateMode
49+
? i18n.translate('xpack.onechat.agents.createSuccessMessage', {
50+
defaultMessage: 'Agent created successfully',
51+
})
52+
: i18n.translate('xpack.onechat.agents.updateSuccessMessage', {
53+
defaultMessage: 'Agent updated successfully',
54+
})
55+
);
56+
navigateToOnechatUrl(appPaths.agents.list);
57+
};
58+
59+
const onSaveError = (err: Error) => {
60+
const errorMessage = isCreateMode
61+
? i18n.translate('xpack.onechat.agents.createErrorMessage', {
62+
defaultMessage: 'Failed to create agent',
63+
})
64+
: i18n.translate('xpack.onechat.agents.updateErrorMessage', {
65+
defaultMessage: 'Failed to update agent',
66+
});
67+
notifications.toasts.addError(err, {
68+
title: errorMessage,
69+
});
70+
};
71+
72+
const { deleteAgent, isDeleting } = useAgentDelete({
73+
onSuccess: () => {
74+
notifications.toasts.addSuccess(
75+
i18n.translate('xpack.onechat.agents.deleteSuccessMessage', {
76+
defaultMessage: 'Agent deleted successfully',
77+
})
78+
);
79+
navigateToOnechatUrl(appPaths.agents.list);
80+
},
81+
onError: (err: Error) => {
82+
notifications.toasts.addError(err, {
83+
title: i18n.translate('xpack.onechat.agents.deleteErrorMessage', {
84+
defaultMessage: 'Failed to delete agent',
85+
}),
86+
});
87+
},
88+
});
89+
90+
const {
91+
state: agentState,
92+
isLoading,
93+
isSubmitting,
94+
submit,
95+
tools,
96+
error,
97+
} = useAgentEdit({
98+
agentId,
99+
onSaveSuccess,
100+
onSaveError,
101+
});
102+
103+
const formMethods = useForm<AgentFormData>({
104+
defaultValues: { ...agentState },
105+
mode: 'onChange',
106+
});
107+
const { control, handleSubmit, reset, formState } = formMethods;
108+
109+
useEffect(() => {
110+
if (agentState && !isLoading) {
111+
reset(agentState);
112+
}
113+
}, [agentState, isLoading, reset]);
114+
115+
if (isLoading) {
116+
return (
117+
<EuiFlexGroup justifyContent="center" alignItems="center" style={{ minHeight: '200px' }}>
118+
<EuiFlexItem grow={false}>
119+
<EuiLoadingSpinner size="xl" />
120+
</EuiFlexItem>
121+
</EuiFlexGroup>
122+
);
123+
}
124+
125+
if (error) {
126+
return (
127+
<EuiCallOut
128+
title={i18n.translate('xpack.onechat.agents.errorTitle', {
129+
defaultMessage: 'Error loading agent',
130+
})}
131+
color="danger"
132+
iconType="error"
133+
>
134+
<p>
135+
{i18n.translate('xpack.onechat.agents.errorMessage', {
136+
defaultMessage: 'Unable to load the agent. {errorMessage}',
137+
values: {
138+
errorMessage: (error as Error)?.message || String(error),
139+
},
140+
})}
141+
</p>
142+
<EuiSpacer size="m" />
143+
<EuiButton onClick={() => navigateToOnechatUrl(appPaths.agents.list)}>
144+
{i18n.translate('xpack.onechat.agents.backToListButton', {
145+
defaultMessage: 'Back to agents list',
146+
})}
147+
</EuiButton>
148+
</EuiCallOut>
149+
);
150+
}
151+
152+
const onSubmit = (data: AgentFormData) => {
153+
submit(data);
154+
};
155+
156+
const isFormDisabled = isLoading || isSubmitting || isDeleting;
157+
158+
return (
159+
<FormProvider {...formMethods}>
160+
<EuiForm component="form" onSubmit={handleSubmit(onSubmit)}>
161+
<EuiFormRow
162+
label={i18n.translate('xpack.onechat.agents.form.idLabel', {
163+
defaultMessage: 'Agent ID',
164+
})}
165+
isInvalid={!!formState.errors.id}
166+
error={formState.errors.id?.message}
167+
>
168+
<Controller
169+
name="id"
170+
control={control}
171+
rules={{
172+
required: i18n.translate('xpack.onechat.agents.form.idRequired', {
173+
defaultMessage: 'Agent ID is required',
174+
}),
175+
}}
176+
render={({ field }) => (
177+
<EuiFieldText
178+
{...field}
179+
disabled={isFormDisabled || !isCreateMode}
180+
placeholder={
181+
isCreateMode
182+
? i18n.translate('xpack.onechat.agents.form.idPlaceholder', {
183+
defaultMessage: 'Enter agent ID',
184+
})
185+
: ''
186+
}
187+
isInvalid={!!formState.errors.id}
188+
/>
189+
)}
190+
/>
191+
</EuiFormRow>
192+
<EuiFormRow
193+
label={i18n.translate('xpack.onechat.agents.form.nameLabel', {
194+
defaultMessage: 'Agent Name',
195+
})}
196+
isInvalid={!!formState.errors.name}
197+
error={formState.errors.name?.message}
198+
>
199+
<Controller
200+
name="name"
201+
control={control}
202+
rules={{
203+
required: i18n.translate('xpack.onechat.agents.form.nameRequired', {
204+
defaultMessage: 'Agent name is required',
205+
}),
206+
}}
207+
render={({ field }) => (
208+
<EuiFieldText
209+
{...field}
210+
disabled={isFormDisabled}
211+
isInvalid={!!formState.errors.name}
212+
/>
213+
)}
214+
/>
215+
</EuiFormRow>
216+
<EuiFormRow
217+
label={i18n.translate('xpack.onechat.agents.form.descriptionLabel', {
218+
defaultMessage: 'Description',
219+
})}
220+
>
221+
<Controller
222+
name="description"
223+
control={control}
224+
render={({ field }) => (
225+
<EuiFieldText
226+
{...field}
227+
disabled={isFormDisabled}
228+
isInvalid={!!formState.errors.description}
229+
/>
230+
)}
231+
/>
232+
</EuiFormRow>
233+
<EuiFormRow
234+
label={i18n.translate('xpack.onechat.agents.form.customInstructionsLabel', {
235+
defaultMessage: 'Custom Instructions',
236+
})}
237+
>
238+
<Controller
239+
name="customInstructions"
240+
control={control}
241+
render={({ field }) => (
242+
<EuiTextArea
243+
{...field}
244+
rows={4}
245+
disabled={isFormDisabled}
246+
isInvalid={!!formState.errors.customInstructions}
247+
/>
248+
)}
249+
/>
250+
</EuiFormRow>
251+
252+
<EuiSpacer size="l" />
253+
<EuiTitle size="m">
254+
<h4>
255+
{i18n.translate('xpack.onechat.agents.form.toolsSelectionTitle', {
256+
defaultMessage: 'Configure Agent Tools',
257+
})}
258+
</h4>
259+
</EuiTitle>
260+
<EuiSpacer size="l" />
261+
<Controller
262+
name="toolSelection"
263+
control={control}
264+
render={({ field }) => (
265+
<ToolsSelection
266+
tools={tools}
267+
toolsLoading={isLoading}
268+
selectedTools={field.value}
269+
onToolsChange={field.onChange}
270+
disabled={isFormDisabled}
271+
/>
272+
)}
273+
/>
274+
275+
<EuiSpacer size="m" />
276+
<EuiFlexGroup justifyContent="spaceBetween">
277+
<EuiFlexItem grow={false}>
278+
<EuiFlexGroup gutterSize="s">
279+
<EuiFlexItem grow={false}>
280+
<EuiButton
281+
type="submit"
282+
fill
283+
iconType="save"
284+
isLoading={isSubmitting}
285+
disabled={isFormDisabled || !formState.isValid}
286+
>
287+
{isCreateMode
288+
? i18n.translate('xpack.onechat.agents.form.createButton', {
289+
defaultMessage: 'Create Agent',
290+
})
291+
: i18n.translate('xpack.onechat.agents.form.saveButton', {
292+
defaultMessage: 'Save Changes',
293+
})}
294+
</EuiButton>
295+
</EuiFlexItem>
296+
<EuiFlexItem grow={false}>
297+
<EuiButton
298+
onClick={() => navigateToOnechatUrl(appPaths.agents.list)}
299+
disabled={isFormDisabled}
300+
>
301+
{i18n.translate('xpack.onechat.agents.form.cancelButton', {
302+
defaultMessage: 'Cancel',
303+
})}
304+
</EuiButton>
305+
</EuiFlexItem>
306+
</EuiFlexGroup>
307+
</EuiFlexItem>
308+
{!isCreateMode && (
309+
<EuiFlexItem grow={false}>
310+
<EuiButton
311+
color="danger"
312+
iconType="trash"
313+
onClick={() => deleteAgent(agentId!)}
314+
disabled={isFormDisabled}
315+
isLoading={isDeleting}
316+
>
317+
{i18n.translate('xpack.onechat.agents.form.deleteButton', {
318+
defaultMessage: 'Delete',
319+
})}
320+
</EuiButton>
321+
</EuiFlexItem>
322+
)}
323+
</EuiFlexGroup>
324+
</EuiForm>
325+
</FormProvider>
326+
);
327+
};

0 commit comments

Comments
 (0)